chuk-puzzles-gym 0.9__py3-none-any.whl → 0.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chuk_puzzles_gym/eval.py +21 -0
- chuk_puzzles_gym/games/__init__.py +22 -0
- chuk_puzzles_gym/games/binary/game.py +2 -0
- chuk_puzzles_gym/games/bridges/game.py +2 -0
- chuk_puzzles_gym/games/cryptarithmetic/__init__.py +7 -0
- chuk_puzzles_gym/games/cryptarithmetic/commands.py +75 -0
- chuk_puzzles_gym/games/cryptarithmetic/config.py +23 -0
- chuk_puzzles_gym/games/cryptarithmetic/game.py +388 -0
- chuk_puzzles_gym/games/einstein/game.py +2 -0
- chuk_puzzles_gym/games/fillomino/game.py +2 -0
- chuk_puzzles_gym/games/futoshiki/game.py +2 -0
- chuk_puzzles_gym/games/graph_coloring/__init__.py +7 -0
- chuk_puzzles_gym/games/graph_coloring/commands.py +96 -0
- chuk_puzzles_gym/games/graph_coloring/config.py +24 -0
- chuk_puzzles_gym/games/graph_coloring/game.py +316 -0
- chuk_puzzles_gym/games/hidato/game.py +2 -0
- chuk_puzzles_gym/games/hitori/game.py +2 -0
- chuk_puzzles_gym/games/kakuro/game.py +2 -0
- chuk_puzzles_gym/games/kenken/game.py +2 -0
- chuk_puzzles_gym/games/killer_sudoku/game.py +2 -0
- chuk_puzzles_gym/games/knapsack/game.py +2 -0
- chuk_puzzles_gym/games/lights_out/game.py +2 -0
- chuk_puzzles_gym/games/logic_grid/game.py +2 -0
- chuk_puzzles_gym/games/mastermind/game.py +2 -0
- chuk_puzzles_gym/games/minesweeper/game.py +2 -0
- chuk_puzzles_gym/games/nonogram/game.py +2 -0
- chuk_puzzles_gym/games/nqueens/__init__.py +6 -0
- chuk_puzzles_gym/games/nqueens/config.py +23 -0
- chuk_puzzles_gym/games/nqueens/game.py +321 -0
- chuk_puzzles_gym/games/numberlink/__init__.py +6 -0
- chuk_puzzles_gym/games/numberlink/config.py +23 -0
- chuk_puzzles_gym/games/numberlink/game.py +344 -0
- chuk_puzzles_gym/games/nurikabe/game.py +2 -0
- chuk_puzzles_gym/games/rush_hour/__init__.py +8 -0
- chuk_puzzles_gym/games/rush_hour/commands.py +57 -0
- chuk_puzzles_gym/games/rush_hour/config.py +25 -0
- chuk_puzzles_gym/games/rush_hour/game.py +479 -0
- chuk_puzzles_gym/games/rush_hour/models.py +15 -0
- chuk_puzzles_gym/games/scheduler/game.py +2 -0
- chuk_puzzles_gym/games/shikaku/game.py +2 -0
- chuk_puzzles_gym/games/skyscrapers/__init__.py +6 -0
- chuk_puzzles_gym/games/skyscrapers/config.py +22 -0
- chuk_puzzles_gym/games/skyscrapers/game.py +282 -0
- chuk_puzzles_gym/games/slitherlink/game.py +2 -0
- chuk_puzzles_gym/games/sokoban/game.py +2 -0
- chuk_puzzles_gym/games/star_battle/game.py +2 -0
- chuk_puzzles_gym/games/sudoku/game.py +2 -0
- chuk_puzzles_gym/games/tents/game.py +2 -0
- chuk_puzzles_gym/server.py +18 -70
- chuk_puzzles_gym/trace/generator.py +87 -0
- {chuk_puzzles_gym-0.9.dist-info → chuk_puzzles_gym-0.10.1.dist-info}/METADATA +60 -19
- {chuk_puzzles_gym-0.9.dist-info → chuk_puzzles_gym-0.10.1.dist-info}/RECORD +55 -33
- {chuk_puzzles_gym-0.9.dist-info → chuk_puzzles_gym-0.10.1.dist-info}/WHEEL +1 -1
- {chuk_puzzles_gym-0.9.dist-info → chuk_puzzles_gym-0.10.1.dist-info}/entry_points.txt +0 -0
- {chuk_puzzles_gym-0.9.dist-info → chuk_puzzles_gym-0.10.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Skyscrapers puzzle game implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ...models import DifficultyLevel, DifficultyProfile, MoveResult
|
|
6
|
+
from .._base import PuzzleGame
|
|
7
|
+
from .config import SkyscrapersConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SkyscrapersGame(PuzzleGame):
|
|
11
|
+
"""Skyscrapers puzzle - fill a Latin square with visibility clues.
|
|
12
|
+
|
|
13
|
+
Rules:
|
|
14
|
+
- Fill an NxN grid with numbers 1 to N
|
|
15
|
+
- Each row and column must contain each number exactly once (Latin square)
|
|
16
|
+
- Numbers represent building heights
|
|
17
|
+
- Clues around the border indicate how many buildings are visible from that direction
|
|
18
|
+
- A taller building hides all shorter buildings behind it
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
|
|
22
|
+
super().__init__(difficulty, seed, **kwargs)
|
|
23
|
+
self.config = SkyscrapersConfig.from_difficulty(self.difficulty)
|
|
24
|
+
self.size = self.config.size
|
|
25
|
+
self.grid: list[list[int]] = []
|
|
26
|
+
self.solution: list[list[int]] = []
|
|
27
|
+
self.initial_grid: list[list[int]] = []
|
|
28
|
+
self.clues: dict[str, list[int]] = {"top": [], "bottom": [], "left": [], "right": []}
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
return "Skyscrapers"
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def description(self) -> str:
|
|
36
|
+
return "Fill the grid with building heights using visibility clues"
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def constraint_types(self) -> list[str]:
|
|
40
|
+
return ["all_different", "visibility", "ordering", "boundary_clues"]
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def business_analogies(self) -> list[str]:
|
|
44
|
+
return ["urban_planning", "line_of_sight_analysis", "signal_visibility"]
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def complexity_profile(self) -> dict[str, str]:
|
|
48
|
+
return {
|
|
49
|
+
"reasoning_type": "deductive",
|
|
50
|
+
"search_space": "medium",
|
|
51
|
+
"constraint_density": "dense",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def complexity_metrics(self) -> dict[str, int | float]:
|
|
56
|
+
empty = sum(1 for row in self.grid for cell in row if cell == 0)
|
|
57
|
+
return {
|
|
58
|
+
"variable_count": self.size * self.size,
|
|
59
|
+
"constraint_count": 2 * self.size + 4 * self.size,
|
|
60
|
+
"domain_size": self.size,
|
|
61
|
+
"branching_factor": self.size / 2.0,
|
|
62
|
+
"empty_cells": empty,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def difficulty_profile(self) -> DifficultyProfile:
|
|
67
|
+
profiles = {
|
|
68
|
+
DifficultyLevel.EASY: DifficultyProfile(
|
|
69
|
+
logic_depth=2, branching_factor=2.0, state_observability=1.0, constraint_density=0.7
|
|
70
|
+
),
|
|
71
|
+
DifficultyLevel.MEDIUM: DifficultyProfile(
|
|
72
|
+
logic_depth=4, branching_factor=3.0, state_observability=1.0, constraint_density=0.6
|
|
73
|
+
),
|
|
74
|
+
DifficultyLevel.HARD: DifficultyProfile(
|
|
75
|
+
logic_depth=6, branching_factor=4.0, state_observability=1.0, constraint_density=0.5
|
|
76
|
+
),
|
|
77
|
+
}
|
|
78
|
+
return profiles[self.difficulty]
|
|
79
|
+
|
|
80
|
+
def _compute_visibility(self, line: list[int]) -> int:
|
|
81
|
+
"""Count how many buildings are visible from the start of a line."""
|
|
82
|
+
count = 0
|
|
83
|
+
max_height = 0
|
|
84
|
+
for h in line:
|
|
85
|
+
if h > max_height:
|
|
86
|
+
count += 1
|
|
87
|
+
max_height = h
|
|
88
|
+
return count
|
|
89
|
+
|
|
90
|
+
def _generate_latin_square(self) -> list[list[int]]:
|
|
91
|
+
"""Generate a random NxN Latin square."""
|
|
92
|
+
n = self.size
|
|
93
|
+
# Start with a shifted-row construction
|
|
94
|
+
base = list(range(1, n + 1))
|
|
95
|
+
grid = []
|
|
96
|
+
for r in range(n):
|
|
97
|
+
row = [(base[(r + c) % n]) for c in range(n)]
|
|
98
|
+
grid.append(row)
|
|
99
|
+
|
|
100
|
+
# Shuffle rows
|
|
101
|
+
rows = list(range(n))
|
|
102
|
+
self._rng.shuffle(rows)
|
|
103
|
+
grid = [grid[r] for r in rows]
|
|
104
|
+
|
|
105
|
+
# Shuffle columns
|
|
106
|
+
cols = list(range(n))
|
|
107
|
+
self._rng.shuffle(cols)
|
|
108
|
+
grid = [[row[c] for c in cols] for row in grid]
|
|
109
|
+
|
|
110
|
+
# Shuffle values (relabel)
|
|
111
|
+
perm = list(range(1, n + 1))
|
|
112
|
+
self._rng.shuffle(perm)
|
|
113
|
+
mapping = {i + 1: perm[i] for i in range(n)}
|
|
114
|
+
grid = [[mapping[cell] for cell in row] for row in grid]
|
|
115
|
+
|
|
116
|
+
return grid
|
|
117
|
+
|
|
118
|
+
def _compute_all_clues(self, grid: list[list[int]]) -> dict[str, list[int]]:
|
|
119
|
+
"""Compute visibility clues from all 4 directions."""
|
|
120
|
+
n = self.size
|
|
121
|
+
clues: dict[str, list[int]] = {"top": [], "bottom": [], "left": [], "right": []}
|
|
122
|
+
|
|
123
|
+
for c in range(n):
|
|
124
|
+
col = [grid[r][c] for r in range(n)]
|
|
125
|
+
clues["top"].append(self._compute_visibility(col))
|
|
126
|
+
clues["bottom"].append(self._compute_visibility(col[::-1]))
|
|
127
|
+
|
|
128
|
+
for r in range(n):
|
|
129
|
+
clues["left"].append(self._compute_visibility(grid[r]))
|
|
130
|
+
clues["right"].append(self._compute_visibility(grid[r][::-1]))
|
|
131
|
+
|
|
132
|
+
return clues
|
|
133
|
+
|
|
134
|
+
async def generate_puzzle(self) -> None:
|
|
135
|
+
"""Generate a Skyscrapers puzzle."""
|
|
136
|
+
self.solution = self._generate_latin_square()
|
|
137
|
+
self.clues = self._compute_all_clues(self.solution)
|
|
138
|
+
|
|
139
|
+
# Copy solution to grid, then remove cells based on difficulty
|
|
140
|
+
self.grid = [row[:] for row in self.solution]
|
|
141
|
+
|
|
142
|
+
# Determine cells to remove
|
|
143
|
+
n = self.size
|
|
144
|
+
total_cells = n * n
|
|
145
|
+
remove_map = {
|
|
146
|
+
DifficultyLevel.EASY: int(total_cells * 0.45),
|
|
147
|
+
DifficultyLevel.MEDIUM: int(total_cells * 0.60),
|
|
148
|
+
DifficultyLevel.HARD: int(total_cells * 0.75),
|
|
149
|
+
}
|
|
150
|
+
cells_to_remove = remove_map[self.difficulty]
|
|
151
|
+
|
|
152
|
+
# Randomly remove cells
|
|
153
|
+
all_cells = [(r, c) for r in range(n) for c in range(n)]
|
|
154
|
+
self._rng.shuffle(all_cells)
|
|
155
|
+
for r, c in all_cells[:cells_to_remove]:
|
|
156
|
+
self.grid[r][c] = 0
|
|
157
|
+
|
|
158
|
+
self.initial_grid = [row[:] for row in self.grid]
|
|
159
|
+
self.game_started = True
|
|
160
|
+
|
|
161
|
+
async def validate_move(self, row: int, col: int, num: int) -> MoveResult:
|
|
162
|
+
"""Validate placing a height value.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
row: 1-indexed row
|
|
166
|
+
col: 1-indexed column
|
|
167
|
+
num: Height value (1-N) or 0 to clear
|
|
168
|
+
"""
|
|
169
|
+
n = self.size
|
|
170
|
+
r, c = row - 1, col - 1
|
|
171
|
+
|
|
172
|
+
if not (0 <= r < n and 0 <= c < n):
|
|
173
|
+
self.record_move((row, col), False)
|
|
174
|
+
return MoveResult(success=False, message=f"Position ({row}, {col}) is out of bounds.")
|
|
175
|
+
|
|
176
|
+
if self.initial_grid[r][c] != 0:
|
|
177
|
+
self.record_move((row, col), False)
|
|
178
|
+
return MoveResult(success=False, message="Cannot modify an initial cell.")
|
|
179
|
+
|
|
180
|
+
if num == 0:
|
|
181
|
+
self.grid[r][c] = 0
|
|
182
|
+
self.record_move((row, col), True)
|
|
183
|
+
return MoveResult(success=True, message=f"Cleared cell ({row}, {col}).", state_changed=True)
|
|
184
|
+
|
|
185
|
+
if not (1 <= num <= n):
|
|
186
|
+
self.record_move((row, col), False)
|
|
187
|
+
return MoveResult(success=False, message=f"Value must be between 1 and {n}.")
|
|
188
|
+
|
|
189
|
+
# Check row uniqueness
|
|
190
|
+
for cc in range(n):
|
|
191
|
+
if cc != c and self.grid[r][cc] == num:
|
|
192
|
+
self.record_move((row, col), False)
|
|
193
|
+
return MoveResult(
|
|
194
|
+
success=False,
|
|
195
|
+
message=f"Value {num} already exists in row {row}.",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Check column uniqueness
|
|
199
|
+
for rr in range(n):
|
|
200
|
+
if rr != r and self.grid[rr][c] == num:
|
|
201
|
+
self.record_move((row, col), False)
|
|
202
|
+
return MoveResult(
|
|
203
|
+
success=False,
|
|
204
|
+
message=f"Value {num} already exists in column {col}.",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
self.grid[r][c] = num
|
|
208
|
+
self.record_move((row, col), True)
|
|
209
|
+
return MoveResult(
|
|
210
|
+
success=True,
|
|
211
|
+
message=f"Placed {num} at ({row}, {col}).",
|
|
212
|
+
state_changed=True,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def is_complete(self) -> bool:
|
|
216
|
+
"""Check if the puzzle is solved correctly."""
|
|
217
|
+
return self.grid == self.solution
|
|
218
|
+
|
|
219
|
+
async def get_hint(self) -> tuple[Any, str] | None:
|
|
220
|
+
"""Get a hint - suggest a cell to fill."""
|
|
221
|
+
if not self.can_use_hint():
|
|
222
|
+
return None
|
|
223
|
+
n = self.size
|
|
224
|
+
for r in range(n):
|
|
225
|
+
for c in range(n):
|
|
226
|
+
if self.grid[r][c] == 0:
|
|
227
|
+
val = self.solution[r][c]
|
|
228
|
+
return (
|
|
229
|
+
(r + 1, c + 1, val),
|
|
230
|
+
f"Try placing {val} at row {r + 1}, column {c + 1}.",
|
|
231
|
+
)
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
def render_grid(self) -> str:
|
|
235
|
+
"""Render the puzzle with visibility clues."""
|
|
236
|
+
n = self.size
|
|
237
|
+
lines = []
|
|
238
|
+
|
|
239
|
+
# Top clues
|
|
240
|
+
top_clues = " " + " ".join(str(c) if c > 0 else " " for c in self.clues["top"])
|
|
241
|
+
lines.append(top_clues)
|
|
242
|
+
lines.append(" " + "+" + "---" * n + "+")
|
|
243
|
+
|
|
244
|
+
# Grid rows with left/right clues
|
|
245
|
+
for r in range(n):
|
|
246
|
+
left = str(self.clues["left"][r]) if self.clues["left"][r] > 0 else " "
|
|
247
|
+
right = str(self.clues["right"][r]) if self.clues["right"][r] > 0 else " "
|
|
248
|
+
cells = " ".join(str(v) if v != 0 else "." for v in self.grid[r])
|
|
249
|
+
lines.append(f" {left} | {cells} | {right}")
|
|
250
|
+
|
|
251
|
+
# Bottom border and clues
|
|
252
|
+
lines.append(" " + "+" + "---" * n + "+")
|
|
253
|
+
bot_clues = " " + " ".join(str(c) if c > 0 else " " for c in self.clues["bottom"])
|
|
254
|
+
lines.append(bot_clues)
|
|
255
|
+
|
|
256
|
+
return "\n".join(lines)
|
|
257
|
+
|
|
258
|
+
def get_stats(self) -> str:
|
|
259
|
+
"""Get current game statistics."""
|
|
260
|
+
empty = sum(1 for r in range(self.size) for c in range(self.size) if self.grid[r][c] == 0)
|
|
261
|
+
return f"Moves: {self.moves_made} | Empty cells: {empty} | Grid: {self.size}x{self.size} | Seed: {self.seed}"
|
|
262
|
+
|
|
263
|
+
def get_rules(self) -> str:
|
|
264
|
+
return (
|
|
265
|
+
f"SKYSCRAPERS ({self.size}x{self.size})\n"
|
|
266
|
+
f"Fill the grid with numbers 1 to {self.size}.\n"
|
|
267
|
+
"Each row and column must contain each number exactly once.\n"
|
|
268
|
+
"Numbers represent building heights.\n"
|
|
269
|
+
"Clues around the border show how many buildings are visible from that direction.\n"
|
|
270
|
+
"A taller building hides all shorter ones behind it."
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def get_commands(self) -> str:
|
|
274
|
+
return (
|
|
275
|
+
"Commands:\n"
|
|
276
|
+
f" place <row> <col> <height> - Place a height (1-{self.size})\n"
|
|
277
|
+
" clear <row> <col> - Clear a cell\n"
|
|
278
|
+
" hint - Get a hint\n"
|
|
279
|
+
" check - Check if solved\n"
|
|
280
|
+
" show - Show current state\n"
|
|
281
|
+
" menu - Return to menu"
|
|
282
|
+
)
|
|
@@ -272,6 +272,8 @@ class SlitherlinkGame(PuzzleGame):
|
|
|
272
272
|
Returns:
|
|
273
273
|
Tuple of (hint_data, hint_message) or None
|
|
274
274
|
"""
|
|
275
|
+
if not self.can_use_hint():
|
|
276
|
+
return None
|
|
275
277
|
# Find an edge that's in the solution but not set by player
|
|
276
278
|
for row in range(self.size + 1):
|
|
277
279
|
for col in range(self.size):
|
|
@@ -301,6 +301,8 @@ class StarBattleGame(PuzzleGame):
|
|
|
301
301
|
Returns:
|
|
302
302
|
Tuple of (hint_data, hint_message) or None if puzzle is complete
|
|
303
303
|
"""
|
|
304
|
+
if not self.can_use_hint():
|
|
305
|
+
return None
|
|
304
306
|
# Find a star location from solution that hasn't been placed
|
|
305
307
|
for r in range(self.size):
|
|
306
308
|
for c in range(self.size):
|
|
@@ -249,6 +249,8 @@ class SudokuGame(PuzzleGame):
|
|
|
249
249
|
Returns:
|
|
250
250
|
Tuple of (hint_data, hint_message) or None if puzzle is complete
|
|
251
251
|
"""
|
|
252
|
+
if not self.can_use_hint():
|
|
253
|
+
return None
|
|
252
254
|
empty_cells = [(r, c) for r in range(9) for c in range(9) if self.grid[r][c] == 0]
|
|
253
255
|
if not empty_cells:
|
|
254
256
|
return None
|
|
@@ -326,6 +326,8 @@ class TentsGame(PuzzleGame):
|
|
|
326
326
|
Returns:
|
|
327
327
|
Tuple of (hint_data, hint_message) or None if puzzle is complete
|
|
328
328
|
"""
|
|
329
|
+
if not self.can_use_hint():
|
|
330
|
+
return None
|
|
329
331
|
# Find a tent location from solution that hasn't been placed
|
|
330
332
|
for r in range(self.size):
|
|
331
333
|
for c in range(self.size):
|
chuk_puzzles_gym/server.py
CHANGED
|
@@ -202,7 +202,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
202
202
|
# Set up command handler if available for this game
|
|
203
203
|
handler_class = GAME_COMMAND_HANDLERS.get(game_id.lower())
|
|
204
204
|
if handler_class:
|
|
205
|
-
self.game_handler = handler_class(self.current_game)
|
|
205
|
+
self.game_handler = handler_class(self.current_game) # type: ignore[abstract]
|
|
206
206
|
else:
|
|
207
207
|
self.game_handler = None
|
|
208
208
|
|
|
@@ -599,10 +599,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
599
599
|
if self.game_handler and cmd_enum in self.game_handler.supported_commands:
|
|
600
600
|
result = await self.game_handler.handle_command(cmd_enum, parts[1:])
|
|
601
601
|
|
|
602
|
-
# Track invalid moves
|
|
603
|
-
if not result.result.success:
|
|
604
|
-
self.current_game.invalid_moves += 1
|
|
605
|
-
|
|
606
602
|
# Send result based on output mode
|
|
607
603
|
code = "OK" if result.result.success else "INVALID"
|
|
608
604
|
await self.send_result(result.result.success, result.result.message, code)
|
|
@@ -626,9 +622,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
626
622
|
num = int(parts[3])
|
|
627
623
|
|
|
628
624
|
result = await self.current_game.validate_move(row, col, num)
|
|
629
|
-
|
|
630
|
-
if not result.success:
|
|
631
|
-
self.current_game.invalid_moves += 1
|
|
625
|
+
self.current_game.record_move((row, col), result.success)
|
|
632
626
|
|
|
633
627
|
await self.send_result(result.success, result.message, "PLACED" if result.success else "INVALID_MOVE")
|
|
634
628
|
|
|
@@ -639,7 +633,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
639
633
|
await self.send_game_complete()
|
|
640
634
|
|
|
641
635
|
except ValueError:
|
|
642
|
-
self.current_game.invalid_moves += 1
|
|
643
636
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
644
637
|
return
|
|
645
638
|
|
|
@@ -653,9 +646,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
653
646
|
col = int(parts[2])
|
|
654
647
|
|
|
655
648
|
result = await self.current_game.validate_move(row, col, 0)
|
|
656
|
-
|
|
657
|
-
if not result.success:
|
|
658
|
-
self.current_game.invalid_moves += 1
|
|
649
|
+
self.current_game.record_move((row, col), result.success)
|
|
659
650
|
|
|
660
651
|
await self.send_result(result.success, result.message, "CLEARED" if result.success else "INVALID_CLEAR")
|
|
661
652
|
|
|
@@ -663,7 +654,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
663
654
|
await self.display_puzzle()
|
|
664
655
|
|
|
665
656
|
except ValueError:
|
|
666
|
-
self.current_game.invalid_moves += 1
|
|
667
657
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
668
658
|
return
|
|
669
659
|
|
|
@@ -693,9 +683,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
693
683
|
col = int(parts[2])
|
|
694
684
|
|
|
695
685
|
result = await self.current_game.validate_move(row, col)
|
|
696
|
-
|
|
697
|
-
if not result.success:
|
|
698
|
-
self.current_game.invalid_moves += 1
|
|
686
|
+
self.current_game.record_move((row, col), result.success)
|
|
699
687
|
|
|
700
688
|
await self.send_result(result.success, result.message, "PRESSED" if result.success else "INVALID_PRESS")
|
|
701
689
|
|
|
@@ -706,7 +694,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
706
694
|
await self.send_game_complete()
|
|
707
695
|
|
|
708
696
|
except ValueError:
|
|
709
|
-
self.current_game.invalid_moves += 1
|
|
710
697
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
711
698
|
return
|
|
712
699
|
|
|
@@ -718,9 +705,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
718
705
|
|
|
719
706
|
cat1, val1, cat2, val2 = parts[1], parts[2], parts[3], parts[4]
|
|
720
707
|
result = await self.current_game.validate_move(cat1, val1, cat2, val2, True)
|
|
721
|
-
|
|
722
|
-
if not result.success:
|
|
723
|
-
self.current_game.invalid_moves += 1
|
|
708
|
+
self.current_game.record_move((cat1, val1, cat2, val2), result.success)
|
|
724
709
|
|
|
725
710
|
await self.send_result(result.success, result.message, "CONNECTED" if result.success else "INVALID_CONNECT")
|
|
726
711
|
if result.success:
|
|
@@ -736,9 +721,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
736
721
|
|
|
737
722
|
cat1, val1, cat2, val2 = parts[1], parts[2], parts[3], parts[4]
|
|
738
723
|
result = await self.current_game.validate_move(cat1, val1, cat2, val2, False)
|
|
739
|
-
|
|
740
|
-
if not result.success:
|
|
741
|
-
self.current_game.invalid_moves += 1
|
|
724
|
+
self.current_game.record_move((cat1, val1, cat2, val2), result.success)
|
|
742
725
|
|
|
743
726
|
await self.send_result(result.success, result.message, "EXCLUDED" if result.success else "INVALID_EXCLUDE")
|
|
744
727
|
if result.success:
|
|
@@ -758,9 +741,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
758
741
|
col = int(parts[2])
|
|
759
742
|
|
|
760
743
|
result = await self.current_game.validate_move("reveal", row, col)
|
|
761
|
-
|
|
762
|
-
if not result.success:
|
|
763
|
-
self.current_game.invalid_moves += 1
|
|
744
|
+
self.current_game.record_move((row, col), result.success)
|
|
764
745
|
|
|
765
746
|
await self.send_result(
|
|
766
747
|
result.success, result.message, "REVEALED" if result.success else "INVALID_REVEAL"
|
|
@@ -783,7 +764,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
783
764
|
await self.send_line("=" * 50 + "\n")
|
|
784
765
|
|
|
785
766
|
except ValueError:
|
|
786
|
-
self.current_game.invalid_moves += 1
|
|
787
767
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
788
768
|
return
|
|
789
769
|
|
|
@@ -797,9 +777,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
797
777
|
col = int(parts[2])
|
|
798
778
|
|
|
799
779
|
result = await self.current_game.validate_move("flag", row, col)
|
|
800
|
-
|
|
801
|
-
if not result.success:
|
|
802
|
-
self.current_game.invalid_moves += 1
|
|
780
|
+
self.current_game.record_move((row, col), result.success)
|
|
803
781
|
|
|
804
782
|
await self.send_result(result.success, result.message, "FLAGGED" if result.success else "INVALID_FLAG")
|
|
805
783
|
|
|
@@ -807,7 +785,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
807
785
|
await self.display_puzzle()
|
|
808
786
|
|
|
809
787
|
except ValueError:
|
|
810
|
-
self.current_game.invalid_moves += 1
|
|
811
788
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
812
789
|
return
|
|
813
790
|
|
|
@@ -824,9 +801,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
824
801
|
state = int(parts[4])
|
|
825
802
|
|
|
826
803
|
result = await self.current_game.validate_move(edge_type, row, col, state)
|
|
827
|
-
|
|
828
|
-
if not result.success:
|
|
829
|
-
self.current_game.invalid_moves += 1
|
|
804
|
+
self.current_game.record_move((edge_type, row, col), result.success)
|
|
830
805
|
|
|
831
806
|
await self.send_result(result.success, result.message, "SET" if result.success else "INVALID_SET")
|
|
832
807
|
|
|
@@ -837,7 +812,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
837
812
|
await self.send_game_complete()
|
|
838
813
|
|
|
839
814
|
except ValueError:
|
|
840
|
-
self.current_game.invalid_moves += 1
|
|
841
815
|
await self.send_result(False, "Invalid input. Use numbers only for row, col, state.", "PARSE_ERROR")
|
|
842
816
|
return
|
|
843
817
|
|
|
@@ -851,9 +825,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
851
825
|
guess = [int(p) for p in parts[1:]]
|
|
852
826
|
|
|
853
827
|
result = await self.current_game.validate_move(*guess)
|
|
854
|
-
|
|
855
|
-
if not result.success:
|
|
856
|
-
self.current_game.invalid_moves += 1
|
|
828
|
+
self.current_game.record_move(tuple(guess), result.success)
|
|
857
829
|
|
|
858
830
|
await self.send_result(result.success, result.message, "GUESSED" if result.success else "INVALID_GUESS")
|
|
859
831
|
|
|
@@ -874,7 +846,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
874
846
|
await self.send_line("=" * 50 + "\n")
|
|
875
847
|
|
|
876
848
|
except ValueError:
|
|
877
|
-
self.current_game.invalid_moves += 1
|
|
878
849
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
879
850
|
return
|
|
880
851
|
|
|
@@ -888,9 +859,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
888
859
|
item_index = int(parts[1])
|
|
889
860
|
|
|
890
861
|
result = await self.current_game.validate_move("select", item_index)
|
|
891
|
-
|
|
892
|
-
if not result.success:
|
|
893
|
-
self.current_game.invalid_moves += 1
|
|
862
|
+
self.current_game.record_move((item_index,), result.success)
|
|
894
863
|
|
|
895
864
|
await self.send_result(
|
|
896
865
|
result.success, result.message, "SELECTED" if result.success else "INVALID_SELECT"
|
|
@@ -900,7 +869,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
900
869
|
await self.display_puzzle()
|
|
901
870
|
|
|
902
871
|
except ValueError:
|
|
903
|
-
self.current_game.invalid_moves += 1
|
|
904
872
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
905
873
|
return
|
|
906
874
|
|
|
@@ -913,9 +881,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
913
881
|
item_index = int(parts[1])
|
|
914
882
|
|
|
915
883
|
result = await self.current_game.validate_move("deselect", item_index)
|
|
916
|
-
|
|
917
|
-
if not result.success:
|
|
918
|
-
self.current_game.invalid_moves += 1
|
|
884
|
+
self.current_game.record_move((item_index,), result.success)
|
|
919
885
|
|
|
920
886
|
await self.send_result(
|
|
921
887
|
result.success, result.message, "DESELECTED" if result.success else "INVALID_DESELECT"
|
|
@@ -925,7 +891,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
925
891
|
await self.display_puzzle()
|
|
926
892
|
|
|
927
893
|
except ValueError:
|
|
928
|
-
self.current_game.invalid_moves += 1
|
|
929
894
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
930
895
|
return
|
|
931
896
|
|
|
@@ -941,9 +906,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
941
906
|
color = parts[3].lower()
|
|
942
907
|
|
|
943
908
|
result = await self.current_game.validate_move(row, col, color)
|
|
944
|
-
|
|
945
|
-
if not result.success:
|
|
946
|
-
self.current_game.invalid_moves += 1
|
|
909
|
+
self.current_game.record_move((row, col), result.success)
|
|
947
910
|
|
|
948
911
|
await self.send_result(result.success, result.message, "MARKED" if result.success else "INVALID_MARK")
|
|
949
912
|
|
|
@@ -954,7 +917,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
954
917
|
await self.send_game_complete()
|
|
955
918
|
|
|
956
919
|
except ValueError:
|
|
957
|
-
self.current_game.invalid_moves += 1
|
|
958
920
|
await self.send_result(False, "Invalid input. Row and col must be numbers.", "PARSE_ERROR")
|
|
959
921
|
return
|
|
960
922
|
|
|
@@ -969,9 +931,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
969
931
|
col = int(parts[2])
|
|
970
932
|
|
|
971
933
|
result = await self.current_game.validate_move(row, col, "shade")
|
|
972
|
-
|
|
973
|
-
if not result.success:
|
|
974
|
-
self.current_game.invalid_moves += 1
|
|
934
|
+
self.current_game.record_move((row, col), result.success)
|
|
975
935
|
|
|
976
936
|
await self.send_result(result.success, result.message, "SHADED" if result.success else "INVALID_SHADE")
|
|
977
937
|
|
|
@@ -982,7 +942,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
982
942
|
await self.send_game_complete()
|
|
983
943
|
|
|
984
944
|
except ValueError:
|
|
985
|
-
self.current_game.invalid_moves += 1
|
|
986
945
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
987
946
|
return
|
|
988
947
|
|
|
@@ -1000,9 +959,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
1000
959
|
count = int(parts[5])
|
|
1001
960
|
|
|
1002
961
|
result = await self.current_game.validate_move(r1, c1, r2, c2, count)
|
|
1003
|
-
|
|
1004
|
-
if not result.success:
|
|
1005
|
-
self.current_game.invalid_moves += 1
|
|
962
|
+
self.current_game.record_move((r1, c1, r2, c2), result.success)
|
|
1006
963
|
|
|
1007
964
|
await self.send_result(
|
|
1008
965
|
result.success, result.message, "BRIDGED" if result.success else "INVALID_BRIDGE"
|
|
@@ -1015,7 +972,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
1015
972
|
await self.send_game_complete()
|
|
1016
973
|
|
|
1017
974
|
except ValueError:
|
|
1018
|
-
self.current_game.invalid_moves += 1
|
|
1019
975
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
1020
976
|
return
|
|
1021
977
|
|
|
@@ -1028,9 +984,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
1028
984
|
direction = parts[1].lower()
|
|
1029
985
|
|
|
1030
986
|
result = await self.current_game.validate_move(direction)
|
|
1031
|
-
|
|
1032
|
-
if not result.success:
|
|
1033
|
-
self.current_game.invalid_moves += 1
|
|
987
|
+
self.current_game.record_move((direction,), result.success)
|
|
1034
988
|
|
|
1035
989
|
await self.send_result(result.success, result.message, "MOVED" if result.success else "INVALID_MOVE")
|
|
1036
990
|
|
|
@@ -1054,9 +1008,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
1054
1008
|
start_time = int(parts[3])
|
|
1055
1009
|
|
|
1056
1010
|
result = await self.current_game.validate_move(task_id, worker_id, start_time)
|
|
1057
|
-
|
|
1058
|
-
if not result.success:
|
|
1059
|
-
self.current_game.invalid_moves += 1
|
|
1011
|
+
self.current_game.record_move((task_id,), result.success)
|
|
1060
1012
|
|
|
1061
1013
|
await self.send_result(
|
|
1062
1014
|
result.success, result.message, "ASSIGNED" if result.success else "INVALID_ASSIGN"
|
|
@@ -1068,7 +1020,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
1068
1020
|
await self.send_game_complete()
|
|
1069
1021
|
|
|
1070
1022
|
except ValueError:
|
|
1071
|
-
self.current_game.invalid_moves += 1
|
|
1072
1023
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
1073
1024
|
return
|
|
1074
1025
|
|
|
@@ -1081,9 +1032,7 @@ class ArcadeHandler(TelnetHandler):
|
|
|
1081
1032
|
task_id = int(parts[1])
|
|
1082
1033
|
|
|
1083
1034
|
result = await self.current_game.validate_move(task_id, 0, -1)
|
|
1084
|
-
|
|
1085
|
-
if not result.success:
|
|
1086
|
-
self.current_game.invalid_moves += 1
|
|
1035
|
+
self.current_game.record_move((task_id,), result.success)
|
|
1087
1036
|
|
|
1088
1037
|
await self.send_result(
|
|
1089
1038
|
result.success, result.message, "UNASSIGNED" if result.success else "INVALID_UNASSIGN"
|
|
@@ -1093,7 +1042,6 @@ class ArcadeHandler(TelnetHandler):
|
|
|
1093
1042
|
await self.display_puzzle()
|
|
1094
1043
|
|
|
1095
1044
|
except ValueError:
|
|
1096
|
-
self.current_game.invalid_moves += 1
|
|
1097
1045
|
await self.send_result(False, "Invalid input. Use numbers only.", "PARSE_ERROR")
|
|
1098
1046
|
return
|
|
1099
1047
|
|