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,431 @@
1
+ """Task Scheduler optimization 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 SchedulerConfig
8
+ from .constants import TASK_NAMES
9
+ from .enums import SchedulerAction
10
+ from .models import Task
11
+
12
+
13
+ class SchedulerGame(PuzzleGame):
14
+ """Task Scheduler optimization puzzle game.
15
+
16
+ Schedule tasks with dependencies and resource constraints
17
+ to minimize total completion time (makespan).
18
+ Demonstrates temporal reasoning and optimization.
19
+ """
20
+
21
+ def __init__(self, difficulty: str = "easy", seed: int | None = None, **kwargs):
22
+ """Initialize a new Scheduler game.
23
+
24
+ Args:
25
+ difficulty: Game difficulty level (easy/medium/hard)
26
+ """
27
+ super().__init__(difficulty, seed, **kwargs)
28
+
29
+ # Configuration using Pydantic model
30
+ self.config = SchedulerConfig.from_difficulty(self.difficulty)
31
+ self.num_tasks: int = self.config.num_tasks
32
+ self.num_workers: int = self.config.num_workers
33
+
34
+ # Task properties - using Task model
35
+ self.tasks: list[Task] = []
36
+ self.dependencies: list[tuple[int, int]] = [] # (task_a, task_b) means A must finish before B starts
37
+
38
+ # Player's schedule: task_id -> (worker_id, start_time)
39
+ self.schedule: dict[int, tuple[int, int]] = {}
40
+
41
+ # Optimal solution
42
+ self.optimal_makespan = 0
43
+ self.optimal_schedule: dict[int, tuple[int, int]] = {}
44
+
45
+ @property
46
+ def name(self) -> str:
47
+ """The display name of this puzzle type."""
48
+ return "Task Scheduler"
49
+
50
+ @property
51
+ def description(self) -> str:
52
+ """A one-line description of this puzzle type."""
53
+ return "Schedule tasks with dependencies to minimize completion time"
54
+
55
+ @property
56
+ def constraint_types(self) -> list[str]:
57
+ """Constraint types demonstrated by this puzzle."""
58
+ return ["optimization", "precedence", "resource_allocation", "makespan_minimization"]
59
+
60
+ @property
61
+ def business_analogies(self) -> list[str]:
62
+ """Business problems this puzzle models."""
63
+ return ["project_scheduling", "sprint_planning", "team_allocation", "workflow_optimization"]
64
+
65
+ @property
66
+ def complexity_profile(self) -> dict[str, str]:
67
+ """Complexity profile of this puzzle."""
68
+ return {"reasoning_type": "optimization", "search_space": "exponential", "constraint_density": "moderate"}
69
+
70
+ @property
71
+ def optimal_steps(self) -> int | None:
72
+ """Minimum steps = tasks to schedule."""
73
+ return len(self.tasks) if hasattr(self, "tasks") else None
74
+
75
+ @property
76
+ def difficulty_profile(self) -> "DifficultyProfile":
77
+ """Difficulty characteristics for Task Scheduler."""
78
+ from ...models import DifficultyLevel
79
+
80
+ logic_depth = {
81
+ DifficultyLevel.EASY.value: 2,
82
+ DifficultyLevel.MEDIUM.value: 4,
83
+ DifficultyLevel.HARD.value: 5,
84
+ }.get(self.difficulty.value, 3)
85
+ n_workers = self.num_workers if hasattr(self, "num_workers") else 3
86
+ return DifficultyProfile(
87
+ logic_depth=logic_depth,
88
+ branching_factor=float(n_workers),
89
+ state_observability=1.0,
90
+ constraint_density=0.5,
91
+ )
92
+
93
+ async def generate_puzzle(self) -> None:
94
+ """Generate a new Scheduler puzzle."""
95
+ # Generate tasks with random durations using constants
96
+ self.tasks = []
97
+ for i in range(self.num_tasks):
98
+ name = TASK_NAMES[i] if i < len(TASK_NAMES) else f"Task {chr(65 + i)}"
99
+ duration = self._rng.randint(2, 8)
100
+ dependencies: list[int] = []
101
+ task = Task(id=i, name=name, duration=duration, dependencies=dependencies)
102
+ self.tasks.append(task)
103
+
104
+ # Generate dependencies (DAG - no cycles)
105
+ self.dependencies = []
106
+ for i in range(self.num_tasks):
107
+ for j in range(i + 1, self.num_tasks):
108
+ if self._rng.random() < self.config.dependency_prob:
109
+ # Task i must complete before task j can start
110
+ self.dependencies.append((i, j))
111
+
112
+ # Calculate optimal solution
113
+ self._solve_optimal()
114
+
115
+ # Initialize empty schedule
116
+ self.schedule = {}
117
+ self.moves_made = 0
118
+ self.game_started = True
119
+
120
+ def _solve_optimal(self) -> None:
121
+ """Solve the scheduling problem optimally using greedy approach with topological sort."""
122
+ # Build dependency graph
123
+ in_degree = [0] * self.num_tasks
124
+ adj_list: list[list[int]] = [[] for _ in range(self.num_tasks)]
125
+
126
+ for task_a, task_b in self.dependencies:
127
+ adj_list[task_a].append(task_b)
128
+ in_degree[task_b] += 1
129
+
130
+ # Topological sort with earliest start times
131
+ earliest_start = [0] * self.num_tasks
132
+ queue = [i for i in range(self.num_tasks) if in_degree[i] == 0]
133
+
134
+ # Calculate earliest start times
135
+ while queue:
136
+ task = queue.pop(0)
137
+ for dependent in adj_list[task]:
138
+ # Dependent can't start until current task finishes
139
+ earliest_start[dependent] = max(
140
+ earliest_start[dependent], earliest_start[task] + self.tasks[task].duration
141
+ )
142
+ in_degree[dependent] -= 1
143
+ if in_degree[dependent] == 0:
144
+ queue.append(dependent)
145
+
146
+ # Schedule tasks greedily with proper dependency handling
147
+ worker_available = [0] * self.num_workers
148
+ self.optimal_schedule = {}
149
+
150
+ # Sort tasks by earliest start time
151
+ sorted_tasks = sorted(range(self.num_tasks), key=lambda t: earliest_start[t])
152
+
153
+ for task_id in sorted_tasks:
154
+ # Calculate actual earliest start based on scheduled dependencies
155
+ actual_earliest_start = 0
156
+ for dep_task, dependent in self.dependencies:
157
+ if dependent == task_id and dep_task in self.optimal_schedule:
158
+ dep_worker, dep_start = self.optimal_schedule[dep_task]
159
+ dep_end = dep_start + self.tasks[dep_task].duration
160
+ actual_earliest_start = max(actual_earliest_start, dep_end)
161
+
162
+ # Find the worker that can start this task earliest
163
+ # considering both worker availability and actual dependency finish times
164
+ best_worker = 0
165
+ best_start_time = max(worker_available[0], actual_earliest_start)
166
+
167
+ for worker_id in range(1, self.num_workers):
168
+ # This task can't start before its dependencies finish
169
+ # and can't start before the worker is available
170
+ candidate_start = max(worker_available[worker_id], actual_earliest_start)
171
+
172
+ if candidate_start < best_start_time:
173
+ best_start_time = candidate_start
174
+ best_worker = worker_id
175
+
176
+ # Store 1-indexed worker_id for consistency
177
+ self.optimal_schedule[task_id] = (best_worker + 1, best_start_time)
178
+ worker_available[best_worker] = best_start_time + self.tasks[task_id].duration
179
+
180
+ # Calculate makespan
181
+ self.optimal_makespan = max(worker_available) if worker_available else 0
182
+
183
+ async def validate_move(self, task_id: int | str, worker_id: int, start_time: int) -> MoveResult:
184
+ """Assign a task to a worker at a specific start time, or unassign a task.
185
+
186
+ Args:
187
+ task_id: Task number (1-indexed) or "unassign" action
188
+ worker_id: Worker number (1-indexed) when assigning, or task number when unassigning
189
+ start_time: Start time (0 or positive integer) when assigning, unused when unassigning
190
+
191
+ Returns:
192
+ MoveResult with success status and message
193
+ """
194
+ # Handle unassign action
195
+ if isinstance(task_id, str) and task_id.lower() == SchedulerAction.UNASSIGN.value:
196
+ return await self._unassign_task(worker_id)
197
+
198
+ # Convert to 0-indexed
199
+ task_id = int(task_id) - 1
200
+ worker_id -= 1
201
+
202
+ # Validate inputs
203
+ if not (0 <= task_id < self.num_tasks):
204
+ return MoveResult(success=False, message=f"Invalid task number. Use 1-{self.num_tasks}.")
205
+
206
+ if not (0 <= worker_id < self.num_workers):
207
+ return MoveResult(success=False, message=f"Invalid worker number. Use 1-{self.num_workers}.")
208
+
209
+ if start_time < 0:
210
+ return MoveResult(success=False, message="Start time must be non-negative.")
211
+
212
+ # Check if task is already scheduled - allow reassignment
213
+ is_reassignment = task_id in self.schedule
214
+
215
+ # Check dependencies
216
+ for dep_task, dependent in self.dependencies:
217
+ if dependent == task_id:
218
+ # This task depends on dep_task
219
+ if dep_task not in self.schedule:
220
+ return MoveResult(
221
+ success=False,
222
+ message=f"Cannot schedule - {self.tasks[dep_task].name} must be scheduled first.",
223
+ )
224
+
225
+ dep_worker, dep_start = self.schedule[dep_task]
226
+ dep_end = dep_start + self.tasks[dep_task].duration
227
+
228
+ if start_time < dep_end:
229
+ return MoveResult(
230
+ success=False,
231
+ message=f"Cannot start at {start_time} - {self.tasks[dep_task].name} finishes at {dep_end}.",
232
+ )
233
+
234
+ # Check worker availability (no overlap)
235
+ task_duration = self.tasks[task_id].duration
236
+ task_end = start_time + task_duration
237
+
238
+ for other_task_id, (other_worker, other_start) in self.schedule.items():
239
+ if other_worker == worker_id + 1: # other_worker is 1-indexed in schedule
240
+ other_end = other_start + self.tasks[other_task_id].duration
241
+ # Check for overlap
242
+ if not (task_end <= other_start or start_time >= other_end):
243
+ return MoveResult(
244
+ success=False,
245
+ message=f"Worker {worker_id + 1} is busy with {self.tasks[other_task_id].name} from {other_start} to {other_end}.",
246
+ )
247
+
248
+ # Schedule the task (store 1-indexed worker_id for consistency with user input)
249
+ self.schedule[task_id] = (worker_id + 1, start_time)
250
+ self.moves_made += 1
251
+
252
+ task_name = self.tasks[task_id].name
253
+ if is_reassignment:
254
+ return MoveResult(
255
+ success=True,
256
+ message=f"Reassigned {task_name} to Worker {worker_id + 1} at time {start_time}",
257
+ state_changed=True,
258
+ )
259
+ else:
260
+ return MoveResult(
261
+ success=True,
262
+ message=f"Scheduled {task_name} on Worker {worker_id + 1} at time {start_time}",
263
+ state_changed=True,
264
+ )
265
+
266
+ async def _unassign_task(self, task_id: int) -> MoveResult:
267
+ """Unassign a task from the schedule.
268
+
269
+ Args:
270
+ task_id: Task number (1-indexed)
271
+
272
+ Returns:
273
+ MoveResult with success status and message
274
+ """
275
+ task_id -= 1
276
+
277
+ if task_id not in self.schedule:
278
+ return MoveResult(success=False, message=f"{self.tasks[task_id].name} is not currently assigned.")
279
+
280
+ # Check if any scheduled task depends on this one
281
+ for dep_task, dependent in self.dependencies:
282
+ if dep_task == task_id and dependent in self.schedule:
283
+ return MoveResult(
284
+ success=False,
285
+ message=f"Cannot unassign - {self.tasks[dependent].name} depends on {self.tasks[task_id].name}.",
286
+ )
287
+
288
+ del self.schedule[task_id]
289
+ self.moves_made += 1
290
+ return MoveResult(success=True, message=f"Unassigned {self.tasks[task_id].name}", state_changed=True)
291
+
292
+ def _get_makespan(self) -> int:
293
+ """Calculate the makespan (total completion time) of current schedule."""
294
+ if not self.schedule:
295
+ return 0
296
+
297
+ max_end = 0
298
+ for task_id, (_worker, start_time) in self.schedule.items():
299
+ end_time = start_time + self.tasks[task_id].duration
300
+ max_end = max(max_end, end_time)
301
+
302
+ return max_end
303
+
304
+ def is_complete(self) -> bool:
305
+ """Check if all tasks are scheduled optimally."""
306
+ # All tasks must be scheduled
307
+ if len(self.schedule) != self.num_tasks:
308
+ return False
309
+
310
+ # Check if makespan is optimal
311
+ return self._get_makespan() == self.optimal_makespan
312
+
313
+ async def get_hint(self) -> tuple[Any, str] | None:
314
+ """Get a hint for the next move.
315
+
316
+ Returns:
317
+ Tuple of (hint_data, hint_message) or None
318
+ """
319
+ # Find an unscheduled task that's in the optimal solution
320
+ for task_id in range(self.num_tasks):
321
+ if task_id not in self.schedule and task_id in self.optimal_schedule:
322
+ worker, start_time = self.optimal_schedule[task_id]
323
+ # worker is already 1-indexed in optimal_schedule
324
+ hint_data = (task_id + 1, worker, start_time)
325
+ hint_message = f"Try scheduling {self.tasks[task_id].name} on Worker {worker} at time {start_time}"
326
+ return hint_data, hint_message
327
+
328
+ return None
329
+
330
+ def render_grid(self) -> str:
331
+ """Render the current schedule as ASCII art.
332
+
333
+ Returns:
334
+ String representation of the schedule
335
+ """
336
+ lines = []
337
+
338
+ lines.append("Task Scheduler")
339
+ lines.append(f"Workers: {self.num_workers} | Tasks: {self.num_tasks}")
340
+ lines.append(f"Current Makespan: {self._get_makespan()}")
341
+ lines.append(f"Optimal Makespan: {self.optimal_makespan}")
342
+ lines.append("")
343
+
344
+ # Tasks table
345
+ lines.append("Tasks:")
346
+ lines.append(" # | Name | Duration | Status")
347
+ lines.append(" --+--------+----------+----------------------------------")
348
+
349
+ for task in self.tasks:
350
+ task_id = task.id
351
+ status = "Not scheduled"
352
+
353
+ if task_id in self.schedule:
354
+ worker, start_time = self.schedule[task_id]
355
+ end_time = start_time + task.duration
356
+ status = f"Worker {worker}, time {start_time}-{end_time}"
357
+
358
+ lines.append(f" {task_id + 1:2d} | {task.name:<6s} | {task.duration:4d}hrs | {status}")
359
+
360
+ # Dependencies
361
+ if self.dependencies:
362
+ lines.append("")
363
+ lines.append("Dependencies:")
364
+ for task_a, task_b in self.dependencies:
365
+ lines.append(f" {self.tasks[task_a].name} → {self.tasks[task_b].name}")
366
+
367
+ # Timeline visualization
368
+ if self.schedule:
369
+ lines.append("")
370
+ lines.append("Timeline:")
371
+ makespan = self._get_makespan()
372
+
373
+ for worker_id in range(self.num_workers):
374
+ timeline = ["."] * (makespan + 1)
375
+
376
+ for task_id, (w, start) in self.schedule.items():
377
+ if w == worker_id + 1:
378
+ duration = self.tasks[task_id].duration
379
+ task_letter = chr(65 + task_id) # A, B, C...
380
+
381
+ for t in range(start, start + duration):
382
+ if t <= makespan:
383
+ timeline[t] = task_letter
384
+
385
+ timeline_str = "".join(timeline)
386
+ lines.append(f" Worker {worker_id + 1}: {timeline_str}")
387
+
388
+ return "\n".join(lines)
389
+
390
+ def get_rules(self) -> str:
391
+ """Get the rules description for Scheduler.
392
+
393
+ Returns:
394
+ Multi-line string describing the puzzle rules
395
+ """
396
+ return f"""TASK SCHEDULER RULES:
397
+ - Schedule all {self.num_tasks} tasks across {self.num_workers} workers
398
+ - Each task has a duration in hours
399
+ - Tasks with dependencies must wait for predecessors
400
+ - Workers can only do one task at a time
401
+ - Goal: Minimize makespan (total completion time)
402
+ - Optimal makespan: {self.optimal_makespan} hours
403
+ - This is an OPTIMIZATION problem!"""
404
+
405
+ def get_commands(self) -> str:
406
+ """Get the available commands for Scheduler.
407
+
408
+ Returns:
409
+ Multi-line string describing available commands
410
+ """
411
+ return """TASK SCHEDULER COMMANDS:
412
+ assign <task> <worker> <time> - Schedule a task (e.g., 'assign 1 2 5')
413
+ unassign <task> - Remove task from schedule
414
+ show - Display current schedule
415
+ hint - Get scheduling hint
416
+ check - Check if schedule is optimal
417
+ solve - Show optimal schedule (ends game)
418
+ menu - Return to game selection
419
+ quit - Exit the server"""
420
+
421
+ def get_stats(self) -> str:
422
+ """Get current game statistics.
423
+
424
+ Returns:
425
+ String with game stats
426
+ """
427
+ scheduled = len(self.schedule)
428
+ makespan = self._get_makespan()
429
+ optimality = (self.optimal_makespan / makespan * 100) if makespan > 0 else 0
430
+
431
+ return f"Moves: {self.moves_made} | Scheduled: {scheduled}/{self.num_tasks} | Makespan: {makespan}/{self.optimal_makespan}hrs ({optimality:.0f}%) | Seed: {self.seed}"
@@ -0,0 +1,14 @@
1
+ """Scheduler game models."""
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class Task(BaseModel):
7
+ """A task in the Scheduler game."""
8
+
9
+ model_config = ConfigDict(frozen=False) # Allow mutation for game state
10
+
11
+ id: int = Field(ge=0, description="Task ID")
12
+ name: str = Field(min_length=1, description="Task name")
13
+ duration: int = Field(gt=0, description="Task duration in time units")
14
+ dependencies: list[int] = Field(default_factory=list, description="List of task IDs this task depends on")
@@ -0,0 +1,6 @@
1
+ """Shikaku puzzle game module."""
2
+
3
+ from .config import ShikakuConfig
4
+ from .game import ShikakuGame
5
+
6
+ __all__ = ["ShikakuGame", "ShikakuConfig"]
@@ -0,0 +1,24 @@
1
+ """Configuration for Shikaku game."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from ...models.enums import DifficultyLevel
6
+
7
+
8
+ class ShikakuConfig(BaseModel):
9
+ """Configuration for Shikaku game."""
10
+
11
+ difficulty: DifficultyLevel = Field(default=DifficultyLevel.EASY, description="Game difficulty level")
12
+ size: int = Field(ge=5, le=10, description="Grid size (NxN)")
13
+ num_clues: int = Field(ge=3, description="Number of clue numbers")
14
+
15
+ @classmethod
16
+ def from_difficulty(cls, difficulty: DifficultyLevel) -> "ShikakuConfig":
17
+ """Create config from difficulty level."""
18
+ config_map = {
19
+ DifficultyLevel.EASY: {"size": 5, "num_clues": 5},
20
+ DifficultyLevel.MEDIUM: {"size": 7, "num_clues": 7},
21
+ DifficultyLevel.HARD: {"size": 9, "num_clues": 10},
22
+ }
23
+ params = config_map[difficulty]
24
+ return cls(difficulty=difficulty, **params)