ubongosolve 0.1.0__tar.gz
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.
- ubongosolve-0.1.0/LICENSE +21 -0
- ubongosolve-0.1.0/PKG-INFO +132 -0
- ubongosolve-0.1.0/README.md +117 -0
- ubongosolve-0.1.0/pyproject.toml +33 -0
- ubongosolve-0.1.0/setup.cfg +4 -0
- ubongosolve-0.1.0/ubongosolve.egg-info/PKG-INFO +132 -0
- ubongosolve-0.1.0/ubongosolve.egg-info/SOURCES.txt +9 -0
- ubongosolve-0.1.0/ubongosolve.egg-info/dependency_links.txt +1 -0
- ubongosolve-0.1.0/ubongosolve.egg-info/requires.txt +2 -0
- ubongosolve-0.1.0/ubongosolve.egg-info/top_level.txt +1 -0
- ubongosolve-0.1.0/ubongosolve.py +485 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kota Mori
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ubongosolve
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Solve ubongo puzzle by constraint programming
|
|
5
|
+
Author-email: Kota Mori <kmori05@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Topic :: Games/Entertainment :: Puzzle Games
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: matplotlib
|
|
13
|
+
Requires-Dist: ortools
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# 🧩 UbongoSolve
|
|
17
|
+
|
|
18
|
+
A Python solver for [Ubongo](https://en.wikipedia.org/wiki/Ubongo)-style tile-placement puzzles using constraint programming ([OR-Tools CP-SAT](https://developers.google.com/optimization/cp/cp_solver)).
|
|
19
|
+
|
|
20
|
+
Given a set of polyomino pieces and a board, UbongoSolver finds a placement that tiles the board exactly — no gaps, no overlaps.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- **Automatic piece orientation**: handles all rotations (0°/90°/180°/270°) and flips
|
|
25
|
+
- **Constraint programming engine**: powered by Google OR-Tools CP-SAT for fast, exact solving
|
|
26
|
+
- **Visualization**: text-based and matplotlib-based solution display with colored pieces and piece-boundary outlines
|
|
27
|
+
- **Built-in puzzles**: includes the [White Chocolate puzzle](https://store.hanayamatoys.co.jp/items/58611532) with a few board variants
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
<img src="images/output-sample-1.png" height="300" /> <img src="images/output-sample-2.png" width="250" />
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Requires Python 3.10+.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install ubongosolve
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### From source
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/kota7/ubongosolve.git
|
|
44
|
+
cd ubongosolve
|
|
45
|
+
pip install -U .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from ubongosolve import Piece, Board, UbongoPuzzle
|
|
52
|
+
|
|
53
|
+
# Define pieces as sets of (x, y) coordinates
|
|
54
|
+
pieces = [
|
|
55
|
+
Piece([(0,0), (0,1), (0,2), (0,3), (1,3)]), # L-shape
|
|
56
|
+
Piece([(0,0), (1,0), (1,1), (2,1)]), # S-shape
|
|
57
|
+
Piece([(0,0), (0,1), (1,1)]), # mini L-shape
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# Define a board
|
|
61
|
+
board = Board([(x, y) for x in range(4) for y in range(3)])
|
|
62
|
+
|
|
63
|
+
# Solve
|
|
64
|
+
solver = UbongoPuzzle(pieces, board)
|
|
65
|
+
status = solver.solve()
|
|
66
|
+
print(status) # "OPTIMAL" or "FEASIBLE"
|
|
67
|
+
|
|
68
|
+
# View the solution
|
|
69
|
+
solver.print_solution() # text output
|
|
70
|
+
solver.plot_solution() # matplotlib figure
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
### Example: Ubongo Sample problem
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from ubongosolve import UbongoPuzzle, ubongo
|
|
78
|
+
|
|
79
|
+
problem = ubongo.sample_problems[0] # Two problems are included
|
|
80
|
+
solver = UbongoPuzzle(problem["pieces"], problem["board"])
|
|
81
|
+
solver.solve()
|
|
82
|
+
solver.plot_solution()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Example: White Chocolate puzzle
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from ubongosolve import UbongoPuzzle, whitechocolate
|
|
89
|
+
|
|
90
|
+
# Solve the basic 5×8 board
|
|
91
|
+
solver = UbongoPuzzle(whitechocolate.pieces, whitechocolate.board)
|
|
92
|
+
solver.solve()
|
|
93
|
+
solver.plot_solution()
|
|
94
|
+
|
|
95
|
+
# Try a challenge board
|
|
96
|
+
solver = UbongoPuzzle(whitechocolate.pieces, whitechocolate.boards["challenge_6"])
|
|
97
|
+
solver.solve()
|
|
98
|
+
solver.plot_solution()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## API Reference
|
|
102
|
+
|
|
103
|
+
### `Piece(coordinates)`
|
|
104
|
+
|
|
105
|
+
A polyomino piece defined by a set of `(x, y)` cell coordinates. Coordinates are automatically normalized so the minimum x and y are zero.
|
|
106
|
+
|
|
107
|
+
### `Board(coordinates)`
|
|
108
|
+
|
|
109
|
+
A board defined by a set of `(x, y)` cell coordinates. Can be any shape — rectangles, L-shapes, boards with holes, split boards, etc.
|
|
110
|
+
|
|
111
|
+
### `UbongoPuzzle(pieces, board)`
|
|
112
|
+
|
|
113
|
+
| Method / Property | Description |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `solve(timeout=100)` | Solve the puzzle. Returns status string (`"OPTIMAL"`, `"FEASIBLE"`, `"INFEASIBLE"`, ...) |
|
|
116
|
+
| `solution` | `dict[tuple[int,int], int]` mapping each cell to its piece ID |
|
|
117
|
+
| `print_solution()` | Print an ASCII representation |
|
|
118
|
+
| `plot_solution()` | Display a matplotlib figure with colored pieces |
|
|
119
|
+
| `solution_as_fig` | Returns `(fig, ax)` without displaying |
|
|
120
|
+
|
|
121
|
+
## How It Works
|
|
122
|
+
|
|
123
|
+
1. **Piece expansion**: each piece is expanded into all distinct orientations (up to 8: 4 rotations × 2 flips).
|
|
124
|
+
2. **Origin candidates**: for each orientation, all valid placements on the board are precomputed.
|
|
125
|
+
3. **CP-SAT model**: boolean variables represent "piece *i* in orientation *o* is placed at origin *(x, y)*". Constraints enforce:
|
|
126
|
+
- Each piece is placed exactly once (one orientation + origin selected).
|
|
127
|
+
- Each board cell is covered by exactly one piece.
|
|
128
|
+
4. **Solve**: OR-Tools CP-SAT finds a feasible assignment or proves infeasibility.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# 🧩 UbongoSolve
|
|
2
|
+
|
|
3
|
+
A Python solver for [Ubongo](https://en.wikipedia.org/wiki/Ubongo)-style tile-placement puzzles using constraint programming ([OR-Tools CP-SAT](https://developers.google.com/optimization/cp/cp_solver)).
|
|
4
|
+
|
|
5
|
+
Given a set of polyomino pieces and a board, UbongoSolver finds a placement that tiles the board exactly — no gaps, no overlaps.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Automatic piece orientation**: handles all rotations (0°/90°/180°/270°) and flips
|
|
10
|
+
- **Constraint programming engine**: powered by Google OR-Tools CP-SAT for fast, exact solving
|
|
11
|
+
- **Visualization**: text-based and matplotlib-based solution display with colored pieces and piece-boundary outlines
|
|
12
|
+
- **Built-in puzzles**: includes the [White Chocolate puzzle](https://store.hanayamatoys.co.jp/items/58611532) with a few board variants
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
<img src="images/output-sample-1.png" height="300" /> <img src="images/output-sample-2.png" width="250" />
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Requires Python 3.10+.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install ubongosolve
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### From source
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/kota7/ubongosolve.git
|
|
29
|
+
cd ubongosolve
|
|
30
|
+
pip install -U .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from ubongosolve import Piece, Board, UbongoPuzzle
|
|
37
|
+
|
|
38
|
+
# Define pieces as sets of (x, y) coordinates
|
|
39
|
+
pieces = [
|
|
40
|
+
Piece([(0,0), (0,1), (0,2), (0,3), (1,3)]), # L-shape
|
|
41
|
+
Piece([(0,0), (1,0), (1,1), (2,1)]), # S-shape
|
|
42
|
+
Piece([(0,0), (0,1), (1,1)]), # mini L-shape
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# Define a board
|
|
46
|
+
board = Board([(x, y) for x in range(4) for y in range(3)])
|
|
47
|
+
|
|
48
|
+
# Solve
|
|
49
|
+
solver = UbongoPuzzle(pieces, board)
|
|
50
|
+
status = solver.solve()
|
|
51
|
+
print(status) # "OPTIMAL" or "FEASIBLE"
|
|
52
|
+
|
|
53
|
+
# View the solution
|
|
54
|
+
solver.print_solution() # text output
|
|
55
|
+
solver.plot_solution() # matplotlib figure
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
### Example: Ubongo Sample problem
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from ubongosolve import UbongoPuzzle, ubongo
|
|
63
|
+
|
|
64
|
+
problem = ubongo.sample_problems[0] # Two problems are included
|
|
65
|
+
solver = UbongoPuzzle(problem["pieces"], problem["board"])
|
|
66
|
+
solver.solve()
|
|
67
|
+
solver.plot_solution()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Example: White Chocolate puzzle
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from ubongosolve import UbongoPuzzle, whitechocolate
|
|
74
|
+
|
|
75
|
+
# Solve the basic 5×8 board
|
|
76
|
+
solver = UbongoPuzzle(whitechocolate.pieces, whitechocolate.board)
|
|
77
|
+
solver.solve()
|
|
78
|
+
solver.plot_solution()
|
|
79
|
+
|
|
80
|
+
# Try a challenge board
|
|
81
|
+
solver = UbongoPuzzle(whitechocolate.pieces, whitechocolate.boards["challenge_6"])
|
|
82
|
+
solver.solve()
|
|
83
|
+
solver.plot_solution()
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API Reference
|
|
87
|
+
|
|
88
|
+
### `Piece(coordinates)`
|
|
89
|
+
|
|
90
|
+
A polyomino piece defined by a set of `(x, y)` cell coordinates. Coordinates are automatically normalized so the minimum x and y are zero.
|
|
91
|
+
|
|
92
|
+
### `Board(coordinates)`
|
|
93
|
+
|
|
94
|
+
A board defined by a set of `(x, y)` cell coordinates. Can be any shape — rectangles, L-shapes, boards with holes, split boards, etc.
|
|
95
|
+
|
|
96
|
+
### `UbongoPuzzle(pieces, board)`
|
|
97
|
+
|
|
98
|
+
| Method / Property | Description |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `solve(timeout=100)` | Solve the puzzle. Returns status string (`"OPTIMAL"`, `"FEASIBLE"`, `"INFEASIBLE"`, ...) |
|
|
101
|
+
| `solution` | `dict[tuple[int,int], int]` mapping each cell to its piece ID |
|
|
102
|
+
| `print_solution()` | Print an ASCII representation |
|
|
103
|
+
| `plot_solution()` | Display a matplotlib figure with colored pieces |
|
|
104
|
+
| `solution_as_fig` | Returns `(fig, ax)` without displaying |
|
|
105
|
+
|
|
106
|
+
## How It Works
|
|
107
|
+
|
|
108
|
+
1. **Piece expansion**: each piece is expanded into all distinct orientations (up to 8: 4 rotations × 2 flips).
|
|
109
|
+
2. **Origin candidates**: for each orientation, all valid placements on the board are precomputed.
|
|
110
|
+
3. **CP-SAT model**: boolean variables represent "piece *i* in orientation *o* is placed at origin *(x, y)*". Constraints enforce:
|
|
111
|
+
- Each piece is placed exactly once (one orientation + origin selected).
|
|
112
|
+
- Each board cell is covered by exactly one piece.
|
|
113
|
+
4. **Solve**: OR-Tools CP-SAT finds a feasible assignment or proves infeasibility.
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ubongosolve"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Solve ubongo puzzle by constraint programming"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Kota Mori", email = "kmori05@gmail.com"}
|
|
11
|
+
]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Topic :: Games/Entertainment :: Puzzle Games",
|
|
16
|
+
]
|
|
17
|
+
readme = "README.md"
|
|
18
|
+
requires-python = ">=3.10"
|
|
19
|
+
dependencies = [
|
|
20
|
+
"matplotlib",
|
|
21
|
+
"ortools",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"jupyter",
|
|
27
|
+
"pytest",
|
|
28
|
+
"twine>=6.2.0",
|
|
29
|
+
"wheel>=0.46.3",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
py-modules = ["ubongosolve"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ubongosolve
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Solve ubongo puzzle by constraint programming
|
|
5
|
+
Author-email: Kota Mori <kmori05@gmail.com>
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Topic :: Games/Entertainment :: Puzzle Games
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: matplotlib
|
|
13
|
+
Requires-Dist: ortools
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# 🧩 UbongoSolve
|
|
17
|
+
|
|
18
|
+
A Python solver for [Ubongo](https://en.wikipedia.org/wiki/Ubongo)-style tile-placement puzzles using constraint programming ([OR-Tools CP-SAT](https://developers.google.com/optimization/cp/cp_solver)).
|
|
19
|
+
|
|
20
|
+
Given a set of polyomino pieces and a board, UbongoSolver finds a placement that tiles the board exactly — no gaps, no overlaps.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- **Automatic piece orientation**: handles all rotations (0°/90°/180°/270°) and flips
|
|
25
|
+
- **Constraint programming engine**: powered by Google OR-Tools CP-SAT for fast, exact solving
|
|
26
|
+
- **Visualization**: text-based and matplotlib-based solution display with colored pieces and piece-boundary outlines
|
|
27
|
+
- **Built-in puzzles**: includes the [White Chocolate puzzle](https://store.hanayamatoys.co.jp/items/58611532) with a few board variants
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
<img src="images/output-sample-1.png" height="300" /> <img src="images/output-sample-2.png" width="250" />
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Requires Python 3.10+.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install ubongosolve
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### From source
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/kota7/ubongosolve.git
|
|
44
|
+
cd ubongosolve
|
|
45
|
+
pip install -U .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from ubongosolve import Piece, Board, UbongoPuzzle
|
|
52
|
+
|
|
53
|
+
# Define pieces as sets of (x, y) coordinates
|
|
54
|
+
pieces = [
|
|
55
|
+
Piece([(0,0), (0,1), (0,2), (0,3), (1,3)]), # L-shape
|
|
56
|
+
Piece([(0,0), (1,0), (1,1), (2,1)]), # S-shape
|
|
57
|
+
Piece([(0,0), (0,1), (1,1)]), # mini L-shape
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# Define a board
|
|
61
|
+
board = Board([(x, y) for x in range(4) for y in range(3)])
|
|
62
|
+
|
|
63
|
+
# Solve
|
|
64
|
+
solver = UbongoPuzzle(pieces, board)
|
|
65
|
+
status = solver.solve()
|
|
66
|
+
print(status) # "OPTIMAL" or "FEASIBLE"
|
|
67
|
+
|
|
68
|
+
# View the solution
|
|
69
|
+
solver.print_solution() # text output
|
|
70
|
+
solver.plot_solution() # matplotlib figure
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
### Example: Ubongo Sample problem
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from ubongosolve import UbongoPuzzle, ubongo
|
|
78
|
+
|
|
79
|
+
problem = ubongo.sample_problems[0] # Two problems are included
|
|
80
|
+
solver = UbongoPuzzle(problem["pieces"], problem["board"])
|
|
81
|
+
solver.solve()
|
|
82
|
+
solver.plot_solution()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Example: White Chocolate puzzle
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from ubongosolve import UbongoPuzzle, whitechocolate
|
|
89
|
+
|
|
90
|
+
# Solve the basic 5×8 board
|
|
91
|
+
solver = UbongoPuzzle(whitechocolate.pieces, whitechocolate.board)
|
|
92
|
+
solver.solve()
|
|
93
|
+
solver.plot_solution()
|
|
94
|
+
|
|
95
|
+
# Try a challenge board
|
|
96
|
+
solver = UbongoPuzzle(whitechocolate.pieces, whitechocolate.boards["challenge_6"])
|
|
97
|
+
solver.solve()
|
|
98
|
+
solver.plot_solution()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## API Reference
|
|
102
|
+
|
|
103
|
+
### `Piece(coordinates)`
|
|
104
|
+
|
|
105
|
+
A polyomino piece defined by a set of `(x, y)` cell coordinates. Coordinates are automatically normalized so the minimum x and y are zero.
|
|
106
|
+
|
|
107
|
+
### `Board(coordinates)`
|
|
108
|
+
|
|
109
|
+
A board defined by a set of `(x, y)` cell coordinates. Can be any shape — rectangles, L-shapes, boards with holes, split boards, etc.
|
|
110
|
+
|
|
111
|
+
### `UbongoPuzzle(pieces, board)`
|
|
112
|
+
|
|
113
|
+
| Method / Property | Description |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `solve(timeout=100)` | Solve the puzzle. Returns status string (`"OPTIMAL"`, `"FEASIBLE"`, `"INFEASIBLE"`, ...) |
|
|
116
|
+
| `solution` | `dict[tuple[int,int], int]` mapping each cell to its piece ID |
|
|
117
|
+
| `print_solution()` | Print an ASCII representation |
|
|
118
|
+
| `plot_solution()` | Display a matplotlib figure with colored pieces |
|
|
119
|
+
| `solution_as_fig` | Returns `(fig, ax)` without displaying |
|
|
120
|
+
|
|
121
|
+
## How It Works
|
|
122
|
+
|
|
123
|
+
1. **Piece expansion**: each piece is expanded into all distinct orientations (up to 8: 4 rotations × 2 flips).
|
|
124
|
+
2. **Origin candidates**: for each orientation, all valid placements on the board are precomputed.
|
|
125
|
+
3. **CP-SAT model**: boolean variables represent "piece *i* in orientation *o* is placed at origin *(x, y)*". Constraints enforce:
|
|
126
|
+
- Each piece is placed exactly once (one orientation + origin selected).
|
|
127
|
+
- Each board cell is covered by exactly one piece.
|
|
128
|
+
4. **Solve**: OR-Tools CP-SAT finds a feasible assignment or proves infeasibility.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ubongosolve
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
#%%
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import itertools
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import TypeAlias
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import matplotlib.patches as patches
|
|
8
|
+
from matplotlib.colors import to_rgba
|
|
9
|
+
from ortools.sat.python import cp_model
|
|
10
|
+
|
|
11
|
+
Coord: TypeAlias = tuple[int, int]
|
|
12
|
+
Coords: TypeAlias = set[Coord]
|
|
13
|
+
|
|
14
|
+
def _normalize_coordinates(coordinates: Coord) -> Coord:
|
|
15
|
+
# Make sure that all coodinates are non-negative, and
|
|
16
|
+
# the smallest is zero for both x and y axis.
|
|
17
|
+
# Note that this does not ensure that (0, 0) is in the coordinates.
|
|
18
|
+
# E.g., shapes like "_|"
|
|
19
|
+
# Return the new coordinates
|
|
20
|
+
min_x = min(c[0] for c in coordinates)
|
|
21
|
+
min_y = min(c[1] for c in coordinates)
|
|
22
|
+
out = set()
|
|
23
|
+
for x, y in coordinates:
|
|
24
|
+
out.add((x - min_x, y - min_y))
|
|
25
|
+
return out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _coordindates_as_str(coordinates: Coords, char_positive: str="# ", char_negative: str=" ") -> str:
|
|
29
|
+
coordinates_normalized = _normalize_coordinates(coordinates)
|
|
30
|
+
#print(coordinates_normalized)
|
|
31
|
+
max_x = max(c[0] for c in coordinates_normalized)
|
|
32
|
+
max_y = max(c[1] for c in coordinates_normalized)
|
|
33
|
+
|
|
34
|
+
flag = []
|
|
35
|
+
for _ in range(max_y + 1):
|
|
36
|
+
flag.append([False] * (max_x + 1))
|
|
37
|
+
#print(flag)
|
|
38
|
+
for x, y in coordinates_normalized:
|
|
39
|
+
flag[y][x] = True
|
|
40
|
+
|
|
41
|
+
out = "\n".join("".join(char_positive if c else char_negative for c in row) for row in flag)
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _rotate(coordinates: Coords, angle: int) -> Coords:
|
|
46
|
+
# Returns new coordinates such that
|
|
47
|
+
# the original is rotated counter-clockwise around the origin.
|
|
48
|
+
# Angle must be multiple of 90
|
|
49
|
+
angle = angle % 360
|
|
50
|
+
if angle % 90 != 0:
|
|
51
|
+
raise ValueError(f"rotation angle must be multiple of 90, but received: {angle}")
|
|
52
|
+
if angle == 90:
|
|
53
|
+
return set((y, -x) for x, y in coordinates)
|
|
54
|
+
elif angle == 180:
|
|
55
|
+
return set((-x, -y) for x, y in coordinates)
|
|
56
|
+
elif angle == 270:
|
|
57
|
+
return set((-y, x) for x, y in coordinates)
|
|
58
|
+
else:
|
|
59
|
+
return set(coordinates)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _flip(coordinates: Coords) -> Coords:
|
|
63
|
+
# Returns new coordinates such that
|
|
64
|
+
# the original is flipped horizontally (over y-axis)
|
|
65
|
+
return set((-x, y) for x, y in coordinates)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Piece:
|
|
69
|
+
def __init__(self, coordinates: Coords):
|
|
70
|
+
self.coordinates = _normalize_coordinates(coordinates)
|
|
71
|
+
|
|
72
|
+
def __str__(self) -> str:
|
|
73
|
+
return _coordindates_as_str(self.coordinates)
|
|
74
|
+
|
|
75
|
+
def rotate(self, angle: int) -> Piece:
|
|
76
|
+
return Piece(_rotate(self.coordinates, angle))
|
|
77
|
+
|
|
78
|
+
def flip(self) -> Piece:
|
|
79
|
+
return Piece(_flip(self.coordinates))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Board:
|
|
83
|
+
def __init__(self, coordinates: Coords):
|
|
84
|
+
self.coordinates = _normalize_coordinates(coordinates)
|
|
85
|
+
|
|
86
|
+
def __str__(self) -> str:
|
|
87
|
+
return _coordindates_as_str(self.coordinates)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class UbongoPuzzle:
|
|
91
|
+
def __init__(self, pieces: list[Piece], board: Board):
|
|
92
|
+
self.pieces = pieces
|
|
93
|
+
self.board = board
|
|
94
|
+
self.model, self.pieces_ext, self.origin_flags = make_ubongo_problem(pieces, board)
|
|
95
|
+
self.solver = cp_model.CpSolver()
|
|
96
|
+
self.status = 0 # unknown
|
|
97
|
+
|
|
98
|
+
def solve(self, timeout: int=100) -> str:
|
|
99
|
+
self.solver.parameters.max_time_in_seconds = timeout
|
|
100
|
+
self.status = self.solver.Solve(self.model)
|
|
101
|
+
return self.solver.StatusName(self.status)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def status_name(self) -> str:
|
|
105
|
+
return self.solver.StatusName(self.status)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def solution(self) -> dict[tuple[int, int], int]:
|
|
109
|
+
if self.status_name not in ("OPTIMAL", "FEASIBLE"):
|
|
110
|
+
raise ValueError("Problem has not been solved yet")
|
|
111
|
+
|
|
112
|
+
return parse_ubongo_solution(self.solver, self.pieces_ext, self.origin_flags)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def solution_as_str(self, **kwargs) -> str:
|
|
116
|
+
return _ubongo_solution_as_str(self.solution, **kwargs)
|
|
117
|
+
|
|
118
|
+
def print_solution(self):
|
|
119
|
+
print(self.solution_as_str)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def solution_as_fig(self, **kwargs) -> tuple[plt.Figure, plt.Axes]:
|
|
123
|
+
return _ubongo_solution_as_fig(self.solution, **kwargs)
|
|
124
|
+
|
|
125
|
+
def plot_solution(self, **kwargs):
|
|
126
|
+
_ubongo_solution_as_fig(self.solution, **kwargs)
|
|
127
|
+
None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def make_ubongo_problem(pieces: list[Piece], board: Board):
|
|
131
|
+
# Generate a or-tools model that solves the "ubongo" problem
|
|
132
|
+
# i.e. put pieces on the board to fill without overwraps
|
|
133
|
+
|
|
134
|
+
model = cp_model.CpModel()
|
|
135
|
+
|
|
136
|
+
# Make sure the areas of pieces and board are equal
|
|
137
|
+
piece_area = sum(len(p.coordinates) for p in pieces)
|
|
138
|
+
board_area = len(board.coordinates)
|
|
139
|
+
if piece_area != board_area:
|
|
140
|
+
raise ValueError(f"Piece area != board area ({piece_area} vs {board_area})")
|
|
141
|
+
|
|
142
|
+
# Expand the list of pieces by flip and rotation
|
|
143
|
+
pieces_ext = {}
|
|
144
|
+
for id, piece in enumerate(pieces):
|
|
145
|
+
for flip, angle in itertools.product([0, 1], [0, 90, 180, 270]):
|
|
146
|
+
p = piece.rotate(angle) if flip == 0 else piece.flip().rotate(angle)
|
|
147
|
+
key = (id, flip, angle)
|
|
148
|
+
pieces_ext[key] = p
|
|
149
|
+
|
|
150
|
+
# For each (extended) piece, precompute the candidate origin positions
|
|
151
|
+
origin_candidates = {}
|
|
152
|
+
max_x = max(x for x, _ in board.coordinates)
|
|
153
|
+
max_y = max(y for _, y in board.coordinates)
|
|
154
|
+
for key, piece in pieces_ext.items():
|
|
155
|
+
candidates = set()
|
|
156
|
+
# Loop over all origin candidates.
|
|
157
|
+
# Due to the normalization, all piece contains at least one cell with x=0 and
|
|
158
|
+
# at least one cell with y=0.
|
|
159
|
+
# As a result, the origin coordinates must be within the range of the board;
|
|
160
|
+
# It if is, cell (0, y) or (x, 0) would be off-board.
|
|
161
|
+
# Note that the board is also normalized to have min_x = min_y = 0
|
|
162
|
+
for x, y in itertools.product(range(max_x + 1), range(max_y + 1)):
|
|
163
|
+
# Can we put the origin of the piece at (x, y)?
|
|
164
|
+
if all((x + a, y + b) in board.coordinates for a, b in piece.coordinates):
|
|
165
|
+
candidates.add((x, y))
|
|
166
|
+
|
|
167
|
+
if len(candidates) > 0:
|
|
168
|
+
origin_candidates[key] = candidates
|
|
169
|
+
|
|
170
|
+
# filter pieces_ext as per origin_candidates, so we omit orientation
|
|
171
|
+
# that has no position to locate on the board
|
|
172
|
+
pieces_ext = {k: v for k, v in pieces_ext.items() if k in origin_candidates}
|
|
173
|
+
|
|
174
|
+
# Detect edge case: if some piece has no orientation, then the problem is not solvable
|
|
175
|
+
covered_ids = set(id for (id, _, _) in origin_candidates)
|
|
176
|
+
for id, piece in enumerate(pieces):
|
|
177
|
+
if id not in covered_ids:
|
|
178
|
+
raise ValueError(f"Piece {id} cannot be placed anywhere on the board")
|
|
179
|
+
|
|
180
|
+
# Flags that indicate the location of the piece origin
|
|
181
|
+
origin_flags = {
|
|
182
|
+
(id, flip, angle, x, y): model.NewBoolVar(f"origin_{id}_{flip}_{angle}_{x}_{y}")
|
|
183
|
+
for (id, flip, angle) in pieces_ext
|
|
184
|
+
for x, y in origin_candidates[id, flip, angle]
|
|
185
|
+
}
|
|
186
|
+
# For each piece id, we must choose exactly one (orientation, origin) pair
|
|
187
|
+
for id in range(len(pieces)):
|
|
188
|
+
flags = [v for (i, _, _, _, _), v in origin_flags.items() if id == i]
|
|
189
|
+
model.AddExactlyOne(flags)
|
|
190
|
+
|
|
191
|
+
# Pieces cannot overwrap and board must be fully filled
|
|
192
|
+
# To implement this, we first collect, for each cell on the board,
|
|
193
|
+
# all "origin_flags" which would cover the cell when positive.
|
|
194
|
+
# Then, exactly one of the flags must be positive
|
|
195
|
+
# for the cell to be filled with no overwrap
|
|
196
|
+
cell_coverers = {(x, y): [] for x, y in board.coordinates}
|
|
197
|
+
for (id, flip, angle, x, y), flag in origin_flags.items():
|
|
198
|
+
for a, b in pieces_ext[id, flip, angle].coordinates:
|
|
199
|
+
cell_coverers[x+a, y+b].append(flag)
|
|
200
|
+
for flags in cell_coverers.values():
|
|
201
|
+
model.AddExactlyOne(flags)
|
|
202
|
+
|
|
203
|
+
return model, pieces_ext, origin_flags
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def parse_ubongo_solution(
|
|
207
|
+
solver: cp_model.CpSolver,
|
|
208
|
+
pieces_ext: dict[tuple[int, int, int], Piece],
|
|
209
|
+
origin_flags: dict[tuple[int, int, int, int, int], cp_model.IntVar]
|
|
210
|
+
) -> dict[tuple[int, int], int]:
|
|
211
|
+
out = {}
|
|
212
|
+
for (id, flip, angle, x, y), flag in origin_flags.items():
|
|
213
|
+
value = solver.Value(flag)
|
|
214
|
+
if value == 0:
|
|
215
|
+
continue
|
|
216
|
+
piece = pieces_ext[id, flip, angle]
|
|
217
|
+
for a, b in piece.coordinates:
|
|
218
|
+
out[x+a, y+b] = id
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _ubongo_solution_as_str(
|
|
223
|
+
solution: dict[tuple[int, int], int],
|
|
224
|
+
chars: str="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*#@%="
|
|
225
|
+
) -> str:
|
|
226
|
+
max_x = max(x for (x, _) in solution)
|
|
227
|
+
max_y = max(y for (_, y) in solution)
|
|
228
|
+
|
|
229
|
+
# Warning when id is out of prepared characters
|
|
230
|
+
maxid = max(solution.values())
|
|
231
|
+
if maxid > len(chars) - 1:
|
|
232
|
+
warnings.warn("We have more pieces than the predefined characters, so the same characters are used repeatedly")
|
|
233
|
+
while maxid > len(chars) - 1:
|
|
234
|
+
chars += chars
|
|
235
|
+
|
|
236
|
+
board = []
|
|
237
|
+
for _ in range(max_y + 1):
|
|
238
|
+
board.append([-1] * (max_x + 1))
|
|
239
|
+
for (x, y), id in solution.items():
|
|
240
|
+
board[y][x] = id
|
|
241
|
+
|
|
242
|
+
out = "\n".join("".join(chars[c] + " " if c >= 0 else " " for c in row) for row in board)
|
|
243
|
+
return out
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _ubongo_solution_as_fig(
|
|
247
|
+
solution: dict[tuple[int, int], int],
|
|
248
|
+
ax=None,
|
|
249
|
+
cmap_name: str = "tab20",
|
|
250
|
+
figsize: tuple[float, float]=(8, 4),
|
|
251
|
+
add_text: bool = True,
|
|
252
|
+
display: bool = True
|
|
253
|
+
):
|
|
254
|
+
if ax is None:
|
|
255
|
+
fig, ax = plt.subplots(1, 1, figsize=figsize)
|
|
256
|
+
else:
|
|
257
|
+
fig = ax.figure
|
|
258
|
+
|
|
259
|
+
max_x = max(x for (x, _) in solution)
|
|
260
|
+
max_y = max(y for (_, y) in solution)
|
|
261
|
+
piece_ids = sorted(set(solution.values()))
|
|
262
|
+
n_pieces = len(piece_ids)
|
|
263
|
+
|
|
264
|
+
cmap = plt.get_cmap(cmap_name, max(n_pieces, 3))
|
|
265
|
+
color_map = {pid: cmap(i) for i, pid in enumerate(piece_ids)}
|
|
266
|
+
|
|
267
|
+
# Draw filled cells
|
|
268
|
+
for (x, y), pid in solution.items():
|
|
269
|
+
color = color_map[pid]
|
|
270
|
+
rect = patches.Rectangle((x, y), 1, 1, facecolor=color, edgecolor="white", linewidth=0.5)
|
|
271
|
+
ax.add_patch(rect)
|
|
272
|
+
if add_text:
|
|
273
|
+
ax.text(x + 0.5, y + 0.5, str(pid), ha="center", va="center",
|
|
274
|
+
fontsize=9, fontweight="bold", color="black")
|
|
275
|
+
|
|
276
|
+
# Draw thick borders between different pieces
|
|
277
|
+
for (x, y), pid in solution.items():
|
|
278
|
+
# Check each edge: right, top, left, bottom
|
|
279
|
+
for dx, dy, x0, y0, x1, y1 in [
|
|
280
|
+
(1, 0, x+1, y, x+1, y+1), # right
|
|
281
|
+
(0, 1, x, y+1, x+1, y+1), # top
|
|
282
|
+
(-1, 0, x, y, x, y+1), # left
|
|
283
|
+
(0, -1, x, y, x+1, y), # bottom
|
|
284
|
+
]:
|
|
285
|
+
neighbor = (x + dx, y + dy)
|
|
286
|
+
if neighbor not in solution or solution[neighbor] != pid:
|
|
287
|
+
ax.plot([x0, x1], [y0, y1], color="black", linewidth=2)
|
|
288
|
+
|
|
289
|
+
ax.set_xlim(0, max_x + 1)
|
|
290
|
+
ax.set_ylim(0, max_y + 1)
|
|
291
|
+
ax.set_aspect("equal")
|
|
292
|
+
ax.invert_yaxis()
|
|
293
|
+
ax.axis("off")
|
|
294
|
+
fig.tight_layout()
|
|
295
|
+
if not display:
|
|
296
|
+
plt.close(fig) # Quick workaround not to display the graph
|
|
297
|
+
|
|
298
|
+
return fig, ax
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class ubongo:
|
|
302
|
+
class pieces:
|
|
303
|
+
I2 = Piece([(0,0), (1,0)])
|
|
304
|
+
|
|
305
|
+
I3 = Piece([(0,0), (1,0), (2,0)])
|
|
306
|
+
L3 = Piece([(0,0), (1,0), (1,1)])
|
|
307
|
+
|
|
308
|
+
I4 = Piece([(0,0), (1,0), (2,0), (3,0)])
|
|
309
|
+
O4 = Piece([(0,0), (1,0), (0,1), (1,1)])
|
|
310
|
+
T4 = Piece([(0,0), (1,0), (2,0), (1,1)])
|
|
311
|
+
S4 = Piece([(1,0), (2,0), (0,1), (1,1)])
|
|
312
|
+
L4 = Piece([(0,0), (1,0), (2,0), (2,1)])
|
|
313
|
+
|
|
314
|
+
F5 = Piece([(0,0), (1,0), (2,0), (0,1), (1,1)])
|
|
315
|
+
L5 = Piece([(0,0), (1,0), (2,0), (3,0), (3,1)])
|
|
316
|
+
T5 = Piece([(0,0), (1,0), (2,0), (3,0), (1,1)])
|
|
317
|
+
Z5 = Piece([(0,0), (0,1), (1,1), (2,1), (2,2)])
|
|
318
|
+
|
|
319
|
+
_all = [I2, I3, L3, I4, O4, T4, S4, L4, F5, L5, T5, Z5]
|
|
320
|
+
|
|
321
|
+
sample_problems = [
|
|
322
|
+
{
|
|
323
|
+
"pieces": [pieces.T4, pieces.T5, pieces.S4],
|
|
324
|
+
"board": Board([
|
|
325
|
+
(1,0), (2,0),
|
|
326
|
+
(0,1), (1,1), (2,1), (3,1),
|
|
327
|
+
(1,2), (2,2), (3,2), (4,2),
|
|
328
|
+
(1,3), (2,3), (3,3)
|
|
329
|
+
])
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"pieces": [pieces.L3, pieces.I3, pieces.T5, pieces.L5],
|
|
333
|
+
"board": Board([
|
|
334
|
+
(1,0), (2,0),
|
|
335
|
+
(1,1), (2,1), (3,1),
|
|
336
|
+
(1,2), (2,2), (3,2),
|
|
337
|
+
(0,3), (1,3), (2,3), (3,3),
|
|
338
|
+
(0,4), (1,4),
|
|
339
|
+
(0,5), (1,5)
|
|
340
|
+
])
|
|
341
|
+
},
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class whitechocolate:
|
|
346
|
+
# White Chocolate Puzzle
|
|
347
|
+
# https://store.hanayamatoys.co.jp/items/58611532
|
|
348
|
+
# https://www.amazon.co.jp/dp/B0BGSH79VJ
|
|
349
|
+
pieces = [
|
|
350
|
+
Piece([[0,0], [0,1], [0,2], [0,3], [0,4], [1,4]]),
|
|
351
|
+
Piece([[0,0], [0,1], [0,2], [0,3], [1,3], [2,3]]),
|
|
352
|
+
Piece([[0,0], [0,1], [0,2], [1,1], [1,2]]),
|
|
353
|
+
Piece([[0,0], [0,1], [0,2], [0,3], [1,3]]),
|
|
354
|
+
Piece([[0,0], [0,1], [0,2], [1,2]]),
|
|
355
|
+
Piece([[0,0], [0,1], [0,2], [0,3], [1,2], [1,3]]),
|
|
356
|
+
Piece([[0,0], [0,1], [1,1]]),
|
|
357
|
+
Piece([[0,0], [0,1], [0,2], [1,2], [2,2]]),
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
board = Board([(x, y) for x, y in itertools.product(range(8), range(5))])
|
|
361
|
+
|
|
362
|
+
boards = {
|
|
363
|
+
# Basic board of 5 x 8 rectangle
|
|
364
|
+
"basic": Board([(x, y) for x, y in itertools.product(range(8), range(5))])
|
|
365
|
+
|
|
366
|
+
# Split boards
|
|
367
|
+
,"split_1": Board([
|
|
368
|
+
[0,0], [1,0], [2,0], [3,0], [4,0], [5,0], [6,0], [7,0],
|
|
369
|
+
[0,1], [1,1], [2,1], [3,1], [4,1], [5,1], [6,2], [7,2],
|
|
370
|
+
[0,2], [1,2], [2,2], [3,2], [4,3], [5,3], [6,3], [7,3],
|
|
371
|
+
[0,3], [1,3], [2,4], [3,4], [4,4], [5,4], [6,4], [7,4],
|
|
372
|
+
[0,5], [1,5], [2,5], [3,5], [4,5], [5,5], [6,5], [7,5],
|
|
373
|
+
])
|
|
374
|
+
,"split_2": Board([
|
|
375
|
+
[0,0], [1,0], [2,0], [3,0], [4,0], [5,0], [6,0], [8,1],
|
|
376
|
+
[0,1], [1,1], [2,1], [3,1], [5,2], [6,2], [7,2], [8,2],
|
|
377
|
+
[0,2], [1,2], [2,2], [3,2], [5,3], [6,3], [7,3], [8,3],
|
|
378
|
+
[0,3], [1,3], [2,3], [3,3], [5,4], [6,4], [7,4], [8,4],
|
|
379
|
+
[0,4], [2,5], [3,5], [4,5], [5,5], [6,5], [7,5], [8,5],
|
|
380
|
+
])
|
|
381
|
+
|
|
382
|
+
# Challenge boards
|
|
383
|
+
,"challenge_1": Board([
|
|
384
|
+
[0,0], [1,0], [2,0], [3,0],
|
|
385
|
+
[0,1], [1,1], [2,1], [3,1],
|
|
386
|
+
[0,2], [1,2], [2,2], [3,2],
|
|
387
|
+
[0,3], [1,3], [2,3], [3,3], [4,3], [5,3], [6,3],
|
|
388
|
+
[0,4], [1,4], [2,4], [3,4], [4,4], [5,4], [6,4],
|
|
389
|
+
[0,5], [1,5], [2,5], [3,5], [4,5], [5,5], [6,5],
|
|
390
|
+
[0,6], [1,6], [2,6], [3,6], [4,6], [5,6], [6,6],
|
|
391
|
+
])
|
|
392
|
+
,"challenge_2": Board([
|
|
393
|
+
[3,0],
|
|
394
|
+
[1,1], [2,1], [3,1], [4,1], [5,1], [6,1],
|
|
395
|
+
[1,2], [2,2], [3,2], [4,2], [5,2], [6,2],
|
|
396
|
+
[1,3], [2,3], [3,3], [4,3], [5,3], [6,3], [7,3],
|
|
397
|
+
[0,4], [1,4], [2,4], [3,4], [4,4], [5,4], [6,4],
|
|
398
|
+
[1,5], [2,5], [3,5], [4,5], [5,5], [6,5],
|
|
399
|
+
[1,6], [2,6], [3,6], [4,6], [5,6], [6,6],
|
|
400
|
+
[4,7]
|
|
401
|
+
])
|
|
402
|
+
,"challenge_3": Board([
|
|
403
|
+
[4,0], [5,0],
|
|
404
|
+
[4,1], [5,1], [6,1], [7,1],
|
|
405
|
+
[0,2], [1,2], [2,2], [3,2], [6,2], [7,2],
|
|
406
|
+
[0,3], [1,3], [2,3], [3,3],
|
|
407
|
+
[0,4], [1,4], [2,4], [3,4], [4,4], [5,4], [6,4], [7,4],
|
|
408
|
+
[0,5], [1,5], [2,5], [3,5], [4,5], [5,5], [6,5], [7,5],
|
|
409
|
+
[4,6], [5,6], [6,6], [7,6],
|
|
410
|
+
[4,7], [5,7], [6,7], [7,7],
|
|
411
|
+
])
|
|
412
|
+
,"challenge_4": Board([
|
|
413
|
+
[2,0], [3,0],
|
|
414
|
+
[2,1], [3,1],
|
|
415
|
+
[0,2], [1,2], [2,2], [3,2], [4,2], [5,2], [6,2], [7,2],
|
|
416
|
+
[0,3], [1,3], [2,3], [3,3], [4,3], [5,3], [6,3], [7,3],
|
|
417
|
+
[0,5], [1,5], [2,5], [3,5], [4,5], [5,5], [6,5], [7,5],
|
|
418
|
+
[0,6], [1,6], [2,6], [3,6], [4,6], [5,6], [6,6], [7,6],
|
|
419
|
+
[2,7], [3,7],
|
|
420
|
+
[2,8], [3,8],
|
|
421
|
+
])
|
|
422
|
+
,"challenge_5": Board([
|
|
423
|
+
[2,0], [3,0],
|
|
424
|
+
[2,1], [3,1], [4,1], [5,1],
|
|
425
|
+
[1,2], [2,2], [3,2], [4,2], [5,2], [6,2], [7,2],
|
|
426
|
+
[1,3], [2,3], [3,3], [4,3], [5,3], [6,3], [7,3],
|
|
427
|
+
[0,4], [1,4], [2,4], [3,4], [4,4], [5,4], [6,4],
|
|
428
|
+
[0,5], [1,5], [2,5], [3,5], [4,5], [5,5], [6,5],
|
|
429
|
+
[2,6], [3,6], [4,6], [5,6],
|
|
430
|
+
[4,7], [5,7],
|
|
431
|
+
])
|
|
432
|
+
,"challenge_6": Board([
|
|
433
|
+
[4,0], [5,0], [6,0], [7,0],
|
|
434
|
+
[3,1], [4,1], [5,1], [6,1], [7,1], [8,1],
|
|
435
|
+
[2,2], [3,2], [4,2], [5,2], [6,2], [7,2], [8,2], [9,2],
|
|
436
|
+
[1,3], [2,3], [3,3], [4,3], [5,3], [6,3], [7,3], [8,3], [9,3], [10,3],
|
|
437
|
+
[0,4], [1,4], [2,4], [3,4], [4,4], [5,4], [6,4], [7,4], [8,4], [9,4], [10,4], [11,4],
|
|
438
|
+
])
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#%%
|
|
442
|
+
if __name__ == "__main__":
|
|
443
|
+
#%% Baic test
|
|
444
|
+
# Define pieces as sets of (x, y) coordinates
|
|
445
|
+
pieces = [
|
|
446
|
+
Piece([(0,0), (0,1), (0,2), (0,3), (1,3)]), # L-shape
|
|
447
|
+
Piece([(0,0), (1,0), (1,1), (2,1)]), # S-shape
|
|
448
|
+
Piece([(0,0), (0,1), (1,1)]), # mini L-shape
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
# Define a board
|
|
452
|
+
board = Board([(x, y) for x in range(4) for y in range(3)])
|
|
453
|
+
|
|
454
|
+
# Solve
|
|
455
|
+
solver = UbongoPuzzle(pieces, board)
|
|
456
|
+
status = solver.solve()
|
|
457
|
+
print(status) # "OPTIMAL" or "FEASIBLE"
|
|
458
|
+
|
|
459
|
+
# View the solution
|
|
460
|
+
solver.print_solution() # text output
|
|
461
|
+
solver.plot_solution() # matplotlib figure
|
|
462
|
+
|
|
463
|
+
#%% Ubongo sample
|
|
464
|
+
for problem in ubongo.sample_problems:
|
|
465
|
+
p = UbongoPuzzle(problem["pieces"], problem["board"])
|
|
466
|
+
status = p.solve()
|
|
467
|
+
print(status)
|
|
468
|
+
#p.print_solution()
|
|
469
|
+
p.plot_solution()
|
|
470
|
+
|
|
471
|
+
#%% White Chocolate example
|
|
472
|
+
p = UbongoPuzzle(whitechocolate.pieces, whitechocolate.board)
|
|
473
|
+
status = p.solve()
|
|
474
|
+
print(status)
|
|
475
|
+
#p.print_solution()
|
|
476
|
+
p.plot_solution()
|
|
477
|
+
|
|
478
|
+
#%% White Chocolate example, more
|
|
479
|
+
for name, board in whitechocolate.boards.items():
|
|
480
|
+
p = UbongoPuzzle(whitechocolate.pieces, board)
|
|
481
|
+
status = p.solve()
|
|
482
|
+
print(f"{name}: {status}")
|
|
483
|
+
#p.print_solution()
|
|
484
|
+
p.plot_solution()
|
|
485
|
+
print()
|