Spaces:
Running
Running
File size: 5,142 Bytes
d747bc4 49f93dd d747bc4 49f93dd d747bc4 49f93dd d747bc4 49f93dd d747bc4 85779ec 581c2fc 85779ec 581c2fc d747bc4 581c2fc d747bc4 226477a d747bc4 10dda8b 99bf0ab d747bc4 feb1c4e d747bc4 |
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 |
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) -> 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.
"""
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
if 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
all_word_cells = set()
for w in state.puzzle.words:
all_word_cells.update(w.cells)
if 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" |