chuk-puzzles-gym 0.9__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.
Files changed (112) hide show
  1. chuk_puzzles_gym/__init__.py +19 -0
  2. chuk_puzzles_gym/constants.py +9 -0
  3. chuk_puzzles_gym/eval.py +763 -0
  4. chuk_puzzles_gym/export/__init__.py +20 -0
  5. chuk_puzzles_gym/export/dataset.py +376 -0
  6. chuk_puzzles_gym/games/__init__.py +94 -0
  7. chuk_puzzles_gym/games/_base/__init__.py +6 -0
  8. chuk_puzzles_gym/games/_base/commands.py +91 -0
  9. chuk_puzzles_gym/games/_base/game.py +337 -0
  10. chuk_puzzles_gym/games/binary/__init__.py +6 -0
  11. chuk_puzzles_gym/games/binary/config.py +23 -0
  12. chuk_puzzles_gym/games/binary/game.py +434 -0
  13. chuk_puzzles_gym/games/bridges/__init__.py +6 -0
  14. chuk_puzzles_gym/games/bridges/config.py +24 -0
  15. chuk_puzzles_gym/games/bridges/game.py +489 -0
  16. chuk_puzzles_gym/games/einstein/__init__.py +6 -0
  17. chuk_puzzles_gym/games/einstein/config.py +23 -0
  18. chuk_puzzles_gym/games/einstein/constants.py +13 -0
  19. chuk_puzzles_gym/games/einstein/game.py +366 -0
  20. chuk_puzzles_gym/games/einstein/models.py +35 -0
  21. chuk_puzzles_gym/games/fillomino/__init__.py +6 -0
  22. chuk_puzzles_gym/games/fillomino/config.py +24 -0
  23. chuk_puzzles_gym/games/fillomino/game.py +516 -0
  24. chuk_puzzles_gym/games/futoshiki/__init__.py +6 -0
  25. chuk_puzzles_gym/games/futoshiki/config.py +23 -0
  26. chuk_puzzles_gym/games/futoshiki/game.py +391 -0
  27. chuk_puzzles_gym/games/hidato/__init__.py +6 -0
  28. chuk_puzzles_gym/games/hidato/config.py +24 -0
  29. chuk_puzzles_gym/games/hidato/game.py +403 -0
  30. chuk_puzzles_gym/games/hitori/__init__.py +6 -0
  31. chuk_puzzles_gym/games/hitori/config.py +23 -0
  32. chuk_puzzles_gym/games/hitori/game.py +451 -0
  33. chuk_puzzles_gym/games/kakuro/__init__.py +6 -0
  34. chuk_puzzles_gym/games/kakuro/config.py +24 -0
  35. chuk_puzzles_gym/games/kakuro/game.py +399 -0
  36. chuk_puzzles_gym/games/kenken/__init__.py +6 -0
  37. chuk_puzzles_gym/games/kenken/config.py +24 -0
  38. chuk_puzzles_gym/games/kenken/enums.py +13 -0
  39. chuk_puzzles_gym/games/kenken/game.py +486 -0
  40. chuk_puzzles_gym/games/kenken/models.py +15 -0
  41. chuk_puzzles_gym/games/killer_sudoku/__init__.py +6 -0
  42. chuk_puzzles_gym/games/killer_sudoku/config.py +23 -0
  43. chuk_puzzles_gym/games/killer_sudoku/game.py +502 -0
  44. chuk_puzzles_gym/games/killer_sudoku/models.py +15 -0
  45. chuk_puzzles_gym/games/knapsack/__init__.py +6 -0
  46. chuk_puzzles_gym/games/knapsack/config.py +24 -0
  47. chuk_puzzles_gym/games/knapsack/enums.py +10 -0
  48. chuk_puzzles_gym/games/knapsack/game.py +340 -0
  49. chuk_puzzles_gym/games/knapsack/models.py +13 -0
  50. chuk_puzzles_gym/games/lights_out/__init__.py +6 -0
  51. chuk_puzzles_gym/games/lights_out/config.py +24 -0
  52. chuk_puzzles_gym/games/lights_out/game.py +249 -0
  53. chuk_puzzles_gym/games/logic_grid/__init__.py +6 -0
  54. chuk_puzzles_gym/games/logic_grid/config.py +24 -0
  55. chuk_puzzles_gym/games/logic_grid/constants.py +12 -0
  56. chuk_puzzles_gym/games/logic_grid/game.py +333 -0
  57. chuk_puzzles_gym/games/logic_grid/models.py +24 -0
  58. chuk_puzzles_gym/games/mastermind/__init__.py +6 -0
  59. chuk_puzzles_gym/games/mastermind/config.py +25 -0
  60. chuk_puzzles_gym/games/mastermind/game.py +297 -0
  61. chuk_puzzles_gym/games/minesweeper/__init__.py +6 -0
  62. chuk_puzzles_gym/games/minesweeper/config.py +24 -0
  63. chuk_puzzles_gym/games/minesweeper/enums.py +12 -0
  64. chuk_puzzles_gym/games/minesweeper/game.py +432 -0
  65. chuk_puzzles_gym/games/nonogram/__init__.py +6 -0
  66. chuk_puzzles_gym/games/nonogram/config.py +23 -0
  67. chuk_puzzles_gym/games/nonogram/game.py +296 -0
  68. chuk_puzzles_gym/games/nurikabe/__init__.py +6 -0
  69. chuk_puzzles_gym/games/nurikabe/config.py +24 -0
  70. chuk_puzzles_gym/games/nurikabe/enums.py +14 -0
  71. chuk_puzzles_gym/games/nurikabe/game.py +586 -0
  72. chuk_puzzles_gym/games/scheduler/__init__.py +6 -0
  73. chuk_puzzles_gym/games/scheduler/config.py +25 -0
  74. chuk_puzzles_gym/games/scheduler/constants.py +15 -0
  75. chuk_puzzles_gym/games/scheduler/enums.py +10 -0
  76. chuk_puzzles_gym/games/scheduler/game.py +431 -0
  77. chuk_puzzles_gym/games/scheduler/models.py +14 -0
  78. chuk_puzzles_gym/games/shikaku/__init__.py +6 -0
  79. chuk_puzzles_gym/games/shikaku/config.py +24 -0
  80. chuk_puzzles_gym/games/shikaku/game.py +419 -0
  81. chuk_puzzles_gym/games/slitherlink/__init__.py +6 -0
  82. chuk_puzzles_gym/games/slitherlink/config.py +23 -0
  83. chuk_puzzles_gym/games/slitherlink/game.py +386 -0
  84. chuk_puzzles_gym/games/sokoban/__init__.py +6 -0
  85. chuk_puzzles_gym/games/sokoban/config.py +24 -0
  86. chuk_puzzles_gym/games/sokoban/game.py +671 -0
  87. chuk_puzzles_gym/games/star_battle/__init__.py +6 -0
  88. chuk_puzzles_gym/games/star_battle/config.py +24 -0
  89. chuk_puzzles_gym/games/star_battle/game.py +390 -0
  90. chuk_puzzles_gym/games/sudoku/__init__.py +7 -0
  91. chuk_puzzles_gym/games/sudoku/commands.py +96 -0
  92. chuk_puzzles_gym/games/sudoku/config.py +22 -0
  93. chuk_puzzles_gym/games/sudoku/game.py +328 -0
  94. chuk_puzzles_gym/games/tents/__init__.py +6 -0
  95. chuk_puzzles_gym/games/tents/config.py +24 -0
  96. chuk_puzzles_gym/games/tents/game.py +416 -0
  97. chuk_puzzles_gym/gym_env.py +465 -0
  98. chuk_puzzles_gym/models/__init__.py +47 -0
  99. chuk_puzzles_gym/models/base.py +30 -0
  100. chuk_puzzles_gym/models/config.py +11 -0
  101. chuk_puzzles_gym/models/enums.py +104 -0
  102. chuk_puzzles_gym/models/evaluation.py +487 -0
  103. chuk_puzzles_gym/models/games.py +12 -0
  104. chuk_puzzles_gym/server.py +1171 -0
  105. chuk_puzzles_gym/trace/__init__.py +10 -0
  106. chuk_puzzles_gym/trace/generator.py +726 -0
  107. chuk_puzzles_gym/utils/__init__.py +4 -0
  108. chuk_puzzles_gym-0.9.dist-info/METADATA +1471 -0
  109. chuk_puzzles_gym-0.9.dist-info/RECORD +112 -0
  110. chuk_puzzles_gym-0.9.dist-info/WHEEL +5 -0
  111. chuk_puzzles_gym-0.9.dist-info/entry_points.txt +4 -0
  112. chuk_puzzles_gym-0.9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,333 @@
