From 71e0fb5418ff639656e76bc2de34f2794707cbe7 Mon Sep 17 00:00:00 2001 From: Jean Kaddour Date: Sat, 8 Feb 2025 15:38:45 +0000 Subject: [PATCH 1/3] feat: add knight_swap --- reasoning_gym/games/__init__.py | 3 + reasoning_gym/games/knight_swap.py | 397 +++++++++++++++++++++++++++++ tests/test_knight_swap.py | 161 ++++++++++++ 3 files changed, 561 insertions(+) create mode 100644 reasoning_gym/games/knight_swap.py create mode 100644 tests/test_knight_swap.py diff --git a/reasoning_gym/games/__init__.py b/reasoning_gym/games/__init__.py index 295f6cdf..dd1ed898 100644 --- a/reasoning_gym/games/__init__.py +++ b/reasoning_gym/games/__init__.py @@ -8,6 +8,7 @@ Game tasks for training reasoning capabilities: from .countdown import CountdownConfig, CountdownDataset from .game_of_life import GameOfLifeConfig, GameOfLifeDataset +from .knight_swap import KnightSwapConfig, KnightSwapDataset from .maze import MazeConfig, MazeDataset from .mini_sudoku import MiniSudokuConfig, MiniSudokuDataset from .n_queens import NQueensDataset @@ -34,4 +35,6 @@ __all__ = [ "NQueensDataset", "TsumegoConfig", "TsumegoDataset", + "KnightSwapConfig", + "KnightSwapDataset", ] diff --git a/reasoning_gym/games/knight_swap.py b/reasoning_gym/games/knight_swap.py new file mode 100644 index 00000000..f76e55a6 --- /dev/null +++ b/reasoning_gym/games/knight_swap.py @@ -0,0 +1,397 @@ +from dataclasses import dataclass +from random import Random +from typing import Dict, List, Optional, Set, Tuple, FrozenSet +import collections +import json + +from ..factory import ProceduralDataset, register_dataset + + +QUESTION_TEMPLATE = """Knight Swap Challenge: + +``` +{board} +``` + +Legend: +- 'w' = White Knight +- 'B' = Black Knight +- Empty squares are marked with '.' + +Objective: +Swap the positions of all white knights with all black knights through valid moves. + +Rules: +1. Knights move in L-shape (2 squares + 1 square perpendicular) +2. Knights can only move to empty squares +3. {start_turn} moves first, then players alternate +4. All knights must reach their target positions (white ↔ black) + +Question: +Is it possible to swap all knights' positions? If yes, list the moves. + +Answer Format: +- For impossible puzzles: "No" +- For possible puzzles: List moves as ["color,from,to", ...] + Example: ["w,A1,B3"] means white knight moves A1→B3 +""" + + +@dataclass +class KnightSwapConfig: + """Configuration for Knight Swap puzzle generation. + + A Knight Swap puzzle involves moving white and black knights on a chess-like board + where each move must be a valid knight's move. The goal is to swap the positions + of white and black knights. + """ + + min_nodes: int = 6 # Minimum number of squares on the board + max_nodes: int = 9 # Maximum number of squares on the board + min_pieces: int = 2 # Minimum number of pieces per color + max_pieces: int = 2 # Maximum number of pieces per color + min_steps: int = 4 # Minimum solution length + max_steps: int = 20 # Maximum solution length + max_attempts: int = 100 # Maximum attempts for board generation and puzzle creation + seed: Optional[int] = None + size: int = 5 # Virtual dataset size + impossible_ratio: float = 0.2 # Ratio of puzzles that should be impossible + + def validate(self): + """Validate configuration parameters""" + assert self.min_nodes >= 6, "min_nodes must be >= 6" + assert self.max_nodes >= self.min_nodes, "max_nodes must be >= min_nodes" + assert self.min_pieces >= 1, "min_pieces must be >= 1" + assert self.max_pieces >= self.min_pieces, "max_pieces must be >= min_pieces" + assert self.min_steps >= 1, "min_steps must be >= 1" + assert self.max_steps >= self.min_steps, "max_steps must be >= min_steps" + assert self.max_attempts >= 1, "max_attempts must be >= 1" + assert 0 <= self.impossible_ratio <= 1, "impossible_ratio must be between 0 and 1" + + +class KnightSwapLogic: + """Core game logic for Knight Swap puzzles.""" + + @staticmethod + def is_knight_move(a: str, b: str) -> bool: + """Check if moving from square 'a' to square 'b' is a legal knight move.""" + a_col = ord(a[0].upper()) - ord("A") + 1 + a_row = int(a[1:]) + b_col = ord(b[0].upper()) - ord("A") + 1 + b_row = int(b[1:]) + return {abs(a_col - b_col), abs(a_row - b_row)} == {1, 2} + + @staticmethod + def is_connected(graph: Dict[str, List[str]]) -> bool: + """Check if a graph is connected (all nodes reachable from any starting node).""" + if not graph: + return True + start = next(iter(graph)) + visited = set() + queue = collections.deque([start]) + while queue: + node = queue.popleft() + if node not in visited: + visited.add(node) + for neighbor in graph[node]: + if neighbor not in visited: + queue.append(neighbor) + return len(visited) == len(graph) + + @staticmethod + def generate_board(num_nodes: int, rng: Random, max_attempts: int = 1000) -> Dict[str, List[str]]: + """Generate a random connected board where edges represent valid knight moves.""" + candidates = ["A1", "A2", "A3", "B1", "B2", "B3", "C1", "C2", "C3", "D1", "D2", "D3"] + attempts = 0 + while True: + attempts += 1 + nodes = rng.sample(candidates, num_nodes) + graph = {node: [] for node in nodes} + for i in range(len(nodes)): + for j in range(i + 1, len(nodes)): + if KnightSwapLogic.is_knight_move(nodes[i], nodes[j]): + graph[nodes[i]].append(nodes[j]) + graph[nodes[j]].append(nodes[i]) + for node in graph: + graph[node].sort() + if KnightSwapLogic.is_connected(graph): + return graph + if attempts > max_attempts: + raise Exception(f"Failed to generate connected board after {max_attempts} attempts") + + @staticmethod + def solve_swap( + board: Dict[str, List[str]], pieces: Dict[str, str], start_turn: str = "w" + ) -> Optional[List[Tuple[str, str, str]]]: + """Find a sequence of moves to swap white and black pieces positions.""" + + @dataclass(frozen=True) + class GameState: + white_set: FrozenSet[str] + black_set: FrozenSet[str] + turn: str + + initial_white = frozenset(pos for pos, piece in pieces.items() if piece == "w") + initial_black = frozenset(pos for pos, piece in pieces.items() if piece == "B") + initial_state = GameState(initial_white, initial_black, start_turn) + + queue = collections.deque([initial_state]) + visited = {initial_state} + predecessors = {initial_state: (None, None)} + + while queue: + state = queue.popleft() + if state.white_set == initial_black and state.black_set == initial_white: + moves = [] + cur_state = state + while predecessors[cur_state][0] is not None: + prev_state, move = predecessors[cur_state] + moves.append(move) + cur_state = prev_state + moves.reverse() + return moves + + current_positions = state.white_set if state.turn == "w" else state.black_set + for pos in current_positions: + for neighbor in board[pos]: + if neighbor in state.white_set or neighbor in state.black_set: + continue + if state.turn == "w": + new_white = frozenset(p if p != pos else neighbor for p in state.white_set) + new_black = state.black_set + else: + new_black = frozenset(p if p != pos else neighbor for p in state.black_set) + new_white = state.white_set + next_turn = "B" if state.turn == "w" else "w" + new_state = GameState(new_white, new_black, next_turn) + if new_state not in visited: + visited.add(new_state) + predecessors[new_state] = (state, (state.turn, pos, neighbor)) + queue.append(new_state) + return None + + +class KnightSwapDataset(ProceduralDataset): + """Generates Knight Swap puzzles with configurable parameters.""" + + def __init__(self, config: KnightSwapConfig): + super().__init__(config=config, seed=config.seed, size=config.size) + self.game_logic = KnightSwapLogic() + + def _format_board(self, board: Dict[str, List[str]], pieces: Dict[str, str]) -> str: + """Format the board state as a string.""" + positions = list(board.keys()) + if not positions: + return "" + + columns = sorted(set(pos[0] for pos in positions)) + rows = sorted(set(int(pos[1:]) for pos in positions), reverse=True) + + lines = [] + # Header + lines.append(" " + " ".join(columns)) + lines.append(" " + "----" * len(columns)) + + # Board rows + for row in rows: + line = f"{row} |" + for col in columns: + pos = col + str(row) + if pos in pieces: + piece = pieces[pos] if pieces[pos] is not None else "." + line += f" {piece} |" + else: + line += " |" + lines.append(line) + lines.append(" " + "----" * len(columns)) + + return "\n".join(lines) + + def _format_moves(self, moves: List[Tuple[str, str, str]]) -> str: + """Format the solution moves as a string.""" + if not moves: + return "No" + return json.dumps([f"{color},{start},{end}" for color, start, end in moves]) + + def __getitem__(self, idx: int) -> Dict: + """Generate a single Knight Swap puzzle.""" + rng = Random(self.seed + idx) + + # Keep trying with new boards until we succeed + board_attempts = 0 + while board_attempts < self.config.max_attempts: + try: + # Generate a new board + num_nodes = rng.randint(self.config.min_nodes, self.config.max_nodes) + board = self.game_logic.generate_board(num_nodes, rng, max_attempts=self.config.max_attempts) + positions = list(board.keys()) + + # Decide if this should be an impossible puzzle + make_impossible = rng.random() < self.config.impossible_ratio + + # Try different piece placements on this board + for _ in range(50): # Reduced attempts per board since we try multiple boards + # Use fixed number of pieces for more reliable generation + num_pieces = self.config.min_pieces + white_positions = rng.sample(positions, num_pieces) + remaining = [p for p in positions if p not in white_positions] + black_positions = rng.sample(remaining, num_pieces) + + pieces = {pos: None for pos in positions} + for pos in white_positions: + pieces[pos] = "w" + for pos in black_positions: + pieces[pos] = "B" + + # For impossible puzzles, try a simpler approach: just remove some key connections + board_copy = {k: list(v) for k, v in board.items()} # Make a copy of the board + if make_impossible: + # Remove critical edges that would make the puzzle impossible + critical_edges = [] + for w_pos in white_positions: + for b_pos in black_positions: + if b_pos in board_copy[w_pos]: + critical_edges.append((w_pos, b_pos)) + + if critical_edges: # Only proceed if we found critical edges + # Remove a random critical edge + w_pos, b_pos = rng.choice(critical_edges) + board_copy[w_pos].remove(b_pos) + board_copy[b_pos].remove(w_pos) + + # Try both starting turns + for start_turn in ["w", "B"]: + solution = self.game_logic.solve_swap(board_copy, pieces, start_turn) + + # Accept solutions with more flexible length requirements + if (make_impossible and solution is None) or ( + not make_impossible + and solution is not None + and self.config.min_steps <= len(solution) <= self.config.max_steps + ): + board_str = self._format_board(board_copy, pieces) + solution_str = self._format_moves(solution) if solution else "No" + + # Generate board states for solvable puzzles + board_states = [] + if solution is not None: + current_pieces = dict(pieces) + board_states.append(dict(current_pieces)) # Initial state + + for color, start, end in solution: + current_pieces[end] = current_pieces[start] + current_pieces[start] = None + board_states.append(dict(current_pieces)) + + return { + "question": QUESTION_TEMPLATE.format(board=board_str, start_turn=start_turn), + "answer": solution_str, + "metadata": { + "board": board_copy, + "pieces": pieces, + "start_turn": start_turn, + "solution": solution, + "is_possible": solution is not None, + "num_steps": len(solution) if solution else 0, + "board_states": board_states if solution is not None else None, + }, + } + + except Exception: + pass # If board generation fails, we'll try again with a new board + + board_attempts += 1 + + raise ValueError(f"Failed to generate valid puzzle after trying {self.config.max_attempts} different boards") + + def score_answer(self, answer: Optional[str], entry: Dict) -> float: + """Score the user's solution for the Knight Swap puzzle. + + The answer should be either: + - "No" if the puzzle is impossible + - A JSON list of moves in format ["color,start,end", ...] where color is 'w' or 'B' + + Returns: + - 1.0 for correct answer (either "No" for impossible puzzles or valid solution of optimal length) + - A proportional score for correct but longer solutions + - 0.05 for valid moves that don't solve the puzzle + - 0.01 for invalid format + - 0.0 for None + """ + if answer is None: + return 0.0 + + answer = answer.strip() + if not answer: + return 0.01 + + # Handle impossible puzzles + if not entry["metadata"]["is_possible"]: + return 1.0 if answer.lower() == "no" else 0.01 + + # Handle "No" answer for possible puzzles + if answer.lower() == "no": + return 0.01 + + try: + # Parse moves from JSON list + move_list = json.loads(answer) + if not isinstance(move_list, list): + return 0.01 + + # Parse moves + moves = [] + for move_str in move_list: + color, start, end = move_str.split(",") + if color not in ("w", "B"): + return 0.01 + moves.append((color, start, end)) + + # Validate and apply moves + board = entry["metadata"]["board"] + pieces = dict(entry["metadata"]["pieces"]) + current_turn = entry["metadata"]["start_turn"] + + # Track board states after each move + board_states = [] + board_states.append(dict(pieces)) # Initial state + + for color, start, end in moves: + if color != current_turn: + return 0.01 + if start not in pieces or pieces[start] != color: + return 0.01 + if end not in board[start]: + return 0.01 + if end in pieces and pieces[end] is not None: + return 0.01 + + # Apply move + pieces[end] = pieces[start] + pieces[start] = None + current_turn = "B" if current_turn == "w" else "w" + + # Store board state after this move + board_states.append(dict(pieces)) + + # Check if solved + white_positions = {pos for pos, piece in pieces.items() if piece == "w"} + black_positions = {pos for pos, piece in pieces.items() if piece == "B"} + initial_white = {pos for pos, piece in entry["metadata"]["pieces"].items() if piece == "w"} + initial_black = {pos for pos, piece in entry["metadata"]["pieces"].items() if piece == "B"} + + if white_positions == initial_black and black_positions == initial_white: + optimal_moves = len(entry["metadata"]["solution"]) + # Add board states to metadata if solution is valid + entry["metadata"]["board_states"] = board_states + if len(moves) <= optimal_moves: + return 1.0 + else: + return optimal_moves / len(moves) + return 0.05 + + except Exception: + return 0.01 + + +register_dataset("knight_swap", KnightSwapDataset, KnightSwapConfig) diff --git a/tests/test_knight_swap.py b/tests/test_knight_swap.py new file mode 100644 index 00000000..c776e5f5 --- /dev/null +++ b/tests/test_knight_swap.py @@ -0,0 +1,161 @@ +import pytest +from reasoning_gym.games.knight_swap import KnightSwapConfig, KnightSwapDataset, KnightSwapLogic + + +def test_default_config_validation(): + """Test that default configuration is valid""" + config = KnightSwapConfig() + config.validate() # Should not raise any exceptions + + +def test_invalid_config(): + """Test that invalid configurations raise appropriate errors""" + with pytest.raises(AssertionError): + config = KnightSwapConfig(min_nodes=4) # Too few nodes + config.validate() + + with pytest.raises(AssertionError): + config = KnightSwapConfig(max_nodes=5, min_nodes=6) # max < min + config.validate() + + +def test_board_connectivity(): + """Test that generated boards are connected""" + config = KnightSwapConfig(min_nodes=6, max_nodes=6) + dataset = KnightSwapDataset(config) + attempts = 10 + # Try multiple puzzles since generation is random + found_connected = False + for i in range(attempts): + board = dataset[i]["metadata"]["board"] + if KnightSwapLogic.is_connected(board): + found_connected = True + break + # Print debug info for failing boards + print(f"\nBoard {i} not connected:") + print(f"Nodes: {list(board.keys())}") + print(f"Edges: {board}") + + assert found_connected, f"Could not find a connected board after {attempts} attempts" + + +def test_known_connected_board(): + """Test connectivity check with a known connected board""" + # Create a simple connected board with valid knight moves + board = { + "A1": ["B3", "C2"], + "B3": ["A1", "C1"], + "C1": ["B3", "A2"], + "A2": ["C1", "B4"], + "B4": ["A2", "C2"], + "C2": ["A1", "B4"], + } + assert KnightSwapLogic.is_connected(board), "Known connected board should be identified as connected" + + +def test_valid_knight_moves(): + """Test that all edges in generated board represent valid knight moves""" + config = KnightSwapConfig(min_nodes=6, max_nodes=6) + dataset = KnightSwapDataset(config) + + board = dataset[0]["metadata"]["board"] + for start, neighbors in board.items(): + for end in neighbors: + assert KnightSwapLogic.is_knight_move(start, end) + + +def test_knight_move_validation(): + """Test basic knight move validation""" + assert KnightSwapLogic.is_knight_move("A1", "B3") # Valid move + assert KnightSwapLogic.is_knight_move("B3", "A1") # Valid move reverse + assert not KnightSwapLogic.is_knight_move("A1", "A2") # Invalid move + assert not KnightSwapLogic.is_knight_move("A1", "B2") # Invalid move + + +def test_simple_solvable_puzzle(): + """Test a minimal solvable puzzle with one piece each""" + config = KnightSwapConfig(min_nodes=6, max_nodes=6, min_pieces=1, max_pieces=1, impossible_ratio=0.0) + dataset = KnightSwapDataset(config) + + # Try to find a solvable puzzle + for i in range(5): # Try a few times since generation is random + puzzle = dataset[i] + if puzzle["metadata"]["is_possible"]: + assert puzzle["answer"] != "No" + assert isinstance(eval(puzzle["answer"]), list) + return + + pytest.fail("Could not find a solvable puzzle") + + +def test_impossible_puzzle(): + """Test that impossible puzzles are correctly identified""" + config = KnightSwapConfig(min_nodes=6, max_nodes=6, min_pieces=2, max_pieces=2, impossible_ratio=1.0) + dataset = KnightSwapDataset(config) + + puzzle = dataset[0] + assert puzzle["metadata"]["is_possible"] is False + assert puzzle["answer"] == "No" + + +def test_alternating_turns(): + """Test that solutions follow alternating turns rule""" + config = KnightSwapConfig(impossible_ratio=0.0) + dataset = KnightSwapDataset(config) + + # Find a solvable puzzle + for i in range(5): + puzzle = dataset[i] + if puzzle["metadata"]["is_possible"]: + moves = eval(puzzle["answer"]) + current_turn = puzzle["metadata"]["start_turn"] + for move in moves: + color = move.split(",")[0] + assert color == current_turn + current_turn = "B" if current_turn == "w" else "w" + return + + pytest.fail("Could not find a solvable puzzle") + + +def test_solution_validation(): + """Test that solutions reach the target state""" + config = KnightSwapConfig(impossible_ratio=0.0) + dataset = KnightSwapDataset(config) + + # Find a solvable puzzle + for i in range(5): + puzzle = dataset[i] + if puzzle["metadata"]["is_possible"]: + # Get initial positions + initial_white = {pos for pos, piece in puzzle["metadata"]["pieces"].items() if piece == "w"} + initial_black = {pos for pos, piece in puzzle["metadata"]["pieces"].items() if piece == "B"} + + # Get final positions from board states + final_state = puzzle["metadata"]["board_states"][-1] + final_white = {pos for pos, piece in final_state.items() if piece == "w"} + final_black = {pos for pos, piece in final_state.items() if piece == "B"} + + # Check that positions are swapped + assert final_white == initial_black + assert final_black == initial_white + return + + pytest.fail("Could not find a solvable puzzle") + + +def test_score_calculation(): + """Test scoring for different answer types""" + config = KnightSwapConfig() + dataset = KnightSwapDataset(config) + + # Get a sample puzzle + puzzle = dataset[0] + + # Test invalid answers + assert dataset.score_answer(None, puzzle) == 0.0 + assert dataset.score_answer("", puzzle) == 0.01 + assert dataset.score_answer("Invalid", puzzle) == 0.01 + + # Test correct answer + assert dataset.score_answer(puzzle["answer"], puzzle) == 1.0 From 689251b3bbffaa0e14edca06825dc92d5f37e110 Mon Sep 17 00:00:00 2001 From: Jean Kaddour Date: Sat, 8 Feb 2025 15:49:10 +0000 Subject: [PATCH 2/3] Update GALLERY.md to include Knight Swap --- GALLERY.md | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) diff --git a/GALLERY.md b/GALLERY.md index a712c1d6..5d96dd84 100644 --- a/GALLERY.md +++ b/GALLERY.md @@ -24,6 +24,7 @@ This gallery shows examples from all available datasets using their default conf - [gsm_symbolic](#gsm_symbolic) - [intermediate_integration](#intermediate_integration) - [isomorphic_strings](#isomorphic_strings) +- [knight_swap](#knight_swap) - [largest_island](#largest_island) - [lcm](#lcm) - [leg_counting](#leg_counting) @@ -1167,6 +1168,342 @@ Metadata: {'words': ['hogtytyof', 'kgqwfwfgh'], 'solution': True, 'solvable': Tr ```` +### knight_swap +Generates Knight Swap puzzles with configurable difficulty + +Default configuration: +```python +min_nodes: int = 6 # Minimum number of squares on the board +max_nodes: int = 9 # Maximum number of squares on the board +min_pieces: int = 2 # Minimum number of pieces per color +max_pieces: int = 2 # Maximum number of pieces per color +min_steps: int = 4 # Minimum solution length +max_steps: int = 20 # Maximum solution length +max_attempts: int = 100 # Maximum attempts for board generation and puzzle creation +seed: Optional[int] = 42 +size: int = 5 # Virtual dataset size +impossible_ratio: float = 0.2 # Ratio of puzzles that should be impossible +``` + +Example tasks: +```` +Example 1: +Question: Knight Swap Challenge: + +``` + A B C D + ---------------- +3 | | w | w | . | + ---------------- +2 | B | | | . | + ---------------- +1 | | B | . | | + ---------------- +``` + +Legend: +- 'w' = White Knight +- 'B' = Black Knight +- Empty squares are marked with '.' + +Objective: +Swap the positions of all white knights with all black knights through valid moves. + +Rules: +1. Knights move in L-shape (2 squares + 1 square perpendicular) +2. Knights can only move to empty squares +3. w moves first, then players alternate +4. All knights must reach their target positions (white ↔ black) + +Question: +Is it possible to swap all knights' positions? If yes, list the moves. + +Answer Format: +- For impossible puzzles: "No" +- For possible puzzles: List moves as ["color,from,to", ...] + Example: ["w,A1,B3"] means white knight moves A1→B3 + +Answer: No + + +Metadata: {'board': {'D3': ['C1'], 'C3': ['B1'], 'B3': ['C1', 'D2'], 'B1': ['C3', 'D2'], 'A2': ['C1'], 'C1': ['A2', 'B3', 'D3'], 'D2': ['B1', 'B3']}, 'pieces': {'D3': None, 'C3': 'w', 'B3': 'w', 'B1': 'B', 'A2': 'B', 'C1': None, 'D2': None}, 'start_turn': 'w', 'solution': None, 'is_possible': False, 'num_steps': 0, 'board_states': None} + +-------------------------------------------------- + +Example 2: +Question: Knight Swap Challenge: + +``` + A B C D + ---------------- +3 | | B | w | | + ---------------- +2 | . | . | | B | + ---------------- +1 | . | w | . | . | + ---------------- +``` + +Legend: +- 'w' = White Knight +- 'B' = Black Knight +- Empty squares are marked with '.' + +Objective: +Swap the positions of all white knights with all black knights through valid moves. + +Rules: +1. Knights move in L-shape (2 squares + 1 square perpendicular) +2. Knights can only move to empty squares +3. w moves first, then players alternate +4. All knights must reach their target positions (white ↔ black) + +Question: +Is it possible to swap all knights' positions? If yes, list the moves. + +Answer Format: +- For impossible puzzles: "No" +- For possible puzzles: List moves as ["color,from,to", ...] + Example: ["w,A1,B3"] means white knight moves A1→B3 + +Answer: ["w,C3,A2", "B,B3,A1", "w,B1,C3", "B,D2,B1", "w,A2,C1", "B,A1,B3", "w,C3,A2", "B,B3,D2", "w,C1,B3", "B,B1,C3", "w,A2,C1", "B,D2,B1", "w,B3,D2", "B,C3,A2", "w,C1,B3", "B,A2,C3"] + +Board states after each move: + +Move 0: + A B C D + ---------------- +3 | | B | w | | + ---------------- +2 | . | . | | B | + ---------------- +1 | . | w | . | . | + ---------------- + +Move 1: +Move made: w C3→A2 + A B C D + ---------------- +3 | | B | . | | + ---------------- +2 | w | . | | B | + ---------------- +1 | . | w | . | . | + ---------------- + +Move 2: +Move made: B B3→A1 + A B C D + ---------------- +3 | | . | . | | + ---------------- +2 | w | . | | B | + ---------------- +1 | B | w | . | . | + ---------------- + +Move 3: +Move made: w B1→C3 + A B C D + ---------------- +3 | | . | w | | + ---------------- +2 | w | . | | B | + ---------------- +1 | B | . | . | . | + ---------------- + +Move 4: +Move made: B D2→B1 + A B C D + ---------------- +3 | | . | w | | + ---------------- +2 | w | . | | . | + ---------------- +1 | B | B | . | . | + ---------------- + +Move 5: +Move made: w A2→C1 + A B C D + ---------------- +3 | | . | w | | + ---------------- +2 | . | . | | . | + ---------------- +1 | B | B | w | . | + ---------------- + +Move 6: +Move made: B A1→B3 + A B C D + ---------------- +3 | | B | w | | + ---------------- +2 | . | . | | . | + ---------------- +1 | . | B | w | . | + ---------------- + +Move 7: +Move made: w C3→A2 + A B C D + ---------------- +3 | | B | . | | + ---------------- +2 | w | . | | . | + ---------------- +1 | . | B | w | . | + ---------------- + +Move 8: +Move made: B B3→D2 + A B C D + ---------------- +3 | | . | . | | + ---------------- +2 | w | . | | B | + ---------------- +1 | . | B | w | . | + ---------------- + +Move 9: +Move made: w C1→B3 + A B C D + ---------------- +3 | | w | . | | + ---------------- +2 | w | . | | B | + ---------------- +1 | . | B | . | . | + ---------------- + +Move 10: +Move made: B B1→C3 + A B C D + ---------------- +3 | | w | B | | + ---------------- +2 | w | . | | B | + ---------------- +1 | . | . | . | . | + ---------------- + +Move 11: +Move made: w A2→C1 + A B C D + ---------------- +3 | | w | B | | + ---------------- +2 | . | . | | B | + ---------------- +1 | . | . | w | . | + ---------------- + +Move 12: +Move made: B D2→B1 + A B C D + ---------------- +3 | | w | B | | + ---------------- +2 | . | . | | . | + ---------------- +1 | . | B | w | . | + ---------------- + +Move 13: +Move made: w B3→D2 + A B C D + ---------------- +3 | | . | B | | + ---------------- +2 | . | . | | w | + ---------------- +1 | . | B | w | . | + ---------------- + +Move 14: +Move made: B C3→A2 + A B C D + ---------------- +3 | | . | . | | + ---------------- +2 | B | . | | w | + ---------------- +1 | . | B | w | . | + ---------------- + +Move 15: +Move made: w C1→B3 + A B C D + ---------------- +3 | | w | . | | + ---------------- +2 | B | . | | w | + ---------------- +1 | . | B | . | . | + ---------------- + +Move 16: +Move made: B A2→C3 + A B C D + ---------------- +3 | | w | B | | + ---------------- +2 | . | . | | w | + ---------------- +1 | . | B | . | . | + ---------------- + +Other metadata: {'board': {'A2': ['C1', 'C3'], 'A1': ['B3'], 'B1': ['C3', 'D2'], 'D2': ['B1', 'B3'], 'B3': ['A1', 'C1', 'D2'], 'D1': ['B2', 'C3'], 'C3': ['A2', 'B1', 'D1'], 'B2': ['D1'], 'C1': ['A2', 'B3']}, 'pieces': {'A2': None, 'A1': None, 'B1': 'w', 'D2': 'B', 'B3': 'B', 'D1': None, 'C3': 'w', 'B2': None, 'C1': None}, 'start_turn': 'w', 'solution': [('w', 'C3', 'A2'), ('B', 'B3', 'A1'), ('w', 'B1', 'C3'), ('B', 'D2', 'B1'), ('w', 'A2', 'C1'), ('B', 'A1', 'B3'), ('w', 'C3', 'A2'), ('B', 'B3', 'D2'), ('w', 'C1', 'B3'), ('B', 'B1', 'C3'), ('w', 'A2', 'C1'), ('B', 'D2', 'B1'), ('w', 'B3', 'D2'), ('B', 'C3', 'A2'), ('w', 'C1', 'B3'), ('B', 'A2', 'C3')], 'is_possible': True, 'num_steps': 16} + +-------------------------------------------------- + +Example 3: +Question: Knight Swap Challenge: + +``` + A B C D + ---------------- +3 | | . | B | . | + ---------------- +2 | w | | w | | + ---------------- +1 | . | . | B | | + ---------------- +``` + +Legend: +- 'w' = White Knight +- 'B' = Black Knight +- Empty squares are marked with '.' + +Objective: +Swap the positions of all white knights with all black knights through valid moves. + +Rules: +1. Knights move in L-shape (2 squares + 1 square perpendicular) +2. Knights can only move to empty squares +3. w moves first, then players alternate +4. All knights must reach their target positions (white ↔ black) + +Question: +Is it possible to swap all knights' positions? If yes, list the moves. + +Answer Format: +- For impossible puzzles: "No" +- For possible puzzles: List moves as ["color,from,to", ...] + Example: ["w,A1,B3"] means white knight moves A1→B3 + +Answer: No + + +Metadata: {'board': {'C1': ['A2', 'B3', 'D3'], 'B3': ['A1', 'C1'], 'C3': ['B1'], 'D3': ['C1'], 'A1': ['B3', 'C2'], 'A2': ['C1'], 'B1': ['C3'], 'C2': ['A1']}, 'pieces': {'C1': 'B', 'B3': None, 'C3': 'B', 'D3': None, 'A1': None, 'A2': 'w', 'B1': None, 'C2': 'w'}, 'start_turn': 'w', 'solution': None, 'is_possible': False, 'num_steps': 0, 'board_states': None} + +-------------------------------------------------- +```` + ### largest_island Generates Largest Island exercises with configurable difficulty From a92692da5749973d4eff1357f56b0bddbb81570f Mon Sep 17 00:00:00 2001 From: Jean Kaddour Date: Sat, 8 Feb 2025 15:53:29 +0000 Subject: [PATCH 3/3] chore: run isort --- reasoning_gym/games/knight_swap.py | 7 +++---- tests/test_knight_swap.py | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/reasoning_gym/games/knight_swap.py b/reasoning_gym/games/knight_swap.py index f76e55a6..8e8c1167 100644 --- a/reasoning_gym/games/knight_swap.py +++ b/reasoning_gym/games/knight_swap.py @@ -1,12 +1,11 @@ -from dataclasses import dataclass -from random import Random -from typing import Dict, List, Optional, Set, Tuple, FrozenSet import collections import json +from dataclasses import dataclass +from random import Random +from typing import Dict, FrozenSet, List, Optional, Set, Tuple from ..factory import ProceduralDataset, register_dataset - QUESTION_TEMPLATE = """Knight Swap Challenge: ``` diff --git a/tests/test_knight_swap.py b/tests/test_knight_swap.py index c776e5f5..74b606d5 100644 --- a/tests/test_knight_swap.py +++ b/tests/test_knight_swap.py @@ -1,4 +1,5 @@ import pytest + from reasoning_gym.games.knight_swap import KnightSwapConfig, KnightSwapDataset, KnightSwapLogic