Spaces:
Running
Running
File size: 6,645 Bytes
d747bc4 49f93dd 2de3d96 49f93dd 2de3d96 49f93dd d747bc4 49f93dd d747bc4 49f93dd 2de3d96 49f93dd d747bc4 85779ec 581c2fc 85779ec 581c2fc d747bc4 581c2fc d747bc4 226477a d747bc4 10dda8b 99bf0ab d747bc4 feb1c4e 2de3d96 feb1c4e d747bc4 8c718b2 fd702c9 8c718b2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
from __future__ import annotations
from typing import Dict, Tuple
from .models import Coord, Puzzle, GameState, Word
def _chebyshev_distance(a: Coord, b: Coord) -> int:
"""8-neighborhood distance (max norm)."""
return max(abs(a.x - b.x), abs(a.y - b.y))
def build_letter_map(puzzle: Puzzle, skip_validation: bool = True) -> Dict[Coord, str]:
"""Build a coordinate->letter map for the given puzzle.
Spacer support (0–2):
- spacer = 0: words may touch (no separation enforced).
- spacer = 1: words must be separated by at least 1 blank tile
(no two letters from different words at Chebyshev distance <= 1).
- spacer = 2: at least 2 blank tiles between words
(no two letters from different words at Chebyshev distance <= 2).
Overlaps are not handled here (negative spacer not supported in this function).
This function raises ValueError if the configured spacing is violated.
Args:
puzzle: The puzzle to build the letter map for
skip_validation: If True, skip spacer validation (default: True).
Validation is expensive and only needed during puzzle
generation, not during gameplay.
"""
mapping: Dict[Coord, str] = {}
spacer = getattr(puzzle, "spacer", 1)
# Build mapping normally (no overlap merging beyond first-come-wins semantics)
for w in puzzle.words:
for idx, c in enumerate(w.cells):
ch = w.text[idx]
if c not in mapping:
mapping[c] = ch
else:
# If an explicit overlap occurs, we don't support it here.
# Keep the first-seen letter and continue.
pass
# Enforce spacer in the range 0–2 (only when validation is requested)
if not skip_validation and spacer in (1, 2):
# Prepare sets of cells per word
word_cells = [set(w.cells) for w in puzzle.words]
for i in range(len(word_cells)):
for j in range(i + 1, len(word_cells)):
cells_i = word_cells[i]
cells_j = word_cells[j]
# If any pair is too close, it's a violation
for c1 in cells_i:
# Early exit by scanning a small neighborhood around c1
# since Chebyshev distance <= spacer
for c2 in cells_j:
if _chebyshev_distance(c1, c2) <= spacer:
raise ValueError(
f"Words too close (spacer={spacer}): {c1} and {c2}"
)
# spacer == 0 -> no checks; other values are ignored here intentionally
return mapping
def reveal_cell(state: GameState, letter_map: Dict[Coord, str], coord: Coord) -> None:
if coord in state.revealed:
state.last_action = "Already revealed."
return
state.revealed.add(coord)
# Determine if this reveal uncovered a letter or an empty cell
ch = letter_map.get(coord, "·")
# Only allow guessing if a letter was revealed; preserve existing True (e.g., after a correct guess)
state.can_guess = state.can_guess or (ch != "·")
if ch == "·":
state.last_action = f"Revealed empty at ({coord.x+1},{coord.y+1})."
else:
state.last_action = f"Revealed '{ch}' at ({coord.x+1},{coord.y+1})."
def guess_word(state: GameState, guess_text: str) -> Tuple[bool, int]:
if not state.can_guess:
state.last_action = "You must reveal a cell before guessing."
return False, 0
guess = (guess_text or "").strip().upper()
if not (len(guess) in (4, 5, 6) and guess.isalpha()):
state.last_action = "Guess must be A–Z and length 4, 5, or 6."
state.can_guess = False
return False, 0
if guess in state.guessed:
state.last_action = f"Already guessed {guess}."
state.can_guess = False
return False, 0
# Find matching unguessed word
target: Word | None = None
for w in state.puzzle.words:
if w.text == guess and w.text not in state.guessed:
target = w
break
if target is None:
state.last_action = f"Try Again! '{guess}' is not in the puzzle."
state.can_guess = False
return False, 0
# Scoring: base = length, bonus = unrevealed cells in that word
unrevealed = sum(1 for c in target.cells if c not in state.revealed)
points = target.length + unrevealed
state.score += points
state.points_by_word[target.text] = points
# Reveal all cells of the word
for c in target.cells:
state.revealed.add(c)
state.guessed.add(target.text)
state.last_action = f"Correct! +{points} points for {target.text}."
if state.game_mode == "classic":
state.can_guess = True # <-- Allow another guess after a correct guess
else:
state.can_guess = False
return True, points
def is_game_over(state: GameState) -> bool:
# Game ends if all words are guessed
if len(state.guessed) == 6:
return True
# Game ends if all word cells are revealed
# Use pre-computed _all_word_cells set from puzzle for performance
if state.puzzle._all_word_cells.issubset(state.revealed):
return True
return False
def compute_tier(score: int) -> str:
if score >= 42:
return "Fantastic"
if 38 <= score <= 41:
return "Great"
if 34 <= score <= 37:
return "Good"
return "Keep practicing"
def hidden_word_display(letters_display: int, character: str = "?") -> str:
"""Return a string of characters of length letters_display."""
return character * letters_display
def auto_mark_completed_words(state: GameState) -> bool:
"""Automatically mark words as found when all their letters are revealed.
Returns True if any word state changed (e.g., guessed/score/points).
Scoring in this case is base length only (no unrevealed bonus).
"""
changed = False
for w in state.puzzle.words:
if w.text in state.guessed:
continue
if all(c in state.revealed for c in w.cells):
# Award base points if not already assigned
if w.text not in state.points_by_word:
base_points = w.length
state.points_by_word[w.text] = base_points
state.score += base_points
state.guessed.add(w.text)
changed = True
if changed:
# Do not alter can_guess; just note the auto-complete
state.last_action = (state.last_action or "") + "\nAuto-complete: revealed word(s) marked as found."
return changed |