2025-11-30 14:45:55 +01:00
|
|
|
"""Keyboard cooperative word game using Pygame.
|
|
|
|
|
|
|
|
|
|
Players take turns selecting adjacent keys to form valid English words.
|
|
|
|
|
"""
|
|
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
import json
|
2025-11-30 14:36:13 +01:00
|
|
|
import logging
|
2025-11-30 23:03:03 +01:00
|
|
|
from pathlib import Path
|
2025-11-30 21:20:17 +01:00
|
|
|
import secrets
|
2025-11-30 13:42:16 +01:00
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
import pygame
|
2025-07-18 00:08:21 +02:00
|
|
|
|
2025-11-30 21:59:24 +01:00
|
|
|
_logger = logging.getLogger(__name__)
|
2025-11-30 14:36:13 +01:00
|
|
|
|
2025-11-30 21:20:17 +01:00
|
|
|
# Use cryptographically secure random number generator
|
|
|
|
|
_rng = secrets.SystemRandom()
|
|
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# 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)]
|
2025-11-30 15:01:14 +01:00
|
|
|
MIN_WORD_LENGTH = 3
|
2025-07-18 00:08:21 +02:00
|
|
|
|
|
|
|
|
# Keyboard layout
|
|
|
|
|
KEYBOARD_LAYOUT = [
|
2025-11-30 13:42:16 +01:00
|
|
|
["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"],
|
2025-07-18 00:08:21 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Key adjacency mapping
|
|
|
|
|
KEY_ADJACENCY = {
|
2025-11-30 13:42:16 +01:00
|
|
|
"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"],
|
2025-07-18 00:08:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
class KeyboardCoopGame:
|
2025-11-30 14:45:55 +01:00
|
|
|
"""Main game class for the keyboard cooperative word game."""
|
|
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def __init__(self) -> None:
|
2025-11-30 14:45:55 +01:00
|
|
|
"""Initialize the game window, fonts, and game state."""
|
2025-07-18 00:08:21 +02:00
|
|
|
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)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Load dictionary
|
|
|
|
|
self.dictionary = self.load_dictionary()
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Initialize game state
|
|
|
|
|
self.current_player = 0
|
|
|
|
|
self.current_word = ""
|
|
|
|
|
self.selected_letters = []
|
|
|
|
|
self.score = 0
|
|
|
|
|
self.game_over = False
|
|
|
|
|
self.message = "Player 1: Choose any letter to start!"
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Generate random keyboard layout and adjacency
|
|
|
|
|
self.generate_random_keyboard()
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Key positions
|
|
|
|
|
self.key_positions = self.calculate_key_positions()
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def load_dictionary(self) -> set[str]:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Load dictionary from words_dictionary.json file."""
|
2025-07-18 00:08:21 +02:00
|
|
|
try:
|
2025-11-30 23:03:03 +01:00
|
|
|
dictionary_path = Path(__file__).parent / "words_dictionary.json"
|
2025-11-30 13:42:16 +01:00
|
|
|
with open(dictionary_path, encoding="utf-8") as f:
|
2025-07-18 00:08:21 +02:00
|
|
|
dictionary_data = json.load(f)
|
|
|
|
|
# Convert to set for faster lookup (we only need the keys)
|
|
|
|
|
return set(dictionary_data.keys())
|
|
|
|
|
except FileNotFoundError:
|
2025-11-30 21:59:24 +01:00
|
|
|
_logger.warning(
|
2025-11-30 14:36:13 +01:00
|
|
|
"words_dictionary.json not found, using fallback dictionary"
|
|
|
|
|
)
|
2025-07-18 00:08:21 +02:00
|
|
|
# Fallback to a smaller dictionary if file not found
|
|
|
|
|
return {
|
2025-11-30 13:42:16 +01:00
|
|
|
"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",
|
2025-07-18 00:08:21 +02:00
|
|
|
}
|
|
|
|
|
except json.JSONDecodeError:
|
2025-11-30 21:59:24 +01:00
|
|
|
_logger.warning(
|
fix(lint): remove remaining global ignores with per-file ignores
Removed from global ignore list:
- PTH (pathlib) - per-file ignores for each file using os.path
- BLE001 (blind except) - per-file ignores for resilient error handling
- S603/S607 (subprocess) - per-file ignores for tests and trusted code
- S310 (URL open) - per-file ignores for test files
- S311 (random) - per-file ignores for non-crypto random usage
- S110 (try-except-pass) - per-file ignores for optional features
- LOG015 (root logger) - per-file ignores for scripts
- G004 (logging f-strings) - per-file ignores for all scripts
Per-file ignores added for:
- Test files: S603, S310, S607, BLE001, PTH
- lichess_bot/: BLE001, S110, S603, PTH, LOG015, G004
- stockfish_analysis/: BLE001, S110, PTH, LOG015, G004
- randomJPG/: S311, PTH, LOG015, G004
- poker-modifier-app/: S311, LOG015, G004
- And other affected files
Global ignore list now only contains:
- Formatter conflicts (D203/D213, COM812, ISC001)
- Style preferences (PERF401, RUF005, SIM*, B904, TRY*)
2025-11-30 20:43:17 +01:00
|
|
|
"Error reading words_dictionary.json, using fallback dictionary"
|
2025-11-30 14:25:35 +01:00
|
|
|
)
|
2025-07-18 00:08:21 +02:00
|
|
|
return {
|
2025-11-30 13:42:16 +01:00
|
|
|
"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",
|
2025-07-18 00:08:21 +02:00
|
|
|
}
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def generate_random_keyboard(self) -> None:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Generate a random keyboard layout and calculate adjacencies."""
|
2025-07-18 00:08:21 +02:00
|
|
|
# All 26 letters
|
2025-11-30 13:42:16 +01:00
|
|
|
all_letters = list("abcdefghijklmnopqrstuvwxyz")
|
2025-11-30 21:20:17 +01:00
|
|
|
_rng.shuffle(all_letters)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Create random layout with same structure as QWERTY (10-9-7)
|
|
|
|
|
self.keyboard_layout = [
|
2025-11-30 13:42:16 +01:00
|
|
|
all_letters[0:10], # Top row: 10 keys
|
|
|
|
|
all_letters[10:19], # Middle row: 9 keys
|
|
|
|
|
all_letters[19:26], # Bottom row: 7 keys
|
2025-07-18 00:08:21 +02:00
|
|
|
]
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Update available letters
|
|
|
|
|
self.available_letters = set(all_letters)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Calculate adjacencies based on new layout
|
|
|
|
|
self.calculate_adjacencies()
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def calculate_adjacencies(self) -> None:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Calculate adjacencies based on current keyboard layout."""
|
2025-07-18 00:08:21 +02:00
|
|
|
self.key_adjacency = {}
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
for row_idx, row in enumerate(self.keyboard_layout):
|
|
|
|
|
for col_idx, letter in enumerate(row):
|
|
|
|
|
adjacents = []
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Check all 8 directions (including diagonals)
|
|
|
|
|
directions = [
|
2025-11-30 13:42:16 +01:00
|
|
|
(-1, -1),
|
|
|
|
|
(-1, 0),
|
|
|
|
|
(-1, 1), # Above
|
|
|
|
|
(0, -1),
|
|
|
|
|
(0, 1), # Same row
|
|
|
|
|
(1, -1),
|
|
|
|
|
(1, 0),
|
|
|
|
|
(1, 1), # Below
|
2025-07-18 00:08:21 +02:00
|
|
|
]
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
for dr, dc in directions:
|
|
|
|
|
new_row = row_idx + dr
|
|
|
|
|
new_col = col_idx + dc
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Check bounds
|
2025-11-30 20:47:38 +01:00
|
|
|
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])
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
self.key_adjacency[letter] = adjacents
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def calculate_key_positions(self) -> dict[str, pygame.Rect]:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Calculate the position of each key on screen."""
|
2025-11-30 15:49:40 +01:00
|
|
|
positions: dict[str, pygame.Rect] = {}
|
2025-07-18 00:08:21 +02:00
|
|
|
key_width = 60
|
|
|
|
|
key_height = 60
|
|
|
|
|
key_spacing = 8
|
|
|
|
|
start_x = 50
|
|
|
|
|
start_y = 320
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
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)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
return positions
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def get_key_at_position(self, pos: tuple[int, int]) -> str | None:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Get the key at the given mouse position."""
|
2025-07-18 00:08:21 +02:00
|
|
|
for key, rect in self.key_positions.items():
|
|
|
|
|
if rect.collidepoint(pos):
|
|
|
|
|
return key
|
|
|
|
|
return None
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def is_valid_move(self, letter: str) -> bool:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Check if the letter is a valid move."""
|
2025-07-18 00:08:21 +02:00
|
|
|
if not self.selected_letters:
|
|
|
|
|
return True # First move can be any letter
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
last_letter = self.selected_letters[-1]
|
|
|
|
|
return letter in self.key_adjacency[last_letter]
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def is_valid_word(self, word: str) -> bool:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Check if the word is in the dictionary."""
|
2025-07-18 00:08:21 +02:00
|
|
|
return word.lower() in self.dictionary
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def calculate_score(self, word_length: int) -> int:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Calculate score exponentially based on word length."""
|
2025-11-30 15:01:14 +01:00
|
|
|
if word_length < MIN_WORD_LENGTH:
|
2025-07-18 00:08:21 +02:00
|
|
|
return 0
|
|
|
|
|
return 2 ** (word_length - 2)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def handle_letter_click(self, letter: str) -> None:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Handle clicking on a letter."""
|
2025-07-18 00:08:21 +02:00
|
|
|
if letter in self.available_letters and self.is_valid_move(letter):
|
|
|
|
|
self.selected_letters.append(letter)
|
|
|
|
|
self.current_word += letter
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:17:41 +02:00
|
|
|
# Update available letters to include adjacent letters AND the same letter
|
2025-11-30 14:25:35 +01:00
|
|
|
adjacent_letters = (
|
|
|
|
|
set(self.key_adjacency[letter])
|
|
|
|
|
if letter in self.key_adjacency
|
|
|
|
|
else set()
|
|
|
|
|
)
|
2025-07-18 00:17:41 +02:00
|
|
|
adjacent_letters.add(letter) # Allow selecting the same letter again
|
|
|
|
|
self.available_letters = adjacent_letters
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Switch player
|
|
|
|
|
self.current_player = 1 - self.current_player
|
2025-11-30 14:25:35 +01:00
|
|
|
self.message = (
|
|
|
|
|
f"Player {self.current_player + 1}: Choose an adjacent letter!"
|
|
|
|
|
)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# If no valid moves available, force word submission
|
|
|
|
|
if not self.available_letters:
|
|
|
|
|
self.submit_word()
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def submit_word(self) -> None:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Submit the current word and check if it's valid."""
|
2025-11-30 15:01:14 +01:00
|
|
|
if len(self.current_word) >= MIN_WORD_LENGTH and self.is_valid_word(
|
|
|
|
|
self.current_word
|
|
|
|
|
):
|
2025-07-18 00:08:21 +02:00
|
|
|
points = self.calculate_score(len(self.current_word))
|
|
|
|
|
self.score += points
|
2025-11-30 14:25:35 +01:00
|
|
|
self.message = (
|
|
|
|
|
f"'{self.current_word}' is valid! +{points} points "
|
|
|
|
|
f"(Total: {self.score}) - New keyboard!"
|
|
|
|
|
)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Randomize keyboard layout after scoring
|
|
|
|
|
self.generate_random_keyboard()
|
|
|
|
|
self.key_positions = self.calculate_key_positions()
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:01:14 +01:00
|
|
|
elif len(self.current_word) < MIN_WORD_LENGTH:
|
|
|
|
|
self.message = (
|
|
|
|
|
f"'{self.current_word}' is too short! "
|
|
|
|
|
f"(minimum {MIN_WORD_LENGTH} letters)"
|
|
|
|
|
)
|
2025-07-18 00:08:21 +02:00
|
|
|
else:
|
|
|
|
|
self.message = f"'{self.current_word}' is not a valid word!"
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Reset for next word
|
|
|
|
|
self.current_word = ""
|
|
|
|
|
self.selected_letters = []
|
|
|
|
|
self.current_player = 0
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def reset_game(self) -> None:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Reset the game to initial state."""
|
2025-07-18 00:08:21 +02:00
|
|
|
self.current_player = 0
|
|
|
|
|
self.current_word = ""
|
|
|
|
|
self.selected_letters = []
|
|
|
|
|
self.score = 0
|
|
|
|
|
self.game_over = False
|
|
|
|
|
self.message = "Player 1: Choose any letter to start!"
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Generate new random keyboard layout
|
|
|
|
|
self.generate_random_keyboard()
|
|
|
|
|
self.key_positions = self.calculate_key_positions()
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def draw_keyboard(self) -> None:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Draw the virtual keyboard."""
|
2025-07-18 00:08:21 +02:00
|
|
|
mouse_pos = pygame.mouse.get_pos()
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
for letter, rect in self.key_positions.items():
|
|
|
|
|
# Determine key color
|
|
|
|
|
if letter in self.selected_letters:
|
|
|
|
|
color = KEY_SELECTED_COLOR
|
|
|
|
|
elif letter in self.available_letters:
|
|
|
|
|
color = KEY_AVAILABLE_COLOR
|
|
|
|
|
elif rect.collidepoint(mouse_pos) and letter in self.available_letters:
|
|
|
|
|
color = KEY_HOVER_COLOR
|
|
|
|
|
else:
|
|
|
|
|
color = KEY_COLOR
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Draw key
|
|
|
|
|
pygame.draw.rect(self.screen, color, rect)
|
|
|
|
|
pygame.draw.rect(self.screen, TEXT_COLOR, rect, 2)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# 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)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def draw_ui(self) -> tuple[pygame.Rect, pygame.Rect]:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Draw the user interface."""
|
2025-07-18 00:08:21 +02:00
|
|
|
# Title
|
|
|
|
|
title = self.large_font.render("Keyboard Coop Game", True, TEXT_COLOR)
|
|
|
|
|
self.screen.blit(title, (30, 20))
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Current word
|
2025-11-30 14:25:35 +01:00
|
|
|
word_text = self.font.render(
|
|
|
|
|
f"Current Word: {self.current_word.upper()}", True, TEXT_COLOR
|
|
|
|
|
)
|
2025-07-18 00:08:21 +02:00
|
|
|
self.screen.blit(word_text, (30, 50))
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Score
|
|
|
|
|
score_text = self.font.render(f"Score: {self.score}", True, TEXT_COLOR)
|
|
|
|
|
self.screen.blit(score_text, (30, 75))
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Current player
|
|
|
|
|
player_color = PLAYER_COLORS[self.current_player]
|
2025-11-30 14:25:35 +01:00
|
|
|
player_text = self.font.render(
|
|
|
|
|
f"Current Player: {self.current_player + 1}", True, player_color
|
|
|
|
|
)
|
2025-07-18 00:08:21 +02:00
|
|
|
self.screen.blit(player_text, (30, 100))
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Message
|
|
|
|
|
message_text = self.font.render(self.message, True, TEXT_COLOR)
|
|
|
|
|
self.screen.blit(message_text, (30, 125))
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Instructions - Move to right side and make more compact
|
|
|
|
|
instructions = [
|
|
|
|
|
"Instructions:",
|
|
|
|
|
"• Take turns selecting adjacent letters",
|
2025-07-18 00:17:41 +02:00
|
|
|
"• Click letters or use keyboard to select",
|
|
|
|
|
"• Same letter can be selected consecutively",
|
2025-07-18 00:08:21 +02:00
|
|
|
"• Press ENTER to submit word (min 3 letters)",
|
|
|
|
|
"• Press R to reset game",
|
|
|
|
|
"• Score increases exponentially",
|
2025-11-30 13:42:16 +01:00
|
|
|
"• Keyboard randomizes after each valid word!",
|
2025-07-18 00:08:21 +02:00
|
|
|
]
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
start_x = 750
|
|
|
|
|
for i, instruction in enumerate(instructions):
|
|
|
|
|
inst_text = self.small_font.render(instruction, True, TEXT_COLOR)
|
|
|
|
|
self.screen.blit(inst_text, (start_x, 20 + i * 22))
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Enter button - smaller and repositioned
|
2025-07-18 00:17:41 +02:00
|
|
|
enter_rect = pygame.Rect(750, 190, 120, 40)
|
2025-07-18 00:08:21 +02:00
|
|
|
pygame.draw.rect(self.screen, KEY_COLOR, enter_rect)
|
|
|
|
|
pygame.draw.rect(self.screen, TEXT_COLOR, enter_rect, 2)
|
|
|
|
|
enter_text = self.small_font.render("ENTER", True, TEXT_COLOR)
|
|
|
|
|
enter_text_rect = enter_text.get_rect(center=enter_rect.center)
|
|
|
|
|
self.screen.blit(enter_text, enter_text_rect)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Reset button - smaller and repositioned
|
2025-07-18 00:17:41 +02:00
|
|
|
reset_rect = pygame.Rect(880, 190, 120, 40)
|
2025-07-18 00:08:21 +02:00
|
|
|
pygame.draw.rect(self.screen, KEY_COLOR, reset_rect)
|
|
|
|
|
pygame.draw.rect(self.screen, TEXT_COLOR, reset_rect, 2)
|
|
|
|
|
reset_text = self.small_font.render("RESET", True, TEXT_COLOR)
|
|
|
|
|
reset_text_rect = reset_text.get_rect(center=reset_rect.center)
|
|
|
|
|
self.screen.blit(reset_text, reset_text_rect)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
return enter_rect, reset_rect
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def handle_click(self, pos: tuple[int, int]) -> None:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Handle mouse clicks."""
|
2025-07-18 00:08:21 +02:00
|
|
|
# Check if clicked on a key
|
|
|
|
|
key = self.get_key_at_position(pos)
|
|
|
|
|
if key:
|
|
|
|
|
self.handle_letter_click(key)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Check UI buttons
|
2025-07-18 00:17:41 +02:00
|
|
|
enter_rect = pygame.Rect(750, 190, 120, 40)
|
|
|
|
|
reset_rect = pygame.Rect(880, 190, 120, 40)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
if enter_rect.collidepoint(pos):
|
|
|
|
|
self.submit_word()
|
|
|
|
|
elif reset_rect.collidepoint(pos):
|
|
|
|
|
self.reset_game()
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-11-30 15:49:40 +01:00
|
|
|
def run(self) -> None:
|
2025-11-30 13:59:21 +01:00
|
|
|
"""Main game loop."""
|
2025-07-18 00:08:21 +02:00
|
|
|
running = True
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
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()
|
2025-07-18 00:17:41 +02:00
|
|
|
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())
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Clear screen
|
|
|
|
|
self.screen.fill(BACKGROUND_COLOR)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Draw everything
|
|
|
|
|
self.draw_keyboard()
|
|
|
|
|
self.draw_ui()
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
# Update display
|
|
|
|
|
pygame.display.flip()
|
|
|
|
|
self.clock.tick(60)
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
pygame.quit()
|
|
|
|
|
sys.exit()
|
|
|
|
|
|
2025-11-30 13:42:16 +01:00
|
|
|
|
2025-07-18 00:08:21 +02:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
game = KeyboardCoopGame()
|
|
|
|
|
game.run()
|