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.
- chuk_puzzles_gym/__init__.py +19 -0
- chuk_puzzles_gym/constants.py +9 -0
- chuk_puzzles_gym/eval.py +763 -0
- chuk_puzzles_gym/export/__init__.py +20 -0
- chuk_puzzles_gym/export/dataset.py +376 -0
- chuk_puzzles_gym/games/__init__.py +94 -0
- chuk_puzzles_gym/games/_base/__init__.py +6 -0
- chuk_puzzles_gym/games/_base/commands.py +91 -0
- chuk_puzzles_gym/games/_base/game.py +337 -0
- chuk_puzzles_gym/games/binary/__init__.py +6 -0
- chuk_puzzles_gym/games/binary/config.py +23 -0
- chuk_puzzles_gym/games/binary/game.py +434 -0
- chuk_puzzles_gym/games/bridges/__init__.py +6 -0
- chuk_puzzles_gym/games/bridges/config.py +24 -0
- chuk_puzzles_gym/games/bridges/game.py +489 -0
- chuk_puzzles_gym/games/einstein/__init__.py +6 -0
- chuk_puzzles_gym/games/einstein/config.py +23 -0
- chuk_puzzles_gym/games/einstein/constants.py +13 -0
- chuk_puzzles_gym/games/einstein/game.py +366 -0
- chuk_puzzles_gym/games/einstein/models.py +35 -0
- chuk_puzzles_gym/games/fillomino/__init__.py +6 -0
- chuk_puzzles_gym/games/fillomino/config.py +24 -0
- chuk_puzzles_gym/games/fillomino/game.py +516 -0
- chuk_puzzles_gym/games/futoshiki/__init__.py +6 -0
- chuk_puzzles_gym/games/futoshiki/config.py +23 -0
- chuk_puzzles_gym/games/futoshiki/game.py +391 -0
- chuk_puzzles_gym/games/hidato/__init__.py +6 -0
- chuk_puzzles_gym/games/hidato/config.py +24 -0
- chuk_puzzles_gym/games/hidato/game.py +403 -0
- chuk_puzzles_gym/games/hitori/__init__.py +6 -0
- chuk_puzzles_gym/games/hitori/config.py +23 -0
- chuk_puzzles_gym/games/hitori/game.py +451 -0
- chuk_puzzles_gym/games/kakuro/__init__.py +6 -0
- chuk_puzzles_gym/games/kakuro/config.py +24 -0
- chuk_puzzles_gym/games/kakuro/game.py +399 -0
- chuk_puzzles_gym/games/kenken/__init__.py +6 -0
- chuk_puzzles_gym/games/kenken/config.py +24 -0
- chuk_puzzles_gym/games/kenken/enums.py +13 -0
- chuk_puzzles_gym/games/kenken/game.py +486 -0
- chuk_puzzles_gym/games/kenken/models.py +15 -0
- chuk_puzzles_gym/games/killer_sudoku/__init__.py +6 -0
- chuk_puzzles_gym/games/killer_sudoku/config.py +23 -0
- chuk_puzzles_gym/games/killer_sudoku/game.py +502 -0
- chuk_puzzles_gym/games/killer_sudoku/models.py +15 -0
- chuk_puzzles_gym/games/knapsack/__init__.py +6 -0
- chuk_puzzles_gym/games/knapsack/config.py +24 -0
- chuk_puzzles_gym/games/knapsack/enums.py +10 -0
- chuk_puzzles_gym/games/knapsack/game.py +340 -0
- chuk_puzzles_gym/games/knapsack/models.py +13 -0
- chuk_puzzles_gym/games/lights_out/__init__.py +6 -0
- chuk_puzzles_gym/games/lights_out/config.py +24 -0
- chuk_puzzles_gym/games/lights_out/game.py +249 -0
- chuk_puzzles_gym/games/logic_grid/__init__.py +6 -0
- chuk_puzzles_gym/games/logic_grid/config.py +24 -0
- chuk_puzzles_gym/games/logic_grid/constants.py +12 -0
- chuk_puzzles_gym/games/logic_grid/game.py +333 -0
- chuk_puzzles_gym/games/logic_grid/models.py +24 -0
- chuk_puzzles_gym/games/mastermind/__init__.py +6 -0
- chuk_puzzles_gym/games/mastermind/config.py +25 -0
- chuk_puzzles_gym/games/mastermind/game.py +297 -0
- chuk_puzzles_gym/games/minesweeper/__init__.py +6 -0
- chuk_puzzles_gym/games/minesweeper/config.py +24 -0
- chuk_puzzles_gym/games/minesweeper/enums.py +12 -0
- chuk_puzzles_gym/games/minesweeper/game.py +432 -0
- chuk_puzzles_gym/games/nonogram/__init__.py +6 -0
- chuk_puzzles_gym/games/nonogram/config.py +23 -0
- chuk_puzzles_gym/games/nonogram/game.py +296 -0
- chuk_puzzles_gym/games/nurikabe/__init__.py +6 -0
- chuk_puzzles_gym/games/nurikabe/config.py +24 -0
- chuk_puzzles_gym/games/nurikabe/enums.py +14 -0
- chuk_puzzles_gym/games/nurikabe/game.py +586 -0
- chuk_puzzles_gym/games/scheduler/__init__.py +6 -0
- chuk_puzzles_gym/games/scheduler/config.py +25 -0
- chuk_puzzles_gym/games/scheduler/constants.py +15 -0
- chuk_puzzles_gym/games/scheduler/enums.py +10 -0
- chuk_puzzles_gym/games/scheduler/game.py +431 -0
- chuk_puzzles_gym/games/scheduler/models.py +14 -0
- chuk_puzzles_gym/games/shikaku/__init__.py +6 -0
- chuk_puzzles_gym/games/shikaku/config.py +24 -0
- chuk_puzzles_gym/games/shikaku/game.py +419 -0
- chuk_puzzles_gym/games/slitherlink/__init__.py +6 -0
- chuk_puzzles_gym/games/slitherlink/config.py +23 -0
- chuk_puzzles_gym/games/slitherlink/game.py +386 -0
- chuk_puzzles_gym/games/sokoban/__init__.py +6 -0
- chuk_puzzles_gym/games/sokoban/config.py +24 -0
- chuk_puzzles_gym/games/sokoban/game.py +671 -0
- chuk_puzzles_gym/games/star_battle/__init__.py +6 -0
- chuk_puzzles_gym/games/star_battle/config.py +24 -0
- chuk_puzzles_gym/games/star_battle/game.py +390 -0
- chuk_puzzles_gym/games/sudoku/__init__.py +7 -0
- chuk_puzzles_gym/games/sudoku/commands.py +96 -0
- chuk_puzzles_gym/games/sudoku/config.py +22 -0
- chuk_puzzles_gym/games/sudoku/game.py +328 -0
- chuk_puzzles_gym/games/tents/__init__.py +6 -0
- chuk_puzzles_gym/games/tents/config.py +24 -0
- chuk_puzzles_gym/games/tents/game.py +416 -0
- chuk_puzzles_gym/gym_env.py +465 -0
- chuk_puzzles_gym/models/__init__.py +47 -0
- chuk_puzzles_gym/models/base.py +30 -0
- chuk_puzzles_gym/models/config.py +11 -0
- chuk_puzzles_gym/models/enums.py +104 -0
- chuk_puzzles_gym/models/evaluation.py +487 -0
- chuk_puzzles_gym/models/games.py +12 -0
- chuk_puzzles_gym/server.py +1171 -0
- chuk_puzzles_gym/trace/__init__.py +10 -0
- chuk_puzzles_gym/trace/generator.py +726 -0
- chuk_puzzles_gym/utils/__init__.py +4 -0
- chuk_puzzles_gym-0.9.dist-info/METADATA +1471 -0
- chuk_puzzles_gym-0.9.dist-info/RECORD +112 -0
- chuk_puzzles_gym-0.9.dist-info/WHEEL +5 -0
- chuk_puzzles_gym-0.9.dist-info/entry_points.txt +4 -0
- chuk_puzzles_gym-0.9.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Einstein's Puzzle (Zebra Puzzle) game implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ...models import DifficultyLevel, DifficultyProfile, MoveResult
|
|
6
|
+
from .._base import PuzzleGame
|
|
7
|
+
from .constants import ATTRIBUTES, COLORS, DRINKS, NATIONALITIES, PETS, SMOKES
|
|
8
|
+
from .models import HouseAssignment
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EinsteinGame(PuzzleGame):
|
|
12
|
+
"""Einstein's Puzzle (also known as Zebra Puzzle).
|
|
13
|
+
|
|
14
|
+
A classic logic puzzle with 5 houses and 5 attributes each.
|
|
15
|
+
Uses complex deduction with multiple constraint types.
|
|
16
|
+
Perfect for testing AI reasoning capabilities.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
|
|
20
|
+
"""Initialize a new Einstein's Puzzle game.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
difficulty: Game difficulty level (easy/medium/hard)
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(difficulty, seed, **kwargs)
|
|
26
|
+
|
|
27
|
+
# 5 houses with 5 attributes each
|
|
28
|
+
self.num_houses = 5
|
|
29
|
+
|
|
30
|
+
# Attributes - using constants for type safety
|
|
31
|
+
self.colors = COLORS
|
|
32
|
+
self.nationalities = NATIONALITIES
|
|
33
|
+
self.drinks = DRINKS
|
|
34
|
+
self.smokes = SMOKES
|
|
35
|
+
self.pets = PETS
|
|
36
|
+
|
|
37
|
+
# Player's assignments: Change from list[dict] to list[HouseAssignment]
|
|
38
|
+
self.assignments: list[HouseAssignment] = [HouseAssignment() for _ in range(self.num_houses)]
|
|
39
|
+
|
|
40
|
+
# Solution
|
|
41
|
+
self.solution: list[HouseAssignment] = []
|
|
42
|
+
|
|
43
|
+
# Clues
|
|
44
|
+
self.clues: list[str] = []
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def name(self) -> str:
|
|
48
|
+
"""The display name of this puzzle type."""
|
|
49
|
+
return "Einstein's Puzzle"
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def description(self) -> str:
|
|
53
|
+
"""A one-line description of this puzzle type."""
|
|
54
|
+
return "Classic logic deduction - who owns the fish?"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def constraint_types(self) -> list[str]:
|
|
58
|
+
"""Constraint types demonstrated by this puzzle."""
|
|
59
|
+
return ["multi_attribute", "relational", "positional", "logical_implication", "transitive_closure"]
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def business_analogies(self) -> list[str]:
|
|
63
|
+
"""Business problems this puzzle models."""
|
|
64
|
+
return ["multi_factor_matching", "relationship_mapping", "eligibility_rules", "complex_deduction"]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def complexity_profile(self) -> dict[str, str]:
|
|
68
|
+
"""Complexity profile of this puzzle."""
|
|
69
|
+
return {"reasoning_type": "deductive", "search_space": "large", "constraint_density": "dense"}
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def optimal_steps(self) -> int | None:
|
|
73
|
+
"""Minimum steps = attribute assignments (houses x attributes)."""
|
|
74
|
+
# 5 attributes: color, nationality, drink, smoke, pet
|
|
75
|
+
return self.num_houses * 5
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def difficulty_profile(self) -> "DifficultyProfile":
|
|
79
|
+
"""Difficulty characteristics for Einstein's Puzzle."""
|
|
80
|
+
|
|
81
|
+
logic_depth = {
|
|
82
|
+
DifficultyLevel.EASY.value: 5,
|
|
83
|
+
DifficultyLevel.MEDIUM.value: 7,
|
|
84
|
+
DifficultyLevel.HARD.value: 9,
|
|
85
|
+
}.get(self.difficulty.value, 6)
|
|
86
|
+
return DifficultyProfile(
|
|
87
|
+
logic_depth=logic_depth,
|
|
88
|
+
branching_factor=5.0, # 5 houses to match
|
|
89
|
+
state_observability=1.0,
|
|
90
|
+
constraint_density=0.7,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
async def generate_puzzle(self) -> None:
|
|
94
|
+
"""Generate a new Einstein's Puzzle."""
|
|
95
|
+
# Generate a random valid solution by shuffling attribute lists
|
|
96
|
+
shuffled_colors = self.colors.copy()
|
|
97
|
+
shuffled_nationalities = self.nationalities.copy()
|
|
98
|
+
shuffled_drinks = self.drinks.copy()
|
|
99
|
+
shuffled_smokes = self.smokes.copy()
|
|
100
|
+
shuffled_pets = self.pets.copy()
|
|
101
|
+
|
|
102
|
+
self._rng.shuffle(shuffled_colors)
|
|
103
|
+
self._rng.shuffle(shuffled_nationalities)
|
|
104
|
+
self._rng.shuffle(shuffled_drinks)
|
|
105
|
+
self._rng.shuffle(shuffled_smokes)
|
|
106
|
+
self._rng.shuffle(shuffled_pets)
|
|
107
|
+
|
|
108
|
+
# Create solution
|
|
109
|
+
self.solution = []
|
|
110
|
+
for i in range(self.num_houses):
|
|
111
|
+
self.solution.append(
|
|
112
|
+
HouseAssignment(
|
|
113
|
+
color=shuffled_colors[i],
|
|
114
|
+
nationality=shuffled_nationalities[i],
|
|
115
|
+
drink=shuffled_drinks[i],
|
|
116
|
+
smoke=shuffled_smokes[i],
|
|
117
|
+
pet=shuffled_pets[i],
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Generate clues based on solution
|
|
122
|
+
self.clues = self._generate_clues()
|
|
123
|
+
|
|
124
|
+
# Initialize player grid
|
|
125
|
+
self.assignments = [HouseAssignment() for _ in range(self.num_houses)]
|
|
126
|
+
|
|
127
|
+
self.moves_made = 0
|
|
128
|
+
self.game_started = True
|
|
129
|
+
|
|
130
|
+
def _generate_clues(self) -> list[str]:
|
|
131
|
+
"""Generate clues based on the solution."""
|
|
132
|
+
clues = []
|
|
133
|
+
|
|
134
|
+
# Find positions of each attribute
|
|
135
|
+
def find_house(attr_type: str, value: str) -> int:
|
|
136
|
+
for i, house in enumerate(self.solution):
|
|
137
|
+
if house.get_attribute(attr_type) == value:
|
|
138
|
+
return i
|
|
139
|
+
return -1
|
|
140
|
+
|
|
141
|
+
# Always include these starter clues
|
|
142
|
+
norwegian_house = find_house("nationality", "Norwegian")
|
|
143
|
+
milk_house = find_house("drink", "Milk")
|
|
144
|
+
|
|
145
|
+
clues.append(f"1. The Norwegian lives in house {norwegian_house + 1}")
|
|
146
|
+
clues.append(f"2. Milk is drunk in house {milk_house + 1}")
|
|
147
|
+
|
|
148
|
+
# Add attribute-to-attribute clues
|
|
149
|
+
clue_num = 3
|
|
150
|
+
|
|
151
|
+
# Same house clues
|
|
152
|
+
for i in range(self.num_houses):
|
|
153
|
+
house = self.solution[i]
|
|
154
|
+
|
|
155
|
+
# Color-Nationality
|
|
156
|
+
if self._rng.random() < 0.4:
|
|
157
|
+
clues.append(f"{clue_num}. The {house.nationality} lives in the {house.color} house")
|
|
158
|
+
clue_num += 1
|
|
159
|
+
|
|
160
|
+
# Nationality-Drink
|
|
161
|
+
if self._rng.random() < 0.4:
|
|
162
|
+
clues.append(f"{clue_num}. The {house.nationality} drinks {house.drink}")
|
|
163
|
+
clue_num += 1
|
|
164
|
+
|
|
165
|
+
# Smoke-Pet (skip if smoke name has spaces to avoid test parsing issues)
|
|
166
|
+
if self._rng.random() < 0.4 and " " not in house.smoke:
|
|
167
|
+
pet = house.pet
|
|
168
|
+
clues.append(f"{clue_num}. The person who smokes {house.smoke} owns a {pet}")
|
|
169
|
+
clue_num += 1
|
|
170
|
+
|
|
171
|
+
# Neighbor clues
|
|
172
|
+
for i in range(self.num_houses - 1):
|
|
173
|
+
house1 = self.solution[i]
|
|
174
|
+
house2 = self.solution[i + 1]
|
|
175
|
+
|
|
176
|
+
if self._rng.random() < 0.3:
|
|
177
|
+
clues.append(f"{clue_num}. The {house1.color} house is next to the {house2.color} house")
|
|
178
|
+
clue_num += 1
|
|
179
|
+
|
|
180
|
+
# Limit number of clues
|
|
181
|
+
max_clues_map = {
|
|
182
|
+
DifficultyLevel.EASY: 12,
|
|
183
|
+
DifficultyLevel.MEDIUM: 10,
|
|
184
|
+
DifficultyLevel.HARD: 8,
|
|
185
|
+
}
|
|
186
|
+
max_clues = max_clues_map[self.difficulty]
|
|
187
|
+
return clues[:max_clues]
|
|
188
|
+
|
|
189
|
+
async def validate_move(self, house: int, attribute: str, value: str) -> MoveResult:
|
|
190
|
+
"""Assign an attribute to a house.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
house: House number (1-indexed, user-facing)
|
|
194
|
+
attribute: Attribute type (color, nationality, drink, smoke, pet)
|
|
195
|
+
value: Attribute value
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
MoveResult indicating success/failure and message
|
|
199
|
+
"""
|
|
200
|
+
# Convert to 0-indexed
|
|
201
|
+
house -= 1
|
|
202
|
+
|
|
203
|
+
# Validate house number
|
|
204
|
+
if not (0 <= house < self.num_houses):
|
|
205
|
+
return MoveResult(success=False, message=f"Invalid house number. Use 1-{self.num_houses}.")
|
|
206
|
+
|
|
207
|
+
# Normalize attribute and value
|
|
208
|
+
attribute = attribute.lower()
|
|
209
|
+
value = value.title() # Use title() to handle multi-word values like "Pall Mall"
|
|
210
|
+
|
|
211
|
+
# Handle space-to-hyphen conversion for backward compatibility
|
|
212
|
+
value_normalized = value.replace(" ", "-")
|
|
213
|
+
|
|
214
|
+
# Validate attribute type and get valid values
|
|
215
|
+
valid_attribute_names = ["color", "nationality", "drink", "smoke", "pet"]
|
|
216
|
+
if attribute not in valid_attribute_names:
|
|
217
|
+
return MoveResult(success=False, message=f"Invalid attribute. Use: {', '.join(valid_attribute_names)}")
|
|
218
|
+
|
|
219
|
+
# Get the valid values list for this attribute
|
|
220
|
+
if attribute == "color":
|
|
221
|
+
valid_values = self.colors
|
|
222
|
+
elif attribute == "nationality":
|
|
223
|
+
valid_values = self.nationalities
|
|
224
|
+
elif attribute == "drink":
|
|
225
|
+
valid_values = self.drinks
|
|
226
|
+
elif attribute == "smoke":
|
|
227
|
+
valid_values = self.smokes
|
|
228
|
+
elif attribute == "pet":
|
|
229
|
+
valid_values = self.pets
|
|
230
|
+
else:
|
|
231
|
+
# Should never reach here due to earlier check
|
|
232
|
+
return MoveResult(success=False, message=f"Invalid attribute: {attribute}")
|
|
233
|
+
|
|
234
|
+
# Validate value (try both original and normalized)
|
|
235
|
+
if value in valid_values:
|
|
236
|
+
pass # Value is valid as-is
|
|
237
|
+
elif value_normalized in valid_values:
|
|
238
|
+
value = value_normalized # Use normalized version
|
|
239
|
+
else:
|
|
240
|
+
return MoveResult(success=False, message=f"Invalid {attribute}. Choose from: {', '.join(valid_values)}")
|
|
241
|
+
|
|
242
|
+
# Check if value is already assigned to another house
|
|
243
|
+
for i, other_house in enumerate(self.assignments):
|
|
244
|
+
if i != house and other_house.get_attribute(attribute) == value:
|
|
245
|
+
return MoveResult(success=False, message=f"{value} is already assigned to house {i + 1}")
|
|
246
|
+
|
|
247
|
+
# Check if this house already has a value for this attribute
|
|
248
|
+
if self.assignments[house].get_attribute(attribute) is not None:
|
|
249
|
+
old_value = self.assignments[house].get_attribute(attribute)
|
|
250
|
+
self.assignments[house].set_attribute(attribute, value)
|
|
251
|
+
self.moves_made += 1
|
|
252
|
+
return MoveResult(
|
|
253
|
+
success=True,
|
|
254
|
+
message=f"Changed house {house + 1}'s {attribute} from {old_value} to {value}",
|
|
255
|
+
state_changed=True,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Assign the value
|
|
259
|
+
self.assignments[house].set_attribute(attribute, value)
|
|
260
|
+
self.moves_made += 1
|
|
261
|
+
return MoveResult(success=True, message=f"Assigned {value} to house {house + 1}", state_changed=True)
|
|
262
|
+
|
|
263
|
+
def is_complete(self) -> bool:
|
|
264
|
+
"""Check if the puzzle is completely and correctly solved."""
|
|
265
|
+
# All houses must have all attributes assigned
|
|
266
|
+
for house in self.assignments:
|
|
267
|
+
if not house.is_complete():
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
# Check if assignments match solution
|
|
271
|
+
for i in range(self.num_houses):
|
|
272
|
+
for attr in ATTRIBUTES:
|
|
273
|
+
if self.assignments[i].get_attribute(attr) != self.solution[i].get_attribute(attr):
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
return True
|
|
277
|
+
|
|
278
|
+
async def get_hint(self) -> tuple[Any, str] | None:
|
|
279
|
+
"""Get a hint for the next move.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Tuple of (hint_data, hint_message) or None
|
|
283
|
+
"""
|
|
284
|
+
# Find first unassigned attribute in solution
|
|
285
|
+
for i in range(self.num_houses):
|
|
286
|
+
for attr in ATTRIBUTES:
|
|
287
|
+
if self.assignments[i].get_attribute(attr) != self.solution[i].get_attribute(attr):
|
|
288
|
+
value = self.solution[i].get_attribute(attr)
|
|
289
|
+
hint_data = (i + 1, attr, value)
|
|
290
|
+
hint_message = f"Try assigning {value} to house {i + 1} as its {attr}"
|
|
291
|
+
return hint_data, hint_message
|
|
292
|
+
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
def render_grid(self) -> str:
|
|
296
|
+
"""Render the current puzzle state as ASCII art.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
String representation of the puzzle
|
|
300
|
+
"""
|
|
301
|
+
lines = []
|
|
302
|
+
|
|
303
|
+
lines.append("Einstein's Puzzle - Who owns the fish?")
|
|
304
|
+
lines.append("")
|
|
305
|
+
|
|
306
|
+
# Houses table
|
|
307
|
+
lines.append("House | Color | Nationality | Drink | Smoke | Pet")
|
|
308
|
+
lines.append("------+---------+-------------+--------+-------------+--------")
|
|
309
|
+
|
|
310
|
+
for i in range(self.num_houses):
|
|
311
|
+
house = self.assignments[i]
|
|
312
|
+
color = house.color or "?"
|
|
313
|
+
nationality = house.nationality or "?"
|
|
314
|
+
drink = house.drink or "?"
|
|
315
|
+
smoke = house.smoke or "?"
|
|
316
|
+
pet = house.pet or "?"
|
|
317
|
+
|
|
318
|
+
lines.append(f" {i + 1} | {color:<7s} | {nationality:<11s} | {drink:<6s} | {smoke:<11s} | {pet:<6s}")
|
|
319
|
+
|
|
320
|
+
lines.append("")
|
|
321
|
+
lines.append("Clues:")
|
|
322
|
+
for clue in self.clues:
|
|
323
|
+
lines.append(f" {clue}")
|
|
324
|
+
|
|
325
|
+
return "\n".join(lines)
|
|
326
|
+
|
|
327
|
+
def get_rules(self) -> str:
|
|
328
|
+
"""Get the rules description for Einstein's Puzzle.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Multi-line string describing the puzzle rules
|
|
332
|
+
"""
|
|
333
|
+
return """EINSTEIN'S PUZZLE RULES:
|
|
334
|
+
- There are 5 houses in a row
|
|
335
|
+
- Each house has a unique color, nationality, drink, smoke, and pet
|
|
336
|
+
- Use the clues to deduce which attribute belongs in which house
|
|
337
|
+
- No attribute can appear in more than one house
|
|
338
|
+
- All houses must have all 5 attributes assigned
|
|
339
|
+
- Question: WHO OWNS THE FISH?"""
|
|
340
|
+
|
|
341
|
+
def get_commands(self) -> str:
|
|
342
|
+
"""Get the available commands for Einstein's Puzzle.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Multi-line string describing available commands
|
|
346
|
+
"""
|
|
347
|
+
return """EINSTEIN'S PUZZLE COMMANDS:
|
|
348
|
+
assign <house> <attr> <value> - Assign attribute (e.g., 'assign 1 color red')
|
|
349
|
+
Attributes: color, nationality, drink, smoke, pet
|
|
350
|
+
show - Display current assignments
|
|
351
|
+
hint - Get a hint
|
|
352
|
+
check - Check if solution is correct
|
|
353
|
+
solve - Show the solution (ends game)
|
|
354
|
+
menu - Return to game selection
|
|
355
|
+
quit - Exit the server"""
|
|
356
|
+
|
|
357
|
+
def get_stats(self) -> str:
|
|
358
|
+
"""Get current game statistics.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
String with game stats
|
|
362
|
+
"""
|
|
363
|
+
assigned = sum(1 for house in self.assignments for attr in ATTRIBUTES if house.get_attribute(attr) is not None)
|
|
364
|
+
total = self.num_houses * 5
|
|
365
|
+
|
|
366
|
+
return f"Moves: {self.moves_made} | Assigned: {assigned}/{total} | Clues: {len(self.clues)} | Seed: {self.seed}"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Einstein's Puzzle game models."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HouseAssignment(BaseModel):
|
|
7
|
+
"""Attribute assignments for a house in Einstein's Puzzle."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(frozen=False) # Allow mutation during gameplay
|
|
10
|
+
|
|
11
|
+
color: str | None = None
|
|
12
|
+
nationality: str | None = None
|
|
13
|
+
drink: str | None = None
|
|
14
|
+
smoke: str | None = None
|
|
15
|
+
pet: str | None = None
|
|
16
|
+
|
|
17
|
+
def is_complete(self) -> bool:
|
|
18
|
+
"""Check if all attributes are assigned."""
|
|
19
|
+
return all(
|
|
20
|
+
[
|
|
21
|
+
self.color is not None,
|
|
22
|
+
self.nationality is not None,
|
|
23
|
+
self.drink is not None,
|
|
24
|
+
self.smoke is not None,
|
|
25
|
+
self.pet is not None,
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def get_attribute(self, attr_type: str) -> str | None:
|
|
30
|
+
"""Get attribute value by type."""
|
|
31
|
+
return getattr(self, attr_type.lower(), None)
|
|
32
|
+
|
|
33
|
+
def set_attribute(self, attr_type: str, value: str) -> None:
|
|
34
|
+
"""Set attribute value by type."""
|
|
35
|
+
setattr(self, attr_type.lower(), value)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Configuration for Fillomino game."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from ...models.enums import DifficultyLevel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FillominoConfig(BaseModel):
|
|
9
|
+
"""Configuration for Fillomino game."""
|
|
10
|
+
|
|
11
|
+
difficulty: DifficultyLevel = Field(default=DifficultyLevel.EASY, description="Game difficulty level")
|
|
12
|
+
size: int = Field(ge=6, le=10, description="Grid size (NxN)")
|
|
13
|
+
num_clues: int = Field(ge=4, description="Number of clue numbers to reveal")
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_difficulty(cls, difficulty: DifficultyLevel) -> "FillominoConfig":
|
|
17
|
+
"""Create config from difficulty level."""
|
|
18
|
+
config_map = {
|
|
19
|
+
DifficultyLevel.EASY: {"size": 6, "num_clues": 8},
|
|
20
|
+
DifficultyLevel.MEDIUM: {"size": 8, "num_clues": 10},
|
|
21
|
+
DifficultyLevel.HARD: {"size": 10, "num_clues": 12},
|
|
22
|
+
}
|
|
23
|
+
params = config_map[difficulty]
|
|
24
|
+
return cls(difficulty=difficulty, **params)
|