testsAndMisc-archive/python_pkg/keyboard_coop/main.py

515 lines
17 KiB
Python
Raw Normal View History

"""Keyboard cooperative word game using Pygame.
Players take turns selecting adjacent keys to form valid English words.
"""
from dataclasses import dataclass, field
import json
import logging
from pathlib import Path
import secrets
import sys
import pygame
_logger = logging.getLogger(__name__)
# Use cryptographically secure random number generator
_rng = secrets.SystemRandom()
# Initialize Pygame
pygame.init()
# Constants
SCREEN_WIDTH = 1366
SCREEN_HEIGHT = 768
BACKGROUND_COLOR = (30, 30, 40)
KEYBOARD_COLOR = (60, 60, 70)
KEY_COLOR = (80, 80, 90)
KEY_HOVER_COLOR = (100, 100, 110)
KEY_SELECTED_COLOR = (150, 150, 200)
KEY_AVAILABLE_COLOR = (100, 150, 100)
TEXT_COLOR = (255, 255, 255)
PLAYER_COLORS = [(255, 100, 100), (100, 100, 255)]
MIN_WORD_LENGTH = 3
# Keyboard layout
KEYBOARD_LAYOUT = [
["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
["a", "s", "d", "f", "g", "h", "j", "k", "l"],
["z", "x", "c", "v", "b", "n", "m"],
]
# Key adjacency mapping
KEY_ADJACENCY = {
"q": ["w", "a", "s"],
"w": ["q", "e", "a", "s", "d"],
"e": ["w", "r", "s", "d", "f"],
"r": ["e", "t", "d", "f", "g"],
"t": ["r", "y", "f", "g", "h"],
"y": ["t", "u", "g", "h", "j"],
"u": ["y", "i", "h", "j", "k"],
"i": ["u", "o", "j", "k", "l"],
"o": ["i", "p", "k", "l"],
"p": ["o", "l"],
"a": ["q", "w", "s", "z", "x"],
"s": ["q", "w", "e", "a", "d", "z", "x", "c"],
"d": ["w", "e", "r", "s", "f", "x", "c", "v"],
"f": ["e", "r", "t", "d", "g", "c", "v", "b"],
"g": ["r", "t", "y", "f", "h", "v", "b", "n"],
"h": ["t", "y", "u", "g", "j", "b", "n", "m"],
"j": ["y", "u", "i", "h", "k", "n", "m"],
"k": ["u", "i", "o", "j", "l", "m"],
"l": ["i", "o", "p", "k"],
"z": ["a", "s", "x"],
"x": ["a", "s", "d", "z", "c"],
"c": ["s", "d", "f", "x", "v"],
"v": ["d", "f", "g", "c", "b"],
"b": ["f", "g", "h", "v", "n"],
"n": ["g", "h", "j", "b", "m"],
"m": ["h", "j", "k", "n"],
}
@dataclass
class GameState:
"""State for an ongoing keyboard coop game."""
current_player: int = 0
current_word: str = ""
selected_letters: list[str] = field(default_factory=list)
score: int = 0
game_over: bool = False
message: str = "Player 1: Choose any letter to start!"
@dataclass
class KeyboardState:
"""State for the keyboard layout and adjacency."""
layout: list[list[str]] = field(default_factory=list)
available_letters: set[str] = field(default_factory=set)
adjacency: dict[str, list[str]] = field(default_factory=dict)
positions: dict[str, pygame.Rect] = field(default_factory=dict)
class KeyboardCoopGame:
"""Main game class for the keyboard cooperative word game."""
def __init__(self) -> None:
"""Initialize the game window, fonts, and game state."""
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Keyboard Coop Game")
self.clock = pygame.time.Clock()
self.font = pygame.font.Font(None, 24)
self.large_font = pygame.font.Font(None, 32)
self.small_font = pygame.font.Font(None, 20)
# Load dictionary
self.dictionary = self._load_dictionary()
# Initialize game state
self.state = GameState()
# Initialize keyboard state
self.keyboard = KeyboardState()
# Generate random keyboard layout and adjacency
self._generate_random_keyboard()
def _load_dictionary(self) -> set[str]:
"""Load dictionary from words_dictionary.json file."""
try:
dictionary_path = Path(__file__).parent / "words_dictionary.json"
with dictionary_path.open(encoding="utf-8") as f:
dictionary_data = json.load(f)
# Convert to set for faster lookup (we only need the keys)
return set(dictionary_data.keys())
except FileNotFoundError:
_logger.warning(
"words_dictionary.json not found, using fallback dictionary"
)
# Fallback to a smaller dictionary if file not found
return {
"cat",
"dog",
"car",
"bat",
"rat",
"hat",
"mat",
"sat",
"fat",
"pat",
"the",
"and",
"for",
"are",
"but",
"not",
"you",
"all",
"can",
"had",
"her",
"was",
"one",
"our",
"out",
"day",
"get",
"has",
"him",
"his",
"how",
"man",
"new",
"now",
"old",
"see",
"two",
"way",
"who",
"boy",
"work",
"know",
"place",
"year",
"live",
"me",
"back",
"give",
"good",
}
except json.JSONDecodeError:
_logger.warning(
"Error reading words_dictionary.json, using fallback dictionary"
)
return {
"cat",
"dog",
"car",
"bat",
"rat",
"hat",
"mat",
"sat",
"fat",
"pat",
"the",
"and",
"for",
"are",
"but",
"not",
"you",
"all",
"can",
"had",
"work",
"know",
"place",
"year",
"live",
"me",
"back",
"give",
"good",
}
def _generate_random_keyboard(self) -> None:
"""Generate a random keyboard layout and calculate adjacencies."""
# All 26 letters
all_letters = list("abcdefghijklmnopqrstuvwxyz")
_rng.shuffle(all_letters)
# Create random layout with same structure as QWERTY (10-9-7)
self.keyboard.layout = [
all_letters[0:10], # Top row: 10 keys
all_letters[10:19], # Middle row: 9 keys
all_letters[19:26], # Bottom row: 7 keys
]
# Update available letters
self.keyboard.available_letters = set(all_letters)
# Calculate adjacencies based on new layout
self._calculate_adjacencies()
# Calculate key positions
self.keyboard.positions = self._calculate_key_positions()
def _calculate_adjacencies(self) -> None:
"""Calculate adjacencies based on current keyboard layout."""
self.keyboard.adjacency = {}
for row_idx, row in enumerate(self.keyboard.layout):
for col_idx, letter in enumerate(row):
adjacents = []
# Check all 8 directions (including diagonals)
directions = [
(-1, -1),
(-1, 0),
(-1, 1), # Above
(0, -1),
(0, 1), # Same row
(1, -1),
(1, 0),
(1, 1), # Below
]
for dr, dc in directions:
new_row = row_idx + dr
new_col = col_idx + dc
# Check bounds
if 0 <= new_row < len(self.keyboard.layout) and 0 <= new_col < len(
self.keyboard.layout[new_row]
):
adjacents.append(self.keyboard.layout[new_row][new_col])
self.keyboard.adjacency[letter] = adjacents
def _calculate_key_positions(self) -> dict[str, pygame.Rect]:
"""Calculate the position of each key on screen."""
positions: dict[str, pygame.Rect] = {}
key_width = 60
key_height = 60
key_spacing = 8
start_x = 50
start_y = 320
for row_idx, row in enumerate(self.keyboard.layout):
row_offset = row_idx * 30 # Offset for layout
for col_idx, key in enumerate(row):
x = start_x + col_idx * (key_width + key_spacing) + row_offset
y = start_y + row_idx * (key_height + key_spacing)
positions[key] = pygame.Rect(x, y, key_width, key_height)
return positions
def _get_key_at_position(self, pos: tuple[int, int]) -> str | None:
"""Get the key at the given mouse position."""
for key, rect in self.keyboard.positions.items():
if rect.collidepoint(pos):
return key
return None
def _is_valid_move(self, letter: str) -> bool:
"""Check if the letter is a valid move."""
if not self.state.selected_letters:
return True # First move can be any letter
last_letter = self.state.selected_letters[-1]
return letter in self.keyboard.adjacency[last_letter]
def _is_valid_word(self, word: str) -> bool:
"""Check if the word is in the dictionary."""
return word.lower() in self.dictionary
def _calculate_score(self, word_length: int) -> int:
"""Calculate score exponentially based on word length."""
if word_length < MIN_WORD_LENGTH:
return 0
return 2 ** (word_length - 2)
def _handle_letter_click(self, letter: str) -> None:
"""Handle clicking on a letter."""
if letter in self.keyboard.available_letters and self._is_valid_move(letter):
self.state.selected_letters.append(letter)
self.state.current_word += letter
# Update available letters to include adjacent letters AND the same letter
adjacent_letters = (
set(self.keyboard.adjacency[letter])
if letter in self.keyboard.adjacency
else set()
)
adjacent_letters.add(letter) # Allow selecting the same letter again
self.keyboard.available_letters = adjacent_letters
# Switch player
self.state.current_player = 1 - self.state.current_player
self.state.message = (
f"Player {self.state.current_player + 1}: Choose an adjacent letter!"
)
# If no valid moves available, force word submission
if not self.keyboard.available_letters:
self._submit_word()
def _submit_word(self) -> None:
"""Submit the current word and check if it's valid."""
if len(self.state.current_word) >= MIN_WORD_LENGTH and self._is_valid_word(
self.state.current_word
):
points = self._calculate_score(len(self.state.current_word))
self.state.score += points
self.state.message = (
f"'{self.state.current_word}' is valid! +{points} points "
f"(Total: {self.state.score}) - New keyboard!"
)
# Randomize keyboard layout after scoring
self._generate_random_keyboard()
elif len(self.state.current_word) < MIN_WORD_LENGTH:
self.state.message = (
f"'{self.state.current_word}' is too short! "
f"(minimum {MIN_WORD_LENGTH} letters)"
)
else:
self.state.message = f"'{self.state.current_word}' is not a valid word!"
# Reset for next word
self.state.current_word = ""
self.state.selected_letters = []
self.state.current_player = 0
def _reset_game(self) -> None:
"""Reset the game to initial state."""
self.state = GameState()
# Generate new random keyboard layout
self._generate_random_keyboard()
def _draw_keyboard(self) -> None:
"""Draw the virtual keyboard."""
mouse_pos = pygame.mouse.get_pos()
for letter, rect in self.keyboard.positions.items():
# Determine key color
if letter in self.state.selected_letters:
color = KEY_SELECTED_COLOR
elif letter in self.keyboard.available_letters:
color = KEY_AVAILABLE_COLOR
elif (
rect.collidepoint(mouse_pos)
and letter in self.keyboard.available_letters
):
color = KEY_HOVER_COLOR
else:
color = KEY_COLOR
# Draw key
pygame.draw.rect(self.screen, color, rect)
pygame.draw.rect(self.screen, TEXT_COLOR, rect, 2)
# Draw letter
text = self.small_font.render(letter.upper(), True, TEXT_COLOR)
text_rect = text.get_rect(center=rect.center)
self.screen.blit(text, text_rect)
def _draw_text_line(
self, text: str, pos: tuple[int, int], font: pygame.font.Font
) -> None:
"""Draw a single line of text at the given position."""
rendered = font.render(text, True, TEXT_COLOR)
self.screen.blit(rendered, pos)
def _draw_button(self, rect: pygame.Rect, label: str) -> None:
"""Draw a button with the given label."""
pygame.draw.rect(self.screen, KEY_COLOR, rect)
pygame.draw.rect(self.screen, TEXT_COLOR, rect, 2)
text = self.small_font.render(label, True, TEXT_COLOR)
self.screen.blit(text, text.get_rect(center=rect.center))
def _draw_ui(self) -> tuple[pygame.Rect, pygame.Rect]:
"""Draw the user interface."""
# Title and status
self._draw_text_line("Keyboard Coop Game", (30, 20), self.large_font)
self._draw_text_line(
f"Current Word: {self.state.current_word.upper()}", (30, 50), self.font
)
self._draw_text_line(f"Score: {self.state.score}", (30, 75), self.font)
# Current player with color
player_color = PLAYER_COLORS[self.state.current_player]
player_text = self.font.render(
f"Current Player: {self.state.current_player + 1}", True, player_color
)
self.screen.blit(player_text, (30, 100))
# Message
self._draw_text_line(self.state.message, (30, 125), self.font)
# Instructions
instructions = [
"Instructions:",
"• Take turns selecting adjacent letters",
"• Click letters or use keyboard to select",
"• Same letter can be selected consecutively",
"• Press ENTER to submit word (min 3 letters)",
"• Press R to reset game",
"• Score increases exponentially",
"• Keyboard randomizes after each valid word!",
]
for idx, instruction in enumerate(instructions):
self._draw_text_line(instruction, (750, 20 + idx * 22), self.small_font)
# Buttons
enter_rect = pygame.Rect(750, 190, 120, 40)
reset_rect = pygame.Rect(880, 190, 120, 40)
self._draw_button(enter_rect, "ENTER")
self._draw_button(reset_rect, "RESET")
return enter_rect, reset_rect
def _handle_click(self, pos: tuple[int, int]) -> None:
"""Handle mouse clicks."""
# Check if clicked on a key
key = self._get_key_at_position(pos)
if key:
self._handle_letter_click(key)
# Check UI buttons
enter_rect = pygame.Rect(750, 190, 120, 40)
reset_rect = pygame.Rect(880, 190, 120, 40)
if enter_rect.collidepoint(pos):
self._submit_word()
elif reset_rect.collidepoint(pos):
self._reset_game()
def run(self) -> None:
"""Main game loop."""
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # Left click
self._handle_click(event.pos)
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN:
self._submit_word()
elif event.key == pygame.K_r:
self._reset_game()
else:
# Handle letter key presses
key_name = pygame.key.name(event.key)
if len(key_name) == 1 and key_name.isalpha():
self._handle_letter_click(key_name.lower())
# Clear screen
self.screen.fill(BACKGROUND_COLOR)
# Draw everything
self._draw_keyboard()
self._draw_ui()
# Update display
pygame.display.flip()
self.clock.tick(60)
pygame.quit()
sys.exit()
if __name__ == "__main__":
game = KeyboardCoopGame()
game.run()