From fc81bbec0636bc7626c6d5a619ad13dc0c570806 Mon Sep 17 00:00:00 2001 From: Rich Jones Date: Thu, 30 Jan 2025 20:08:44 +0100 Subject: [PATCH] game of life via cellpylib --- pyproject.toml | 1 + reasoning_gym/games/__init__.py | 4 ++ reasoning_gym/games/game_of_life.py | 96 +++++++++++++++++++++++++++++ tests/test_game_of_life.py | 33 ++++++++++ 4 files changed, 134 insertions(+) create mode 100644 reasoning_gym/games/game_of_life.py create mode 100644 tests/test_game_of_life.py diff --git a/pyproject.toml b/pyproject.toml index 0c2ec1ab..3a546f8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "bfi==1.0.4", + "cellpylib==2.4.0", "sympy>=1.13.1", "magiccube==0.3.0", "pyfiglet==1.0.2" diff --git a/reasoning_gym/games/__init__.py b/reasoning_gym/games/__init__.py index cf083ba4..0826dea6 100644 --- a/reasoning_gym/games/__init__.py +++ b/reasoning_gym/games/__init__.py @@ -3,12 +3,14 @@ Game tasks for training reasoning capabilities: - Board games - Puzzle games - Strategy games +- Simulation games """ from .countdown import CountdownConfig, CountdownDataset from .maze import MazeConfig, MazeDataset from .mini_sudoku import MiniSudokuConfig, MiniSudokuDataset from .sudoku import SudokuConfig, SudokuDataset +from .game_of_life import GameOfLifeConfig, GameOfLifeDataset __all__ = [ "CountdownConfig", @@ -19,4 +21,6 @@ __all__ = [ "SudokuDataset", "MazeConfig", "MazeDataset", + "GameOfLifeConfig", + "GameOfLifeDataset", ] diff --git a/reasoning_gym/games/game_of_life.py b/reasoning_gym/games/game_of_life.py new file mode 100644 index 00000000..cc4dc6a8 --- /dev/null +++ b/reasoning_gym/games/game_of_life.py @@ -0,0 +1,96 @@ +from dataclasses import dataclass +from random import Random +from typing import List, Optional, Tuple, Dict + +import cellpylib as cpl + +from ..factory import ProceduralDataset, register_dataset + +@dataclass +class GameOfLifeConfig: + """Configuration for sudoku puzzle generation""" + + grid_size_x: int = 20 + grid_size_y: int = 20 + filled_cells: int = 100 # actually a max + simulation_steps: int = 1 + seed: Optional[int] = None + size: int = 500 + + def validate(self): + """Validate configuration parameters""" + assert 3 <= self.grid_size_x <= 999, "grid_size_x must be between 0 and 999" + assert 3 <= self.grid_size_y <= 999, "grid_size_y must be between 0 and 999" + assert self.simulation_steps >= 0, "simulation_steps must be gte 0" + assert self.filled_cells <= self.grid_size_x * self.grid_size_y, "filled_cells must fit in x times y" + + +class GameOfLifeConfigDataset(ProceduralDataset): + """Generates Game of Life games with configurable parameters""" + + def __init__(self, config: GameOfLifeConfig): + self._prompt_templates = ["What will this Game of Life board look like after {simulation_steps} steps of simulation?\n\n{board}" + ] + + super().__init__(config=config, seed=config.seed, size=config.size) + + def __getitem__(self, idx: int) -> dict: + """Generate a single GameOfLife task + + Returns: + dict with keys: + - question: str, the task description + - answer: str, a solution string + - metadata: dict with generation parameters + """ + rng = Random(self.seed + idx) + + # Make the board + board = cpl.init_simple2d(self.config.grid_size_x, self.config.grid_size_y) + board[:, :, :] = 0 + + # Add the cells + for i in range(0, self.config.filled_cells): + rx = rng.randint(0, self.config.grid_size_x - 1) + ry = rng.randint(0, self.config.grid_size_y - 1) + board[:, rx, ry] = 1 + + # Simulate the result to get the answer + evolved = cpl.evolve2d(board, timesteps=self.config.simulation_steps + 1, apply_rule=cpl.game_of_life_rule, memoize='recursive') + + board_str = str(board[0]) + result_str = str(evolved[-1]) + + return { + "question": rng.choice(self._prompt_templates).format(simulation_steps=self.config.simulation_steps, board=board_str), + "answer": result_str, + "metadata": { + "grid_size_x": self.config.grid_size_x, + "grid_size_y": self.config.grid_size_y, + "filled_cells": self.config.filled_cells, + "simulation_steps": self.config.simulation_steps, + }, + } + + def score_answer(self, answer: Optional[str], entry: Dict[str, any]) -> float: + """Determine if the solution provided solves the GoL task. + + The function awards 1.0 for a correct answer. + + Args: + answer (Optional[str]): The user's answer. + entry (Dict[str, any]): The original dataset entry containing the correct answer. + + Returns: + float: The computed score between 0.0 and 1.0. + """ + + if answer == None: + return 0.0 + if answer.replace('\n', '') != entry['answer'].replace('\n', ''): + return 0.01 + else: + return 1.0 # Yay + + +register_dataset("game_of_life", GameOfLifeConfigDataset, GameOfLifeConfig) diff --git a/tests/test_game_of_life.py b/tests/test_game_of_life.py new file mode 100644 index 00000000..288a1fe4 --- /dev/null +++ b/tests/test_game_of_life.py @@ -0,0 +1,33 @@ +import pytest + +from reasoning_gym.games.game_of_life import GameOfLifeConfig, GameOfLifeConfigDataset + +def test_game_of_life(): + """Test basic properties and solution of generated items""" + + # Easy + config = GameOfLifeConfig( + seed=42, + size=1, + grid_size_x=20, + grid_size_y=20, + filled_cells=10, + simulation_steps=1 + ) + dataset = GameOfLifeConfigDataset(config) + + for item in dataset: + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + + # # Check metadata contains required fields + assert "grid_size_x" in item["metadata"] + assert "grid_size_y" in item["metadata"] + assert "filled_cells" in item["metadata"] + assert "simulation_steps" in item["metadata"] + + # # Test the scoring + assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 + assert dataset.score_answer(answer=None, entry=item) == 0.0