testsAndMisc/python_pkg/keyboard_coop/main.py

507 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.
"""
2025-07-18 00:08:21 +02:00
import json
import logging
2025-07-18 00:08:21 +02:00
import os
import secrets
import sys
import pygame
2025-07-18 00:08:21 +02:00
_logger = logging.getLogger(__name__)
# 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)]
MIN_WORD_LENGTH = 3
2025-07-18 00:08:21 +02:00
# 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"],
2025-07-18 00:08:21 +02:00
]
# 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"],
2025-07-18 00:08:21 +02:00
}
2025-07-18 00:08:21 +02:00
class KeyboardCoopGame:
"""Main game class for the keyboard cooperative word game."""
def __init__(self) -> None:
"""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-07-18 00:08:21 +02:00
# Load dictionary
self.dictionary = self.load_dictionary()
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-07-18 00:08:21 +02:00
# Generate random keyboard layout and adjacency
self.generate_random_keyboard()
2025-07-18 00:08:21 +02:00
# Key positions
self.key_positions = self.calculate_key_positions()
def load_dictionary(self) -> set[str]:
"""Load dictionary from words_dictionary.json file."""
2025-07-18 00:08:21 +02:00
try:
dictionary_path = os.path.join(
os.path.dirname(__file__), "words_dictionary.json"
)
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:
_logger.warning(
"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 {
"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:
_logger.warning(
"Error reading words_dictionary.json, using fallback dictionary"
)
2025-07-18 00:08:21 +02:00
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",
2025-07-18 00:08:21 +02:00
}
def generate_random_keyboard(self) -> None:
"""Generate a random keyboard layout and calculate adjacencies."""
2025-07-18 00:08:21 +02:00
# All 26 letters
all_letters = list("abcdefghijklmnopqrstuvwxyz")
_rng.shuffle(all_letters)
2025-07-18 00:08:21 +02:00
# 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
2025-07-18 00:08:21 +02:00
]
2025-07-18 00:08:21 +02:00
# Update available letters
self.available_letters = set(all_letters)
2025-07-18 00:08:21 +02:00
# Calculate adjacencies based on new layout
self.calculate_adjacencies()
def calculate_adjacencies(self) -> None:
"""Calculate adjacencies based on current keyboard layout."""
2025-07-18 00:08:21 +02:00
self.key_adjacency = {}
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-07-18 00:08:21 +02:00
# 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
2025-07-18 00:08:21 +02: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-07-18 00:08:21 +02:00
# 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])
2025-07-18 00:08:21 +02:00
self.key_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] = {}
2025-07-18 00:08:21 +02:00
key_width = 60
key_height = 60
key_spacing = 8
start_x = 50
start_y = 320
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-07-18 00:08:21 +02:00
return positions
def get_key_at_position(self, pos: tuple[int, int]) -> str | None:
"""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
def is_valid_move(self, letter: str) -> bool:
"""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-07-18 00:08:21 +02:00
last_letter = self.selected_letters[-1]
return letter in self.key_adjacency[last_letter]
def is_valid_word(self, word: str) -> bool:
"""Check if the word is in the dictionary."""
2025-07-18 00:08:21 +02:00
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:
2025-07-18 00:08:21 +02:00
return 0
return 2 ** (word_length - 2)
def handle_letter_click(self, letter: str) -> None:
"""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
# Update available letters to include adjacent letters AND the same letter
adjacent_letters = (
set(self.key_adjacency[letter])
if letter in self.key_adjacency
else set()
)
adjacent_letters.add(letter) # Allow selecting the same letter again
self.available_letters = adjacent_letters
2025-07-18 00:08:21 +02:00
# Switch player
self.current_player = 1 - self.current_player
self.message = (
f"Player {self.current_player + 1}: Choose an adjacent letter!"
)
2025-07-18 00:08:21 +02:00
# If no valid moves available, force word submission
if not self.available_letters:
self.submit_word()
def submit_word(self) -> None:
"""Submit the current word and check if it's valid."""
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
self.message = (
f"'{self.current_word}' is valid! +{points} points "
f"(Total: {self.score}) - New keyboard!"
)
2025-07-18 00:08:21 +02:00
# Randomize keyboard layout after scoring
self.generate_random_keyboard()
self.key_positions = self.calculate_key_positions()
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-07-18 00:08:21 +02:00
# Reset for next word
self.current_word = ""
self.selected_letters = []
self.current_player = 0
def reset_game(self) -> None:
"""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-07-18 00:08:21 +02:00
# Generate new random keyboard layout
self.generate_random_keyboard()
self.key_positions = self.calculate_key_positions()
def draw_keyboard(self) -> None:
"""Draw the virtual keyboard."""
2025-07-18 00:08:21 +02:00
mouse_pos = pygame.mouse.get_pos()
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-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-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)
def draw_ui(self) -> tuple[pygame.Rect, pygame.Rect]:
"""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-07-18 00:08:21 +02:00
# Current word
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-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-07-18 00:08:21 +02:00
# Current player
player_color = PLAYER_COLORS[self.current_player]
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-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-07-18 00:08:21 +02:00
# Instructions - Move to right side and make more compact
instructions = [
"Instructions:",
"• Take turns selecting adjacent letters",
"• 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",
"• Keyboard randomizes after each valid word!",
2025-07-18 00:08:21 +02: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-07-18 00:08:21 +02:00
# Enter button - smaller and repositioned
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-07-18 00:08:21 +02:00
# Reset button - smaller and repositioned
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-07-18 00:08:21 +02:00
return enter_rect, reset_rect
def handle_click(self, pos: tuple[int, int]) -> None:
"""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-07-18 00:08:21 +02:00
# Check UI buttons
enter_rect = pygame.Rect(750, 190, 120, 40)
reset_rect = pygame.Rect(880, 190, 120, 40)
2025-07-18 00:08:21 +02:00
if enter_rect.collidepoint(pos):
self.submit_word()
elif reset_rect.collidepoint(pos):
self.reset_game()
def run(self) -> None:
"""Main game loop."""
2025-07-18 00:08:21 +02:00
running = True
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()
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-07-18 00:08:21 +02:00
# Clear screen
self.screen.fill(BACKGROUND_COLOR)
2025-07-18 00:08:21 +02:00
# Draw everything
self.draw_keyboard()
self.draw_ui()
2025-07-18 00:08:21 +02:00
# Update display
pygame.display.flip()
self.clock.tick(60)
2025-07-18 00:08:21 +02:00
pygame.quit()
sys.exit()
2025-07-18 00:08:21 +02:00
if __name__ == "__main__":
game = KeyboardCoopGame()
game.run()