Skip to content

Commit

Permalink
minification of sokoban contrib
Browse files Browse the repository at this point in the history
  • Loading branch information
andreaskoepf committed Feb 7, 2025
1 parent a065100 commit ef92b18
Show file tree
Hide file tree
Showing 19 changed files with 94 additions and 561 deletions.
74 changes: 0 additions & 74 deletions reasoning_gym/games/contrib/sokoban/README-pt-BR.md

This file was deleted.

30 changes: 3 additions & 27 deletions reasoning_gym/games/contrib/sokoban/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
# 📦 Sokoban Solver and Generator

🗒️ [README pt-BR](https://github.com/xbandrade/sokoban-solver-generator/blob/main/README-pt-BR.md)
This folder contains a minified version of Bruno Andrade's Sokoban game, all pygame dependencies were stripped.

▶️ Video showing the game mechanics, the generator and the solver: [Sokoban Generator and Solver](https://www.youtube.com/watch?v=l0BHKkoViII)

This is a Sokoban puzzle generator and solver that uses BFS, A* and Dijkstra search algorithms.
The original version can be found here: [xbandrade/sokoban-solver-generator](https://github.com/xbandrade/sokoban-solver-generator)


This is a Sokoban puzzle generator and solver that uses BFS, A* and Dijkstra search algorithms.

`Sokoban` is a puzzle game in which the player pushes boxes around in a warehouse, trying to get every box to a goal.



### ➡️ Setup
```pip install -r requirements.txt```

```python -m sokoban```


### ❕Sokoban Puzzle
The puzzle states are stored in a matrix, and each element of the puzzle is represented by a single character in the matrix.
```
Expand All @@ -39,17 +31,13 @@ A box on a goal will have its color changed to green on the game window.


### ❕Sokoban Generator
A pseudo-random valid puzzle will be generated by using the `Random` button on the sidebar.
Entering a valid seed number (1-99999) before using the `Random` button will generate a puzzle using the specified seed.

The generator will initially create a puzzle with a random board size, then the player and the boxes on goals will be randomly placed on the board.
The player will only be able to pull boxes from their positions during the generation of a puzzle, breaking every wall on his way, so it is guaranteed that the puzzle will have a valid solution.


### ❕ Sokoban Solver

<img src="https://raw.githubusercontent.com/xbandrade/sokoban-solver-generator/main/img/levelclear.gif" width=80% height=80%>

The algorithms used to implement the Sokoban puzzle solvers were `Breadth-First Search(BFS)` and `A*`.

The `BFS` solver uses a queue to store the next states of the puzzle it needs to visit. A visited state is stored in a hashset, and BFS won't try to visit the same state twice.
Expand All @@ -60,17 +48,5 @@ The state costs are defined by heuristic functions, and this solver was implemen

All three implementations check for possible deadlocks (states that are impossible to solve) before adding the new state to the queue.

### ❕ Interface Buttons and Options
- `Restart` Reset the current level to its initial state
- `Seed` Specify a seed to be loaded with the `Random` button
- `Random` Generate a pseudo-random valid puzzle
- `Solve BFS` Solve the current puzzle using Breadth-First Search
- `A* Manhattan` Solve the current puzzle using A* with Manhattan Distance heuristic
- `Dijkstra` Solve the current puzzle using A* with Dijkstra distance heuristic
- `Visualize` Display the process of generating the puzzle and show the current best path for the solutions


### ❕ Unit Tests
All unit tests are stored in the `/tests` directory, separated by categories in different classes and files. Use `pytest` to run all unit tests at once.

More about Sokoban: [Wikipedia Article](https://en.wikipedia.org/wiki/Sokoban)
8 changes: 0 additions & 8 deletions reasoning_gym/games/contrib/sokoban/pytest.ini

This file was deleted.

Binary file removed reasoning_gym/games/contrib/sokoban/requirements.txt
Binary file not shown.
2 changes: 1 addition & 1 deletion reasoning_gym/games/contrib/sokoban/src/astar.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def astar(matrix, player_pos, debug=False, heuristic="manhattan"):
return (None, -1 if not heap else depth + 1)


def solve_astar(puzzle, widget=None, visualizer=False, heuristic="manhattan"):
def solve_astar(puzzle, visualizer=False, heuristic="manhattan"):
matrix = puzzle
where = np.where((matrix == "*") | (matrix == "%"))
player_pos = where[0][0], where[1][0]
Expand Down
14 changes: 5 additions & 9 deletions reasoning_gym/games/contrib/sokoban/src/box.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
class Box:
def __init__(self, *groups, x, y, game=None):
def __init__(self, x, y, game=None):
self.game = game
# self.rect = pygame.Rect(x * 64, y * 64, 64, 64)
self.x = x
self.y = y

def can_move(self, move):
target_x, target_y = self.x + move[0] // 64, self.y + move[1] // 64
target_x, target_y = self.x + move[0], self.y + move[1]
target = target_y, target_x
curr = self.y, self.x
target_elem = self.game.puzzle[target]
if not isinstance(target_elem.obj, Box):
curr_elem = self.game.puzzle[curr]
self.rect.y, self.rect.x = target[0] * 64, target[1] * 64
self.y, self.x = target
curr_elem.char = "-" if not curr_elem.ground else "X"
curr_elem.obj = None
Expand All @@ -22,17 +20,15 @@ def can_move(self, move):
return False

def reverse_move(self, move):
target = self.y + move[0] // 64, self.x + move[1] // 64
target = self.y + move[0], self.x + move[1]
curr_pos = self.y, self.x
self.game.puzzle[curr_pos].obj = None
self.game.puzzle[target].obj = self
# self.rect.y, self.rect.x = target[0] * 64, target[1] * 64
self.y, self.x = target
self.game.puzzle[curr_pos].char = "X" if self.game.puzzle[curr_pos].ground else "-"
self.game.puzzle[target].char = "$" if self.game.puzzle[target].ground else "@"


class Obstacle(Box):
def __init__(self, *groups, x, y):
super().__init__(*groups, x=x, y=y)
# self.rect = pygame.Rect(x * 64, y * 64, 64, 64)
def __init__(self, x, y):
super().__init__(x=x, y=y)
28 changes: 0 additions & 28 deletions reasoning_gym/games/contrib/sokoban/src/floor.py

This file was deleted.

63 changes: 37 additions & 26 deletions reasoning_gym/games/contrib/sokoban/src/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,37 @@
import numpy as np

from reasoning_gym.games.contrib.sokoban.src.box import Box, Obstacle
from reasoning_gym.games.contrib.sokoban.src.floor import Floor, Goal
from reasoning_gym.games.contrib.sokoban.src.player import Player, ReversePlayer
from reasoning_gym.games.contrib.sokoban.src.utils import get_state


class Floor:
def __init__(self, x, y):
self.x = x
self.y = y


class Goal(Floor):
def __init__(self, x, y):
super().__init__(x=x, y=y)


class PuzzleElement:
def __init__(self, char, obj=None, ground=None):
def __init__(self, char: str, obj=None, ground=None):
self.char = char
self.ground = ground
self.obj = obj

def __str__(self):
def __str__(self) -> str:
return self.char


class Game:
def __init__(self, width=1216, height=640, level=None, path=None):
def __init__(self, width=19, height=10, level=None, path=None):
self.level = level
self.width = width
self.height = height
self.puzzle = np.empty((height // 64, width // 64), dtype=PuzzleElement)
self.puzzle = np.empty((height, width), dtype=PuzzleElement)

self.player = None
self.puzzle_size = None
Expand All @@ -49,8 +59,8 @@ def get_curr_state(self):
return get_state(self.get_matrix())

def print_puzzle(self):
for h in range(self.height // 64):
for w in range(self.width // 64):
for h in range(self.height):
for w in range(self.width):
if self.puzzle[h, w]:
print(self.puzzle[h, w].char, end=" ")
else:
Expand All @@ -59,8 +69,8 @@ def print_puzzle(self):

def is_level_complete(self):
boxes_left = 0
for h in range(self.height // 64):
for w in range(self.width // 64):
for h in range(self.height):
for w in range(self.width):
if self.puzzle[h, w] and self.puzzle[h, w].char == "@":
boxes_left += 1
return boxes_left == 0
Expand Down Expand Up @@ -98,8 +108,8 @@ def _process_puzzle_data(self, data):

# Calculate puzzle size and padding
self.puzzle_size = (len(data), len(data[0]) if len(data) > 0 else 0)
pad_x = (self.width // 64 - self.puzzle_size[1] - 2) // 2 # -2 matches original file-based logic
pad_y = (self.height // 64 - self.puzzle_size[0]) // 2
pad_x = (self.width - self.puzzle_size[1] - 2) // 2 # -2 matches original file-based logic
pad_y = (self.height - self.puzzle_size[0]) // 2
self.pad_x, self.pad_y = pad_x, pad_y

# Populate puzzle elements
Expand All @@ -110,53 +120,54 @@ def _process_puzzle_data(self, data):

# Create game objects based on characters
if c == "+": # Wall
new_elem.obj = Obstacle(None, x=j + pad_x, y=i + pad_y)
new_elem.obj = Obstacle(x=j + pad_x, y=i + pad_y)
elif c == "@": # Box
new_elem.obj = Box(None, x=j + pad_x, y=i + pad_y, game=self)
new_elem.obj = Box(x=j + pad_x, y=i + pad_y, game=self)
elif c == "*": # Player
new_elem.obj = Player(x=j + pad_x, y=i + pad_y, game=self)
self.player = new_elem.obj
elif c == "X": # Goal
new_elem.ground = Goal(None, x=j + pad_x, y=i + pad_y)
new_elem.ground = Goal(x=j + pad_x, y=i + pad_y)
elif c == "$": # Box on goal
new_elem.ground = Goal(None, x=j + pad_x, y=i + pad_y)
new_elem.obj = Box(None, x=j + pad_x, y=i + pad_y, game=self)
new_elem.ground = Goal(x=j + pad_x, y=i + pad_y)
new_elem.obj = Box(x=j + pad_x, y=i + pad_y, game=self)
elif c == "%": # Player on goal
new_elem.obj = Player(x=j + pad_x, y=i + pad_y, game=self)
new_elem.ground = Goal(None, x=j + pad_x, y=i + pad_y)
new_elem.ground = Goal(x=j + pad_x, y=i + pad_y)
self.player = new_elem.obj
elif c not in " -": # Validation
raise ValueError(f"Invalid character in puzzle: {c}")


class ReverseGame(Game):
def __init__(self, rng: Random, width=1216, height=640, level=None):
def __init__(self, rng: Random, width=19, height=10, level=None):
super().__init__(width, height, level)
self.rng = rng
self.pad_x = 0
self.pad_y = 0

def load_puzzle(self, puzzle):
pad_x = (self.width // 64 - len(puzzle[0]) - 2) // 2
pad_y = (self.height // 64 - len(puzzle)) // 2
self.puzzle_size = (len(puzzle), len(puzzle[0]) if len(puzzle) > 0 else 0)
pad_x = (self.width - len(puzzle[0]) - 2) // 2
pad_y = (self.height - len(puzzle)) // 2
self.pad_x, self.pad_y = pad_x, pad_y
for i, row in enumerate(puzzle):
for j, c in enumerate(row):
new_elem = PuzzleElement(c)
self.puzzle[i + pad_y, j + pad_x] = new_elem
if c == "+": # wall
new_elem.obj = Obstacle(None, x=j + pad_x, y=i + pad_y)
new_elem.obj = Obstacle(x=j + pad_x, y=i + pad_y)
elif c == "@": # box
new_elem.obj = Box(None, x=j + pad_x, y=i + pad_y, game=self)
new_elem.obj = Box(x=j + pad_x, y=i + pad_y, game=self)
elif c == "*": # player
new_elem.obj = ReversePlayer(rng=self.rng, x=j + pad_x, y=i + pad_y, game=self)
self.player = new_elem.obj
elif c == "X": # goal
new_elem.ground = Goal(None, x=j + pad_x, y=i + pad_y)
new_elem.ground = Goal(x=j + pad_x, y=i + pad_y)
elif c == "$": # box on goal
new_elem.ground = Goal(None, x=j + pad_x, y=i + pad_y)
new_elem.obj = Box(None, x=j + pad_x, y=i + pad_y, game=self)
new_elem.ground = Goal(x=j + pad_x, y=i + pad_y)
new_elem.obj = Box(x=j + pad_x, y=i + pad_y, game=self)
elif c == "%": # player on goal
new_elem.obj = ReversePlayer(rng=self.rng, x=j + pad_x, y=i + pad_y, game=self)
new_elem.ground = Goal(None, x=j + pad_x, y=i + pad_y)
new_elem.ground = Goal(x=j + pad_x, y=i + pad_y)
self.player = new_elem.obj
Loading

0 comments on commit ef92b18

Please sign in to comment.