1
+ """Logic Grid puzzle game implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from ...models import DifficultyProfile, MoveResult
6
+ from .._base import PuzzleGame
7
+ from .config import LogicGridConfig
8
+ from .constants import CATEGORIES, COLORS, DRINKS, PEOPLE, PETS
9
+ from .models import LogicGridCategories, PersonAttributes
10
+
11
+
12
+ class LogicGridGame(PuzzleGame):
13
+ """Logic Grid puzzle game (like Einstein's Riddle or Zebra Puzzle).
14
+
15
+ Use logical deduction to determine which attributes belong together.
16
+ Each person/house has exactly one of each attribute type.
17
+ """
18
+
19
+ def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
20
+ """Initialize a new Logic Grid game.
21
+
22
+ Args:
23
+ difficulty: Game difficulty level (easy=3x3, medium=4x4, hard=5x5)
24
+ """
25
+ super().__init__(difficulty, seed, **kwargs)
26
+
27
+ # Use pydantic config based on difficulty
28
+ self.config = LogicGridConfig.from_difficulty(self.difficulty)
29
+ self.num_people = self.config.num_people
30
+
31
+ # Categories using Pydantic model with constants
32
+ self.categories = LogicGridCategories(
33
+ person=PEOPLE[: self.num_people],
34
+ color=COLORS[: self.num_people],
35
+ pet=PETS[: self.num_people],
36
+ drink=DRINKS[: self.num_people],
37
+ )
38
+
39
+ # Solution: dict mapping person -> PersonAttributes
40
+ self.solution: dict[str, PersonAttributes] = {}
41
+
42
+ # Player grid: dict of (category1, value1, category2, value2) -> bool | None
43
+ # True = definitely connected, False = definitely not connected, None = unknown
44
+ self.player_grid: dict[tuple[str, str, str, str], bool | None] = {}
45
+
46
+ # Clues: list of clue strings
47
+ self.clues: list[str] = []
48
+
49
+ @property
50
+ def name(self) -> str:
51
+ """The display name of this puzzle type."""
52
+ return "Logic Grid"
53
+
54
+ @property
55
+ def description(self) -> str:
56
+ """A one-line description of this puzzle type."""
57
+ return "Deductive reasoning puzzle - match attributes using logic"
58
+
59
+ @property
60
+ def constraint_types(self) -> list[str]:
61
+ """Constraint types demonstrated by this puzzle."""
62
+ return [
63
+ "all_different_per_attribute",
64
+ "cross_attribute_links",
65
+ "transitive_closure",
66
+ "bi-directional_inference",
67
+ ]
68
+
69
+ @property
70
+ def business_analogies(self) -> list[str]:
71
+ """Business problems this puzzle models."""
72
+ return ["multi_factor_matching", "relationship_mapping", "entity_resolution", "attribute_correlation"]
73
+
74
+ @property
75
+ def complexity_profile(self) -> dict[str, str]:
76
+ """Complexity profile of this puzzle."""
77
+ return {"reasoning_type": "deductive", "search_space": "medium", "constraint_density": "moderate"}
78
+
79
+ @property
80
+ def optimal_steps(self) -> int | None:
81
+ """Minimum steps = attribute assignments (people x attributes)."""
82
+ return self.num_people * 3 # 3 attributes per person
83
+
84
+ @property
85
+ def difficulty_profile(self) -> "DifficultyProfile":
86
+ """Difficulty characteristics for Logic Grid."""
87
+ from ...models import DifficultyLevel
88
+
89
+ logic_depth = {
90
+ DifficultyLevel.EASY.value: 3,
91
+ DifficultyLevel.MEDIUM.value: 5,
92
+ DifficultyLevel.HARD.value: 7,
93
+ }.get(self.difficulty.value, 4)
94
+ return DifficultyProfile(
95
+ logic_depth=logic_depth,
96
+ branching_factor=float(self.num_people),
97
+ state_observability=1.0,
98
+ constraint_density=0.6,
99
+ )
100
+
101
+ def _generate_solution(self) -> None:
102
+ """Generate a random valid solution."""
103
+ people = self.categories.person
104
+
105
+ # Randomly assign each attribute to each person
106
+ colors = self.categories.color[:]
107
+ pets = self.categories.pet[:]
108
+ drinks = self.categories.drink[:]
109
+
110
+ self._rng.shuffle(colors)
111
+ self._rng.shuffle(pets)
112
+ self._rng.shuffle(drinks)
113
+
114
+ self.solution = {}
115
+ for i, person in enumerate(people):
116
+ self.solution[person] = PersonAttributes(
117
+ color=colors[i],
118
+ pet=pets[i],
119
+ drink=drinks[i],
120
+ )
121
+
122
+ def _generate_clues(self) -> None:
123
+ """Generate clues from the solution."""
124
+ self.clues = []
125
+ people = self.categories.person
126
+
127
+ # Generate direct association clues
128
+ num_direct = self.num_people - 1
129
+ for i in range(num_direct):
130
+ person = people[i]
131
+ attrs = self.solution[person]
132
+
133
+ # Choose two attributes to reveal
134
+ cat1, cat2 = self._rng.sample(["color", "pet", "drink"], 2)
135
+ val1 = getattr(attrs, cat1)
136
+ val2 = getattr(attrs, cat2)
137
+ clue = f"{person} has the {val1} {cat1} and drinks {val2}"
138
+ self.clues.append(clue)
139
+
140
+ # Generate relative/constraint clues
141
+ for _ in range(self.num_people):
142
+ p1, p2 = self._rng.sample(people, 2)
143
+ cat = self._rng.choice(["color", "pet", "drink"])
144
+
145
+ val = getattr(self.solution[p2], cat)
146
+ clue = f"{p1} does not have the {val} {cat}"
147
+ self.clues.append(clue)
148
+
149
+ async def generate_puzzle(self) -> None:
150
+ """Generate a new Logic Grid puzzle."""
151
+ self._generate_solution()
152
+ self._generate_clues()
153
+
154
+ # Initialize player grid (all unknown)
155
+ self.player_grid = {}
156
+
157
+ self.moves_made = 0
158
+ self.game_started = True
159
+
160
+ async def validate_move(self, cat1: str, val1: str, cat2: str, val2: str, state: bool) -> MoveResult:
161
+ """Mark a connection in the logic grid.
162
+
163
+ Args:
164
+ cat1: First category
165
+ val1: First value
166
+ cat2: Second category
167
+ val2: Second value
168
+ state: True = connected, False = not connected
169
+
170
+ Returns:
171
+ MoveResult with success status and message
172
+ """
173
+ # Normalize categories
174
+ cat1 = cat1.lower()
175
+ cat2 = cat2.lower()
176
+
177
+ # Validate categories
178
+ valid_categories = ["person", "color", "pet", "drink"]
179
+ if cat1 not in valid_categories or cat2 not in valid_categories:
180
+ return MoveResult(success=False, message=f"Invalid category. Use: {', '.join(valid_categories)}")
181
+
182
+ if cat1 == cat2:
183
+ return MoveResult(success=False, message="Cannot connect values from the same category")
184
+
185
+ # Validate values
186
+ cat1_values = getattr(self.categories, cat1)
187
+ cat2_values = getattr(self.categories, cat2)
188
+
189
+ if val1 not in cat1_values:
190
+ return MoveResult(success=False, message=f"Invalid {cat1}. Choose from: {', '.join(cat1_values)}")
191
+
192
+ if val2 not in cat2_values:
193
+ return MoveResult(success=False, message=f"Invalid {cat2}. Choose from: {', '.join(cat2_values)}")
194
+
195
+ # Store the connection (normalize order)
196
+ key = (cat1, val1, cat2, val2) if cat1 < cat2 else (cat2, val2, cat1, val1)
197
+ self.player_grid[key] = state
198
+ self.moves_made += 1
199
+
200
+ return MoveResult(
201
+ success=True,
202
+ message=f"Marked {val1} ({cat1}) and {val2} ({cat2}) as {'connected' if state else 'not connected'}",
203
+ state_changed=True,
204
+ )
205
+
206
+ def is_complete(self) -> bool:
207
+ """Check if the puzzle is complete and correct."""
208
+ # Check if player has correctly identified all connections
209
+ for person in self.categories.person:
210
+ attrs = self.solution[person]
211
+
212
+ # Check person -> color
213
+ key1 = ("color", attrs.color, "person", person)
214
+ key2 = ("person", person, "color", attrs.color)
215
+ if not self.player_grid.get(key1) and not self.player_grid.get(key2):
216
+ return False
217
+
218
+ # Check person -> pet
219
+ key1 = ("person", person, "pet", attrs.pet)
220
+ key2 = ("pet", attrs.pet, "person", person)
221
+ if not self.player_grid.get(key1) and not self.player_grid.get(key2):
222
+ return False
223
+
224
+ # Check person -> drink
225
+ key1 = ("drink", attrs.drink, "person", person)
226
+ key2 = ("person", person, "drink", attrs.drink)
227
+ if not self.player_grid.get(key1) and not self.player_grid.get(key2):
228
+ return False
229
+
230
+ return True
231
+
232
+ async def get_hint(self) -> tuple[Any, str] | None:
233
+ """Get a hint for the next move.
234
+
235
+ Returns:
236
+ Tuple of (hint_data, hint_message) or None if puzzle is complete
237
+ """
238
+ # Find a connection that hasn't been marked
239
+ for person in self.categories.person:
240
+ attrs = self.solution[person]
241
+
242
+ # Check all categories except person
243
+ for cat in [c for c in CATEGORIES if c != "person"]:
244
+ val = getattr(attrs, cat)
245
+ key1 = (cat, val, "person", person)
246
+ key2 = ("person", person, cat, val)
247
+
248
+ if not self.player_grid.get(key1) and not self.player_grid.get(key2):
249
+ hint_data = (person, cat, val)
250
+ hint_message = f"{person} has the {val} {cat}"
251
+ return hint_data, hint_message
252
+
253
+ return None
254
+
255
+ def render_grid(self) -> str:
256
+ """Render the current puzzle state.
257
+
258
+ Returns:
259
+ String representation of the clues and current deductions
260
+ """
261
+ lines = []
262
+
263
+ lines.append("\n=== LOGIC GRID PUZZLE ===\n")
264
+
265
+ # Show clues
266
+ lines.append("CLUES:")
267
+ for i, clue in enumerate(self.clues, 1):
268
+ lines.append(f" {i}. {clue}")
269
+
270
+ lines.append("\nYOUR DEDUCTIONS:")
271
+ if not self.player_grid:
272
+ lines.append(" (none yet)")
273
+ else:
274
+ for (cat1, val1, cat2, val2), state in sorted(self.player_grid.items()):
275
+ if state is True:
276
+ lines.append(f" ✓ {val1} ({cat1}) ←→ {val2} ({cat2})")
277
+ elif state is False:
278
+ lines.append(f" ✗ {val1} ({cat1}) ←/→ {val2} ({cat2})")
279
+
280
+ lines.append("\nCATEGORIES:")
281
+ for cat in CATEGORIES:
282
+ values = getattr(self.categories, cat)
283
+ lines.append(f" {cat.capitalize()}: {', '.join(values)}")
284
+
285
+ return "\n".join(lines)
286
+
287
+ def get_rules(self) -> str:
288
+ """Get the rules description for Logic Grid.
289
+
290
+ Returns:
291
+ Multi-line string describing the puzzle rules
292
+ """
293
+ return """LOGIC GRID RULES:
294
+ - Use logical deduction to match attributes
295
+ - Each person has exactly one color, one pet, and one drink
296
+ - No two people share the same attribute value
297
+ - Read the clues carefully and mark connections
298
+ - Mark connections as True (✓) or False (✗)
299
+ - Use elimination and deduction to solve"""
300
+
301
+ def get_commands(self) -> str:
302
+ """Get the available commands for Logic Grid.
303
+
304
+ Returns:
305
+ Multi-line string describing available commands
306
+ """
307
+ return """LOGIC GRID COMMANDS:
308
+ connect <cat1> <val1> <cat2> <val2>
309
+ - Mark that val1 and val2 are connected (belong to same person)
310
+ - Example: 'connect person Alice color Red'
311
+
312
+ exclude <cat1> <val1> <cat2> <val2>
313
+ - Mark that val1 and val2 are NOT connected
314
+ - Example: 'exclude person Bob pet Cat'
315
+
316
+ show - Display clues and deductions
317
+ hint - Get a hint
318
+ check - Check if puzzle is solved
319
+ solve - Show the solution (ends game)
320
+ menu - Return to game selection
321
+ quit - Exit the server"""
322
+
323
+ def get_stats(self) -> str:
324
+ """Get current game statistics.
325
+
326
+ Returns:
327
+ String with game stats
328
+ """
329
+ connections = sum(1 for v in self.player_grid.values() if v is True)
330
+ exclusions = sum(1 for v in self.player_grid.values() if v is False)
331
+ return (
332
+ f"Moves made: {self.moves_made} | Connections: {connections} | Exclusions: {exclusions} | Seed: {self.seed}"
333
+ )
@@ -0,0 +1,24 @@
1
+ """Logic Grid puzzle game models."""
2
+
3
+ from pydantic import BaseModel, ConfigDict
4
+
5
+
6
+ class LogicGridCategories(BaseModel):
7
+ """Categories for Logic Grid puzzle."""
8
+
9
+ model_config = ConfigDict(frozen=True)
10
+
11
+ person: list[str]
12
+ color: list[str]
13
+ pet: list[str]
14
+ drink: list[str]
15
+
16
+
17
+ class PersonAttributes(BaseModel):
18
+ """Attributes for a person in Logic Grid puzzle."""
19
+
20
+ model_config = ConfigDict(frozen=False)
21
+
22
+ color: str
23
+ pet: str
24
+ drink: str
@@ -0,0 +1,6 @@
1
+ """Mastermind puzzle game module."""
2
+
3
+ from .config import MastermindConfig
4
+ from .game import MastermindGame
5
+
6
+ __all__ = ["MastermindGame", "MastermindConfig"]
@@ -0,0 +1,25 @@
1
+ """Configuration for Mastermind game."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ...models.enums import DifficultyLevel
6
+
7
+
8
+ class MastermindConfig(BaseModel):
9
+ """Configuration for Mastermind game."""
10
+
11
+ difficulty: DifficultyLevel = Field(default=DifficultyLevel.EASY, description="Game difficulty level")
12
+ code_length: int = Field(ge=3, le=6, description="Length of the secret code")
13
+ num_colors: int = Field(ge=4, le=8, description="Number of available colors")
14
+ max_guesses: int = Field(ge=8, le=15, description="Maximum number of guesses allowed")
15
+
16
+ @classmethod
17
+ def from_difficulty(cls, difficulty: DifficultyLevel) -> "MastermindConfig":
18
+ """Create config from difficulty level."""
19
+ config_map = {
20
+ DifficultyLevel.EASY: {"code_length": 4, "num_colors": 6, "max_guesses": 12},
21
+ DifficultyLevel.MEDIUM: {"code_length": 5, "num_colors": 7, "max_guesses": 12},
22
+ DifficultyLevel.HARD: {"code_length": 6, "num_colors": 8, "max_guesses": 15},
23
+ }
24
+ params = config_map[difficulty]
25
+ return cls(difficulty=difficulty, **params)
@@ -0,0 +1,297 @@
1
+ """Mastermind puzzle game implementation."""
2
+
3
+ from typing import Any
4
+
5
+ from ...models import DifficultyProfile, MoveResult
6
+ from .._base import PuzzleGame
7
+ from .config import MastermindConfig
8
+
9
+
10
+ class MastermindGame(PuzzleGame):
11
+ """Mastermind code-breaking puzzle game.
12
+
13
+ Guess the secret code using logical deduction from feedback.
14
+ Each guess gives you black pegs (correct color + position)
15
+ and white pegs (correct color, wrong position).
16
+ """
17
+
18
+ def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
19
+ """Initialize a new Mastermind game.
20
+
21
+ Args:
22
+ difficulty: Game difficulty level (easy/medium/hard)
23
+ """
24
+ super().__init__(difficulty, seed, **kwargs)
25
+
26
+ # Use pydantic config based on difficulty
27
+ self.config = MastermindConfig.from_difficulty(self.difficulty)
28
+ self.code_length = self.config.code_length
29
+ self.num_colors = self.config.num_colors
30
+ self.max_guesses = self.config.max_guesses
31
+
32
+ # Color representation (1-8 for easy display)
33
+ self.colors = list(range(1, self.num_colors + 1))
34
+ self.color_names = {
35
+ 1: "Red",
36
+ 2: "Blue",
37
+ 3: "Green",
38
+ 4: "Yellow",
39
+ 5: "Orange",
40
+ 6: "Purple",
41
+ 7: "Cyan",
42
+ 8: "Magenta",
43
+ }
44
+
45
+ # Game state
46
+ self.secret_code: list[int] = []
47
+ self.guesses: list[list[int]] = []
48
+ self.feedback: list[tuple[int, int]] = [] # (black_pegs, white_pegs)
49
+
50
+ @property
51
+ def name(self) -> str:
52
+ """The display name of this puzzle type."""
53
+ return "Mastermind"
54
+
55
+ @property
56
+ def description(self) -> str:
57
+ """A one-line description of this puzzle type."""
58
+ return "Code-breaking with logical deduction and feedback"
59
+
60
+ @property
61
+ def constraint_types(self) -> list[str]:
62
+ """Constraint types demonstrated by this puzzle."""
63
+ return ["feedback", "elimination", "logical_inference", "pattern_matching", "iterative_refinement"]
64
+
65
+ @property
66
+ def business_analogies(self) -> list[str]:
67
+ """Business problems this puzzle models."""
68
+ return ["hypothesis_testing", "feedback_loops", "iterative_optimization", "parameter_tuning"]
69
+
70
+ @property
71
+ def complexity_profile(self) -> dict[str, str]:
72
+ """Complexity profile of this puzzle."""
73
+ return {"reasoning_type": "hybrid", "search_space": "exponential", "constraint_density": "sparse"}
74
+
75
+ @property
76
+ def optimal_steps(self) -> int | None:
77
+ """Optimal guesses for Mastermind with hints = 1 (hint reveals code)."""
78
+ # With hints, the code is revealed directly, so optimal is 1
79
+ return 1
80
+
81
+ @property
82
+ def difficulty_profile(self) -> "DifficultyProfile":
83
+ """Difficulty characteristics for Mastermind."""
84
+ from ...models import DifficultyLevel
85
+
86
+ logic_depth = {
87
+ DifficultyLevel.EASY.value: 2,
88
+ DifficultyLevel.MEDIUM.value: 4,
89
+ DifficultyLevel.HARD.value: 5,
90
+ }.get(self.difficulty.value, 3)
91
+ branching = self.num_colors**self.code_length # Huge search space
92
+ return DifficultyProfile(
93
+ logic_depth=logic_depth,
94
+ branching_factor=min(10.0, branching / 100), # Normalized
95
+ state_observability=0.3, # Hidden code
96
+ constraint_density=0.2,
97
+ )
98
+
99
+ async def generate_puzzle(self) -> None:
100
+ """Generate a new Mastermind puzzle."""
101
+ # Generate random secret code
102
+ self.secret_code = [self._rng.choice(self.colors) for _ in range(self.code_length)]
103
+
104
+ # Reset game state
105
+ self.guesses = []
106
+ self.feedback = []
107
+ self.moves_made = 0
108
+ self.game_started = True
109
+
110
+ def _calculate_feedback(self, guess: list[int]) -> tuple[int, int]:
111
+ """Calculate feedback for a guess.
112
+
113
+ Args:
114
+ guess: The guessed code
115
+
116
+ Returns:
117
+ Tuple of (black_pegs, white_pegs)
118
+ - black_pegs: correct color in correct position
119
+ - white_pegs: correct color in wrong position
120
+ """
121
+ black_pegs = 0
122
+ white_pegs = 0
123
+
124
+ # Create copies to track which positions have been matched
125
+ secret_remaining = list(self.secret_code)
126
+ guess_remaining = list(guess)
127
+
128
+ # First pass: count black pegs (exact matches)
129
+ for i in range(self.code_length):
130
+ if guess[i] == self.secret_code[i]:
131
+ black_pegs += 1
132
+ secret_remaining[i] = -1 # Mark as used
133
+ guess_remaining[i] = -1 # Mark as used
134
+
135
+ # Second pass: count white pegs (color matches in wrong position)
136
+ for i in range(self.code_length):
137
+ if guess_remaining[i] != -1: # Not already matched
138
+ if guess_remaining[i] in secret_remaining:
139
+ white_pegs += 1
140
+ # Remove the first occurrence from secret_remaining
141
+ idx = secret_remaining.index(guess_remaining[i])
142
+ secret_remaining[idx] = -1
143
+
144
+ return black_pegs, white_pegs
145
+
146
+ async def validate_move(self, *guess: int) -> MoveResult:
147
+ """Make a guess.
148
+
149
+ Args:
150
+ *guess: Variable number of color values (should match code_length)
151
+
152
+ Returns:
153
+ MoveResult with success status and message
154
+ """
155
+ # Check if game is over
156
+ if len(self.guesses) >= self.max_guesses:
157
+ return MoveResult(
158
+ success=False, message=f"No guesses remaining! The code was: {' '.join(map(str, self.secret_code))}"
159
+ )
160
+
161
+ # Validate guess length
162
+ if len(guess) != self.code_length:
163
+ return MoveResult(success=False, message=f"Guess must be exactly {self.code_length} colors.")
164
+
165
+ # Validate all colors are in range
166
+ for color in guess:
167
+ if color not in self.colors:
168
+ return MoveResult(success=False, message=f"Invalid color {color}. Use colors 1-{self.num_colors}.")
169
+
170
+ # Convert tuple to list
171
+ guess_list = list(guess)
172
+
173
+ # Calculate feedback
174
+ black_pegs, white_pegs = self._calculate_feedback(guess_list)
175
+
176
+ # Store guess and feedback
177
+ self.guesses.append(guess_list)
178
+ self.feedback.append((black_pegs, white_pegs))
179
+ self.moves_made += 1
180
+
181
+ # Check if won
182
+ if black_pegs == self.code_length:
183
+ return MoveResult(
184
+ success=True,
185
+ message=f"Congratulations! You cracked the code in {len(self.guesses)} guesses!",
186
+ state_changed=True,
187
+ game_over=True,
188
+ )
189
+
190
+ # Check if out of guesses
191
+ if len(self.guesses) >= self.max_guesses:
192
+ code_str = " ".join(map(str, self.secret_code))
193
+ return MoveResult(
194
+ success=True, message=f"Game over! The code was: {code_str}", state_changed=True, game_over=True
195
+ )
196
+
197
+ return MoveResult(success=True, message=f"Feedback: {black_pegs} black, {white_pegs} white", state_changed=True)
198
+
199
+ def is_complete(self) -> bool:
200
+ """Check if the puzzle is complete (code cracked)."""
201
+ if not self.feedback:
202
+ return False
203
+ black_pegs, _white_pegs = self.feedback[-1]
204
+ return black_pegs == self.code_length
205
+
206
+ async def get_hint(self) -> tuple[Any, str] | None:
207
+ """Get a hint for the next move.
208
+
209
+ For evaluation purposes, provides the complete secret code as a guess.
210
+
211
+ Returns:
212
+ Tuple of (hint_data, hint_message) or None if no hints available
213
+ """
214
+ if self.is_complete():
215
+ return None
216
+
217
+ # Provide the complete secret code as hint for evaluation
218
+ # The validate_move function expects the full code as arguments
219
+ hint_data = tuple(self.secret_code)
220
+ code_str = " ".join(str(c) for c in self.secret_code)
221
+ hint_message = f"The secret code is: {code_str}"
222
+ return hint_data, hint_message
223
+
224
+ def render_grid(self) -> str:
225
+ """Render the current game state as ASCII art.
226
+
227
+ Returns:
228
+ String representation of the game state
229
+ """
230
+ lines = []
231
+
232
+ # Header
233
+ lines.append(f"Mastermind - Crack the {self.code_length}-color code!")
234
+ lines.append(f"Colors available: 1-{self.num_colors}")
235
+ lines.append(f"Guesses remaining: {self.max_guesses - len(self.guesses)}")
236
+ lines.append("")
237
+
238
+ # Color legend
239
+ lines.append("Color Legend:")
240
+ legend_parts = []
241
+ for color in range(1, self.num_colors + 1):
242
+ legend_parts.append(f"{color}={self.color_names[color][:3]}")
243
+ lines.append(" " + ", ".join(legend_parts))
244
+ lines.append("")
245
+
246
+ # Guess history
247
+ if self.guesses:
248
+ lines.append("Guess History:")
249
+ lines.append(" # | Code | Black | White")
250
+ lines.append(" " + "-" * 38)
251
+
252
+ for i, (guess, (black, white)) in enumerate(zip(self.guesses, self.feedback, strict=True), 1):
253
+ guess_str = " ".join(str(c) for c in guess)
254
+ lines.append(f" {i:2d} | {guess_str:11s} | {black} | {white}")
255
+ else:
256
+ lines.append("No guesses yet. Make your first guess!")
257
+
258
+ return "\n".join(lines)
259
+
260
+ def get_rules(self) -> str:
261
+ """Get the rules description for Mastermind.
262
+
263
+ Returns:
264
+ Multi-line string describing the puzzle rules
265
+ """
266
+ return f"""MASTERMIND RULES:
267
+ - Guess the secret {self.code_length}-color code
268
+ - Each position uses colors 1-{self.num_colors}
269
+ - Colors can repeat in the code
270
+ - After each guess, you get feedback:
271
+ * Black peg = correct color in correct position
272
+ * White peg = correct color in wrong position
273
+ - You have {self.max_guesses} guesses to crack the code
274
+ - Use logic to eliminate possibilities!"""
275
+
276
+ def get_commands(self) -> str:
277
+ """Get the available commands for Mastermind.
278
+
279
+ Returns:
280
+ Multi-line string describing available commands
281
+ """
282
+ example = " ".join(["1"] * self.code_length)
283
+ return f"""MASTERMIND COMMANDS:
284
+ guess <c1> <c2> ... <c{self.code_length}> - Make a guess (e.g., 'guess {example}')
285
+ show - Display current game state
286
+ hint - Get a hint for the code
287
+ solve - Reveal the solution (ends game)
288
+ menu - Return to game selection
289
+ quit - Exit the server"""
290
+
291
+ def get_stats(self) -> str:
292
+ """Get current game statistics.
293
+
294
+ Returns:
295
+ String with game stats
296
+ """
297
+ return f"Guesses made: {len(self.guesses)}/{self.max_guesses} | Code length: {self.code_length} | Colors: 1-{self.num_colors} | Seed: {self.seed}"
@@ -0,0 +1,6 @@
1
+ """Minesweeper puzzle game module."""
2
+
3
+ from .config import MinesweeperConfig
4
+ from .game import MinesweeperGame
5
+
6
+ __all__ = ["MinesweeperGame", "MinesweeperConfig"]