quantik-core 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.
- quantik_core-0.1.0/LICENSE +21 -0
- quantik_core-0.1.0/PKG-INFO +202 -0
- quantik_core-0.1.0/README.md +157 -0
- quantik_core-0.1.0/pyproject.toml +138 -0
- quantik_core-0.1.0/setup.cfg +4 -0
- quantik_core-0.1.0/src/quantik_core/__init__.py +20 -0
- quantik_core-0.1.0/src/quantik_core/constants.py +0 -0
- quantik_core-0.1.0/src/quantik_core/core.py +174 -0
- quantik_core-0.1.0/src/quantik_core/exceptions.py +0 -0
- quantik_core-0.1.0/src/quantik_core/move.py +0 -0
- quantik_core-0.1.0/src/quantik_core.egg-info/PKG-INFO +202 -0
- quantik_core-0.1.0/src/quantik_core.egg-info/SOURCES.txt +15 -0
- quantik_core-0.1.0/src/quantik_core.egg-info/dependency_links.txt +1 -0
- quantik_core-0.1.0/src/quantik_core.egg-info/requires.txt +22 -0
- quantik_core-0.1.0/src/quantik_core.egg-info/top_level.txt +1 -0
- quantik_core-0.1.0/tests/test_core.py +198 -0
- quantik_core-0.1.0/tests/test_game_state.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mauro Berlanda
|
|
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,202 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quantik-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: High-performance core utilities for Quantik game state manipulation
|
|
5
|
+
Author-email: Mauro Berlanda <mauro.berlanda@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mauroberlanda/quantik-core-py
|
|
8
|
+
Project-URL: Repository, https://github.com/mauroberlanda/quantik-core-py
|
|
9
|
+
Project-URL: Documentation, https://quantik-core-py.readthedocs.io
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/mauroberlanda/quantik-core-py/issues
|
|
11
|
+
Keywords: quantik,board-games,game-ai,monte-carlo,bitboards,game-engine,mcts,game-theory,combinatorial-games
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Games/Entertainment :: Board Games
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
31
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
32
|
+
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
35
|
+
Provides-Extra: cbor
|
|
36
|
+
Requires-Dist: cbor2>=5.4.0; extra == "cbor"
|
|
37
|
+
Provides-Extra: docs
|
|
38
|
+
Requires-Dist: sphinx>=5.0; extra == "docs"
|
|
39
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
|
|
40
|
+
Provides-Extra: benchmark
|
|
41
|
+
Requires-Dist: pytest-benchmark>=4.0; extra == "benchmark"
|
|
42
|
+
Provides-Extra: all
|
|
43
|
+
Requires-Dist: quantik-core[benchmark,cbor,dev,docs]; extra == "all"
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+
# Quantik Core
|
|
47
|
+
|
|
48
|
+
A high-performance Python library for manipulating Quantik game states, optimized for Monte Carlo simulations, game analysis, and AI engines.
|
|
49
|
+
|
|
50
|
+
## What is Quantik?
|
|
51
|
+
|
|
52
|
+
Quantik is an elegant 4×4 abstract strategy game where players compete to complete lines with all four unique shapes.
|
|
53
|
+
|
|
54
|
+
### Game Rules
|
|
55
|
+
|
|
56
|
+
- **Board**: 4×4 grid (16 squares)
|
|
57
|
+
- **Pieces**: 4 different shapes (A, B, C, D) in 2 colors (one per player)
|
|
58
|
+
- **Objective**: Be the first to complete a **row**, **column**, or **2×2 zone** containing all four different shapes
|
|
59
|
+
- **Gameplay**:
|
|
60
|
+
- Players alternate placing one of their remaining pieces on an empty square
|
|
61
|
+
- A piece cannot be placed if the opponent already has the same shape in the target square's row, column, or 2×2 zone
|
|
62
|
+
- Colors don't matter for winning - only the presence of all four shapes in a line
|
|
63
|
+
|
|
64
|
+
### Example Victory
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
A B C D ← Row with all 4 shapes = WIN!
|
|
68
|
+
. . . .
|
|
69
|
+
. . . .
|
|
70
|
+
. . . .
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Features
|
|
74
|
+
|
|
75
|
+
This library provides the core foundation for building:
|
|
76
|
+
|
|
77
|
+
- **Monte Carlo Tree Search (MCTS)** engines
|
|
78
|
+
- **Game analysis** and position evaluation systems
|
|
79
|
+
- **AI training** and recommendation engines
|
|
80
|
+
- **Opening book** generation and endgame databases
|
|
81
|
+
- **Statistical analysis** of game patterns
|
|
82
|
+
- **Game engines** and tournament systems
|
|
83
|
+
- **Research tools** for combinatorial game theory
|
|
84
|
+
|
|
85
|
+
**Current Implementation:**
|
|
86
|
+
- **State Representation**: Complete bitboard-based game state management
|
|
87
|
+
- **Serialization**: Binary, QFEN, and CBOR formats
|
|
88
|
+
- **Canonicalization**: Symmetry-aware position normalization
|
|
89
|
+
- **Move Generation**: Coming in next release
|
|
90
|
+
- **Game Logic**: Win detection and move validation (planned)
|
|
91
|
+
|
|
92
|
+
## Core Capabilities
|
|
93
|
+
|
|
94
|
+
- **Blazing Fast Operations**: Bitboard-based representation enables O(1) move generation and win detection
|
|
95
|
+
- **Compact Memory Footprint**: Game states fit in just 16 bytes with optional 18-byte canonical serialization
|
|
96
|
+
- **Symmetry Normalization**: Automatic canonicalization under rotations, reflections, color swaps, and shape relabeling
|
|
97
|
+
- **Cross-Language Compatibility**: Binary format designed for interoperability with Go, Rust, and other engines
|
|
98
|
+
- **Human-Readable Format**: QFEN (Quantik FEN) notation for debugging and documentation
|
|
99
|
+
- **Self-Describing Serialization**: CBOR-based format for robust data exchange
|
|
100
|
+
|
|
101
|
+
## Installation
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install quantik-core
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Quick Start
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from quantik_core import State
|
|
111
|
+
|
|
112
|
+
# Create an empty game state
|
|
113
|
+
state = State.empty()
|
|
114
|
+
|
|
115
|
+
# Create a position using QFEN notation
|
|
116
|
+
state = State.from_qfen("A.../..b./.c../...D")
|
|
117
|
+
|
|
118
|
+
# Convert to human-readable format
|
|
119
|
+
qfen = state.to_qfen()
|
|
120
|
+
print(f"Position: {qfen}") # Output: A.../..b./.c../...D
|
|
121
|
+
|
|
122
|
+
# Get canonical representation for symmetry analysis
|
|
123
|
+
canonical_key = state.canonical_key()
|
|
124
|
+
print(f"Canonical key: {canonical_key.hex()}")
|
|
125
|
+
|
|
126
|
+
# Serialize to binary format (18 bytes)
|
|
127
|
+
binary_data = state.pack()
|
|
128
|
+
restored_state = State.unpack(binary_data)
|
|
129
|
+
|
|
130
|
+
# Serialize to CBOR for cross-language compatibility
|
|
131
|
+
cbor_data = state.to_cbor(canon=True, meta={"game_id": 123})
|
|
132
|
+
restored_from_cbor = State.from_cbor(cbor_data)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Performance
|
|
136
|
+
|
|
137
|
+
- **State Operations**: Bitboard-based representation enables fast position manipulation
|
|
138
|
+
- **Canonicalization**: <1µs per position with precomputed lookup tables
|
|
139
|
+
- **Memory Usage**: 16 bytes per game state + 1MB for transformation LUTs
|
|
140
|
+
- **Serialization**: 18-byte binary format, human-readable QFEN, or self-describing CBOR
|
|
141
|
+
|
|
142
|
+
## Use Cases
|
|
143
|
+
|
|
144
|
+
### Position Analysis and Canonicalization
|
|
145
|
+
```python
|
|
146
|
+
from quantik_core import State
|
|
147
|
+
|
|
148
|
+
# Create different equivalent positions
|
|
149
|
+
pos1 = State.from_qfen("A.../..../..../....")
|
|
150
|
+
pos2 = State.from_qfen("..../..../..../.a..") # Rotated + color swapped
|
|
151
|
+
|
|
152
|
+
# Both have the same canonical representation
|
|
153
|
+
assert pos1.canonical_key() == pos2.canonical_key()
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Database Storage and Retrieval
|
|
157
|
+
```python
|
|
158
|
+
# Use canonical keys as database indices
|
|
159
|
+
positions_db = {}
|
|
160
|
+
canonical_key = state.canonical_key()
|
|
161
|
+
positions_db[canonical_key] = {"eval": 0.75, "visits": 1000}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Cross-Language Data Exchange
|
|
165
|
+
```python
|
|
166
|
+
# Save position with metadata for other engines
|
|
167
|
+
data = state.to_cbor(
|
|
168
|
+
canon=True,
|
|
169
|
+
mc=5000, # Monte Carlo simulations
|
|
170
|
+
meta={"depth": 12, "engine": "quantik-py-v1"}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Binary format for high-performance applications
|
|
174
|
+
binary = state.pack() # Just 18 bytes
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Technical Details
|
|
178
|
+
|
|
179
|
+
- **Representation**: 8 disjoint 16-bit bitboards (one per color-shape combination)
|
|
180
|
+
- **Symmetries**: Dihedral group D4 (8 rotations/reflections) × color swap × shape permutations = 384 total
|
|
181
|
+
- **Serialization**: Versioned binary format with little-endian 16-bit words
|
|
182
|
+
- **Canonicalization**: Lexicographically minimal representation across symmetry orbit
|
|
183
|
+
|
|
184
|
+
## Contributing
|
|
185
|
+
|
|
186
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
191
|
+
|
|
192
|
+
## Citation
|
|
193
|
+
|
|
194
|
+
If you use this library in research, please cite:
|
|
195
|
+
```bibtex
|
|
196
|
+
@software{quantik_core,
|
|
197
|
+
title={Quantik Core: High-Performance Game State Manipulation},
|
|
198
|
+
author={Mauro Berlanda},
|
|
199
|
+
year={2025},
|
|
200
|
+
url={https://github.com/mberlanda/quantik-core-py}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Quantik Core
|
|
2
|
+
|
|
3
|
+
A high-performance Python library for manipulating Quantik game states, optimized for Monte Carlo simulations, game analysis, and AI engines.
|
|
4
|
+
|
|
5
|
+
## What is Quantik?
|
|
6
|
+
|
|
7
|
+
Quantik is an elegant 4×4 abstract strategy game where players compete to complete lines with all four unique shapes.
|
|
8
|
+
|
|
9
|
+
### Game Rules
|
|
10
|
+
|
|
11
|
+
- **Board**: 4×4 grid (16 squares)
|
|
12
|
+
- **Pieces**: 4 different shapes (A, B, C, D) in 2 colors (one per player)
|
|
13
|
+
- **Objective**: Be the first to complete a **row**, **column**, or **2×2 zone** containing all four different shapes
|
|
14
|
+
- **Gameplay**:
|
|
15
|
+
- Players alternate placing one of their remaining pieces on an empty square
|
|
16
|
+
- A piece cannot be placed if the opponent already has the same shape in the target square's row, column, or 2×2 zone
|
|
17
|
+
- Colors don't matter for winning - only the presence of all four shapes in a line
|
|
18
|
+
|
|
19
|
+
### Example Victory
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
A B C D ← Row with all 4 shapes = WIN!
|
|
23
|
+
. . . .
|
|
24
|
+
. . . .
|
|
25
|
+
. . . .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
This library provides the core foundation for building:
|
|
31
|
+
|
|
32
|
+
- **Monte Carlo Tree Search (MCTS)** engines
|
|
33
|
+
- **Game analysis** and position evaluation systems
|
|
34
|
+
- **AI training** and recommendation engines
|
|
35
|
+
- **Opening book** generation and endgame databases
|
|
36
|
+
- **Statistical analysis** of game patterns
|
|
37
|
+
- **Game engines** and tournament systems
|
|
38
|
+
- **Research tools** for combinatorial game theory
|
|
39
|
+
|
|
40
|
+
**Current Implementation:**
|
|
41
|
+
- **State Representation**: Complete bitboard-based game state management
|
|
42
|
+
- **Serialization**: Binary, QFEN, and CBOR formats
|
|
43
|
+
- **Canonicalization**: Symmetry-aware position normalization
|
|
44
|
+
- **Move Generation**: Coming in next release
|
|
45
|
+
- **Game Logic**: Win detection and move validation (planned)
|
|
46
|
+
|
|
47
|
+
## Core Capabilities
|
|
48
|
+
|
|
49
|
+
- **Blazing Fast Operations**: Bitboard-based representation enables O(1) move generation and win detection
|
|
50
|
+
- **Compact Memory Footprint**: Game states fit in just 16 bytes with optional 18-byte canonical serialization
|
|
51
|
+
- **Symmetry Normalization**: Automatic canonicalization under rotations, reflections, color swaps, and shape relabeling
|
|
52
|
+
- **Cross-Language Compatibility**: Binary format designed for interoperability with Go, Rust, and other engines
|
|
53
|
+
- **Human-Readable Format**: QFEN (Quantik FEN) notation for debugging and documentation
|
|
54
|
+
- **Self-Describing Serialization**: CBOR-based format for robust data exchange
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install quantik-core
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from quantik_core import State
|
|
66
|
+
|
|
67
|
+
# Create an empty game state
|
|
68
|
+
state = State.empty()
|
|
69
|
+
|
|
70
|
+
# Create a position using QFEN notation
|
|
71
|
+
state = State.from_qfen("A.../..b./.c../...D")
|
|
72
|
+
|
|
73
|
+
# Convert to human-readable format
|
|
74
|
+
qfen = state.to_qfen()
|
|
75
|
+
print(f"Position: {qfen}") # Output: A.../..b./.c../...D
|
|
76
|
+
|
|
77
|
+
# Get canonical representation for symmetry analysis
|
|
78
|
+
canonical_key = state.canonical_key()
|
|
79
|
+
print(f"Canonical key: {canonical_key.hex()}")
|
|
80
|
+
|
|
81
|
+
# Serialize to binary format (18 bytes)
|
|
82
|
+
binary_data = state.pack()
|
|
83
|
+
restored_state = State.unpack(binary_data)
|
|
84
|
+
|
|
85
|
+
# Serialize to CBOR for cross-language compatibility
|
|
86
|
+
cbor_data = state.to_cbor(canon=True, meta={"game_id": 123})
|
|
87
|
+
restored_from_cbor = State.from_cbor(cbor_data)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Performance
|
|
91
|
+
|
|
92
|
+
- **State Operations**: Bitboard-based representation enables fast position manipulation
|
|
93
|
+
- **Canonicalization**: <1µs per position with precomputed lookup tables
|
|
94
|
+
- **Memory Usage**: 16 bytes per game state + 1MB for transformation LUTs
|
|
95
|
+
- **Serialization**: 18-byte binary format, human-readable QFEN, or self-describing CBOR
|
|
96
|
+
|
|
97
|
+
## Use Cases
|
|
98
|
+
|
|
99
|
+
### Position Analysis and Canonicalization
|
|
100
|
+
```python
|
|
101
|
+
from quantik_core import State
|
|
102
|
+
|
|
103
|
+
# Create different equivalent positions
|
|
104
|
+
pos1 = State.from_qfen("A.../..../..../....")
|
|
105
|
+
pos2 = State.from_qfen("..../..../..../.a..") # Rotated + color swapped
|
|
106
|
+
|
|
107
|
+
# Both have the same canonical representation
|
|
108
|
+
assert pos1.canonical_key() == pos2.canonical_key()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Database Storage and Retrieval
|
|
112
|
+
```python
|
|
113
|
+
# Use canonical keys as database indices
|
|
114
|
+
positions_db = {}
|
|
115
|
+
canonical_key = state.canonical_key()
|
|
116
|
+
positions_db[canonical_key] = {"eval": 0.75, "visits": 1000}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Cross-Language Data Exchange
|
|
120
|
+
```python
|
|
121
|
+
# Save position with metadata for other engines
|
|
122
|
+
data = state.to_cbor(
|
|
123
|
+
canon=True,
|
|
124
|
+
mc=5000, # Monte Carlo simulations
|
|
125
|
+
meta={"depth": 12, "engine": "quantik-py-v1"}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Binary format for high-performance applications
|
|
129
|
+
binary = state.pack() # Just 18 bytes
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Technical Details
|
|
133
|
+
|
|
134
|
+
- **Representation**: 8 disjoint 16-bit bitboards (one per color-shape combination)
|
|
135
|
+
- **Symmetries**: Dihedral group D4 (8 rotations/reflections) × color swap × shape permutations = 384 total
|
|
136
|
+
- **Serialization**: Versioned binary format with little-endian 16-bit words
|
|
137
|
+
- **Canonicalization**: Lexicographically minimal representation across symmetry orbit
|
|
138
|
+
|
|
139
|
+
## Contributing
|
|
140
|
+
|
|
141
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
146
|
+
|
|
147
|
+
## Citation
|
|
148
|
+
|
|
149
|
+
If you use this library in research, please cite:
|
|
150
|
+
```bibtex
|
|
151
|
+
@software{quantik_core,
|
|
152
|
+
title={Quantik Core: High-Performance Game State Manipulation},
|
|
153
|
+
author={Mauro Berlanda},
|
|
154
|
+
year={2025},
|
|
155
|
+
url={https://github.com/mberlanda/quantik-core-py}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "quantik-core"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "High-performance core utilities for Quantik game state manipulation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Mauro Berlanda", email = "mauro.berlanda@gmail.com"}
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Games/Entertainment :: Board Games",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
keywords = [
|
|
29
|
+
"quantik", "board-games", "game-ai", "monte-carlo", "bitboards",
|
|
30
|
+
"game-engine", "mcts", "game-theory", "combinatorial-games"
|
|
31
|
+
]
|
|
32
|
+
requires-python = ">=3.9"
|
|
33
|
+
dependencies = []
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=7.0",
|
|
38
|
+
"pytest-cov>=4.0",
|
|
39
|
+
"hypothesis>=6.0",
|
|
40
|
+
"black>=23.0",
|
|
41
|
+
"flake8>=6.0",
|
|
42
|
+
"mypy>=1.0",
|
|
43
|
+
"pre-commit>=3.0",
|
|
44
|
+
]
|
|
45
|
+
cbor = [
|
|
46
|
+
"cbor2>=5.4.0",
|
|
47
|
+
]
|
|
48
|
+
docs = [
|
|
49
|
+
"sphinx>=5.0",
|
|
50
|
+
"sphinx-rtd-theme>=1.0",
|
|
51
|
+
]
|
|
52
|
+
benchmark = [
|
|
53
|
+
"pytest-benchmark>=4.0",
|
|
54
|
+
]
|
|
55
|
+
all = [
|
|
56
|
+
"quantik-core[dev,cbor,docs,benchmark]",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[project.urls]
|
|
60
|
+
Homepage = "https://github.com/mauroberlanda/quantik-core-py"
|
|
61
|
+
Repository = "https://github.com/mauroberlanda/quantik-core-py"
|
|
62
|
+
Documentation = "https://quantik-core-py.readthedocs.io"
|
|
63
|
+
"Bug Tracker" = "https://github.com/mauroberlanda/quantik-core-py/issues"
|
|
64
|
+
|
|
65
|
+
[tool.setuptools.packages.find]
|
|
66
|
+
where = ["src"]
|
|
67
|
+
|
|
68
|
+
[tool.setuptools.package-data]
|
|
69
|
+
quantik_core = ["py.typed"]
|
|
70
|
+
|
|
71
|
+
[tool.black]
|
|
72
|
+
line-length = 88
|
|
73
|
+
target-version = ['py39']
|
|
74
|
+
include = '\.pyi?$'
|
|
75
|
+
extend-exclude = '''
|
|
76
|
+
/(
|
|
77
|
+
# directories
|
|
78
|
+
\.eggs
|
|
79
|
+
| \.git
|
|
80
|
+
| \.hg
|
|
81
|
+
| \.mypy_cache
|
|
82
|
+
| \.tox
|
|
83
|
+
| \.venv
|
|
84
|
+
| build
|
|
85
|
+
| dist
|
|
86
|
+
)/
|
|
87
|
+
'''
|
|
88
|
+
|
|
89
|
+
[tool.mypy]
|
|
90
|
+
python_version = "3.9"
|
|
91
|
+
warn_return_any = true
|
|
92
|
+
warn_unused_configs = true
|
|
93
|
+
disallow_untyped_defs = true
|
|
94
|
+
disallow_incomplete_defs = true
|
|
95
|
+
check_untyped_defs = true
|
|
96
|
+
disallow_untyped_decorators = true
|
|
97
|
+
no_implicit_optional = true
|
|
98
|
+
warn_redundant_casts = true
|
|
99
|
+
warn_unused_ignores = true
|
|
100
|
+
warn_no_return = true
|
|
101
|
+
warn_unreachable = true
|
|
102
|
+
strict_equality = true
|
|
103
|
+
|
|
104
|
+
[tool.pytest.ini_options]
|
|
105
|
+
testpaths = ["tests"]
|
|
106
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
107
|
+
python_classes = ["Test*"]
|
|
108
|
+
python_functions = ["test_*"]
|
|
109
|
+
addopts = [
|
|
110
|
+
"--strict-markers",
|
|
111
|
+
"--strict-config",
|
|
112
|
+
"--cov=quantik_core",
|
|
113
|
+
"--cov-report=term-missing",
|
|
114
|
+
"--cov-report=html",
|
|
115
|
+
"--cov-fail-under=90",
|
|
116
|
+
]
|
|
117
|
+
markers = [
|
|
118
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
119
|
+
"benchmark: marks tests as benchmarks",
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
[tool.coverage.run]
|
|
123
|
+
source = ["src/quantik_core"]
|
|
124
|
+
omit = ["*/tests/*", "*/benchmarks/*"]
|
|
125
|
+
|
|
126
|
+
[tool.coverage.report]
|
|
127
|
+
exclude_lines = [
|
|
128
|
+
"pragma: no cover",
|
|
129
|
+
"def __repr__",
|
|
130
|
+
"if self.debug:",
|
|
131
|
+
"if settings.DEBUG",
|
|
132
|
+
"raise AssertionError",
|
|
133
|
+
"raise NotImplementedError",
|
|
134
|
+
"if 0:",
|
|
135
|
+
"if __name__ == .__main__.:",
|
|
136
|
+
"class .*\\bProtocol\\):",
|
|
137
|
+
"@(abc\\.)?abstractmethod",
|
|
138
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantik Core - High-performance game state manipulation library.
|
|
3
|
+
|
|
4
|
+
This library provides the foundational components for building Quantik game engines,
|
|
5
|
+
Monte Carlo simulations, and AI analysis tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .core import State, VERSION, FLAG_CANON, D4, permute16, ALL_SHAPE_PERMS
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__author__ = "Mauro Berlanda"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"State",
|
|
15
|
+
"VERSION",
|
|
16
|
+
"FLAG_CANON",
|
|
17
|
+
"D4",
|
|
18
|
+
"permute16",
|
|
19
|
+
"ALL_SHAPE_PERMS",
|
|
20
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Tuple
|
|
3
|
+
import itertools
|
|
4
|
+
import struct
|
|
5
|
+
|
|
6
|
+
# --- versioning/flags --------------------------------------------------------
|
|
7
|
+
VERSION = 1
|
|
8
|
+
FLAG_CANON = 1 << 1 # bit1
|
|
9
|
+
|
|
10
|
+
# --- board indexing ----------------------------------------------------------
|
|
11
|
+
def rc_to_i(r: int, c: int) -> int: return r * 4 + c
|
|
12
|
+
def i_to_rc(i: int) -> Tuple[int, int]: return divmod(i, 4)
|
|
13
|
+
|
|
14
|
+
def build_perm(fn):
|
|
15
|
+
m = [0]*16
|
|
16
|
+
for i in range(16):
|
|
17
|
+
r, c = i_to_rc(i)
|
|
18
|
+
r2, c2 = fn(r, c)
|
|
19
|
+
m[i] = rc_to_i(r2, c2)
|
|
20
|
+
return m
|
|
21
|
+
|
|
22
|
+
# 8 D4 symmetries
|
|
23
|
+
D4 = [
|
|
24
|
+
("id", build_perm(lambda r,c:(r, c ))),
|
|
25
|
+
("rot90", build_perm(lambda r,c:(c, 3-r))),
|
|
26
|
+
("rot180", build_perm(lambda r,c:(3-r, 3-c))),
|
|
27
|
+
("rot270", build_perm(lambda r,c:(3-c, r ))),
|
|
28
|
+
("reflV", build_perm(lambda r,c:(r, 3-c))),
|
|
29
|
+
("reflH", build_perm(lambda r,c:(3-r, c ))),
|
|
30
|
+
("reflD", build_perm(lambda r,c:(c, r ))),
|
|
31
|
+
("reflAD", build_perm(lambda r,c:(3-c, 3-r))),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# --- precomputed LUT: 8 × 65,536 --------------------------------------------
|
|
35
|
+
# perm16[S][mask] -> transformed 16-bit mask
|
|
36
|
+
def _build_perm16_lut() -> List[List[int]]:
|
|
37
|
+
tables: List[List[int]] = []
|
|
38
|
+
for _, mapping in D4:
|
|
39
|
+
t = [0]*65536
|
|
40
|
+
for x in range(65536):
|
|
41
|
+
y = 0
|
|
42
|
+
# scatter bits by mapping[i] in tight loop
|
|
43
|
+
m = x
|
|
44
|
+
i = 0
|
|
45
|
+
while m:
|
|
46
|
+
if m & 1:
|
|
47
|
+
y |= 1 << mapping[i]
|
|
48
|
+
i += 1
|
|
49
|
+
m >>= 1
|
|
50
|
+
# finish remaining zeros if any
|
|
51
|
+
while i < 16:
|
|
52
|
+
# (no-op; just advance)
|
|
53
|
+
i += 1
|
|
54
|
+
t[x] = y
|
|
55
|
+
tables.append(t)
|
|
56
|
+
return tables
|
|
57
|
+
|
|
58
|
+
_perm16 = _build_perm16_lut() # ~1.0 MB RAM, fast and worth it
|
|
59
|
+
|
|
60
|
+
# Convenience function for external use
|
|
61
|
+
def permute16(mask: int, mapping: List[int]) -> int:
|
|
62
|
+
"""Apply a 16-element permutation to a 16-bit mask."""
|
|
63
|
+
result = 0
|
|
64
|
+
for i in range(16):
|
|
65
|
+
if (mask >> i) & 1:
|
|
66
|
+
result |= 1 << mapping[i]
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
ALL_SHAPE_PERMS = list(itertools.permutations(range(4))) # 24 tuples
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class State:
|
|
73
|
+
# bitboards in order C0S0..C0S3, C1S0..C1S3 (each uint16)
|
|
74
|
+
bb: Tuple[int, int, int, int, int, int, int, int]
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def empty(): return State((0,0,0,0,0,0,0,0))
|
|
78
|
+
|
|
79
|
+
# ----- binary core (18 bytes: B B 8H) ------------------------------------
|
|
80
|
+
def pack(self, flags: int = 0) -> bytes:
|
|
81
|
+
return struct.pack("<BB8H", VERSION, flags, *self.bb)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def unpack(data: bytes) -> "State":
|
|
85
|
+
if len(data) < 18: raise ValueError("Buffer too small for v1 core (18 bytes).")
|
|
86
|
+
ver, flags, *rest = struct.unpack("<BB8H", data[:18])
|
|
87
|
+
if ver != VERSION: raise ValueError(f"Unsupported version {ver}")
|
|
88
|
+
bb = tuple(int(x) & 0xFFFF for x in rest)
|
|
89
|
+
return State(bb) # flags ignored in state; carried in header
|
|
90
|
+
|
|
91
|
+
# ----- human-friendly (QFEN) ---------------------------------------------
|
|
92
|
+
SHAPE_LETTERS = "ABCD"
|
|
93
|
+
|
|
94
|
+
def to_qfen(self) -> str:
|
|
95
|
+
grid = []
|
|
96
|
+
for r in range(4):
|
|
97
|
+
row = []
|
|
98
|
+
for c in range(4):
|
|
99
|
+
i = rc_to_i(r,c)
|
|
100
|
+
ch = "."
|
|
101
|
+
for color in (0,1):
|
|
102
|
+
for s in range(4):
|
|
103
|
+
if (self.bb[color*4 + s] >> i) & 1:
|
|
104
|
+
letter = State.SHAPE_LETTERS[s]
|
|
105
|
+
ch = letter if color == 0 else letter.lower()
|
|
106
|
+
row.append(ch)
|
|
107
|
+
grid.append("".join(row))
|
|
108
|
+
return "/".join(grid)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def from_qfen(qfen: str) -> "State":
|
|
112
|
+
parts = [p.strip() for p in qfen.replace(" ", "").split("/")]
|
|
113
|
+
if len(parts) != 4 or any(len(p) != 4 for p in parts):
|
|
114
|
+
raise ValueError("QFEN must be 4 ranks of 4 chars separated by '/'")
|
|
115
|
+
bb = [0]*8
|
|
116
|
+
letter_to_shape = {ch:i for i,ch in enumerate(State.SHAPE_LETTERS)}
|
|
117
|
+
for r in range(4):
|
|
118
|
+
for c in range(4):
|
|
119
|
+
ch = parts[r][c]
|
|
120
|
+
if ch == ".": continue
|
|
121
|
+
color = 0 if ch.isupper() else 1
|
|
122
|
+
s = letter_to_shape[ch.upper()]
|
|
123
|
+
bb[color*4 + s] |= 1 << rc_to_i(r,c)
|
|
124
|
+
return State(tuple(bb))
|
|
125
|
+
|
|
126
|
+
# ----- canonicalization (uses LUT) ---------------------------------------
|
|
127
|
+
def canonical_payload(self) -> bytes:
|
|
128
|
+
best = None
|
|
129
|
+
B = [[self.bb[c*4 + s] for s in range(4)] for c in range(2)]
|
|
130
|
+
for s_idx, _ in enumerate(D4):
|
|
131
|
+
lut = _perm16[s_idx]
|
|
132
|
+
# geometry
|
|
133
|
+
G0 = [lut[B[0][s]] for s in range(4)]
|
|
134
|
+
G1 = [lut[B[1][s]] for s in range(4)]
|
|
135
|
+
for color_swap in (0,1):
|
|
136
|
+
C0, C1 = (G0, G1) if color_swap == 0 else (G1, G0)
|
|
137
|
+
for perm in ALL_SHAPE_PERMS:
|
|
138
|
+
flat = [C0[perm[0]], C0[perm[1]], C0[perm[2]], C0[perm[3]],
|
|
139
|
+
C1[perm[0]], C1[perm[1]], C1[perm[2]], C1[perm[3]]]
|
|
140
|
+
candidate = struct.pack("<8H", *flat)
|
|
141
|
+
if best is None or candidate < best:
|
|
142
|
+
best = candidate
|
|
143
|
+
return best # 16 bytes
|
|
144
|
+
|
|
145
|
+
def canonical_key(self) -> bytes:
|
|
146
|
+
return bytes([VERSION, FLAG_CANON]) + self.canonical_payload()
|
|
147
|
+
|
|
148
|
+
# ----- CBOR wrappers (portable, self-describing) -------------------------
|
|
149
|
+
# { "v":1, "canon":bool, "bb": h'16bytes', ? "mc":uint, ? "meta":{...} }
|
|
150
|
+
def to_cbor(self, canon: bool = False, mc: int | None = None, meta: dict | None = None) -> bytes:
|
|
151
|
+
try:
|
|
152
|
+
import cbor2 # pip install cbor2
|
|
153
|
+
except ImportError:
|
|
154
|
+
raise RuntimeError("Please install cbor2 (pip install cbor2)")
|
|
155
|
+
payload = struct.pack("<8H", *self.bb)
|
|
156
|
+
m = {"v": VERSION, "canon": bool(canon), "bb": payload}
|
|
157
|
+
if mc is not None: m["mc"] = int(mc)
|
|
158
|
+
if meta: m["meta"] = meta
|
|
159
|
+
return cbor2.dumps(m)
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def from_cbor(data: bytes) -> "State":
|
|
163
|
+
try:
|
|
164
|
+
import cbor2
|
|
165
|
+
except ImportError:
|
|
166
|
+
raise RuntimeError("Please install cbor2 (pip install cbor2)")
|
|
167
|
+
m = cbor2.loads(data)
|
|
168
|
+
if m.get("v") != VERSION: raise ValueError("Unsupported CBOR version")
|
|
169
|
+
bb = m.get("bb")
|
|
170
|
+
if not isinstance(bb, (bytes, bytearray)) or len(bb) != 16:
|
|
171
|
+
raise ValueError("CBOR field 'bb' must be 16 bytes")
|
|
172
|
+
vals = struct.unpack("<8H", bb)
|
|
173
|
+
return State(tuple(int(x) & 0xFFFF for x in vals))
|
|
174
|
+
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quantik-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: High-performance core utilities for Quantik game state manipulation
|
|
5
|
+
Author-email: Mauro Berlanda <mauro.berlanda@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mauroberlanda/quantik-core-py
|
|
8
|
+
Project-URL: Repository, https://github.com/mauroberlanda/quantik-core-py
|
|
9
|
+
Project-URL: Documentation, https://quantik-core-py.readthedocs.io
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/mauroberlanda/quantik-core-py/issues
|
|
11
|
+
Keywords: quantik,board-games,game-ai,monte-carlo,bitboards,game-engine,mcts,game-theory,combinatorial-games
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Games/Entertainment :: Board Games
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
31
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
32
|
+
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
35
|
+
Provides-Extra: cbor
|
|
36
|
+
Requires-Dist: cbor2>=5.4.0; extra == "cbor"
|
|
37
|
+
Provides-Extra: docs
|
|
38
|
+
Requires-Dist: sphinx>=5.0; extra == "docs"
|
|
39
|
+
Requires-Dist: sphinx-rtd-theme>=1.0; extra == "docs"
|
|
40
|
+
Provides-Extra: benchmark
|
|
41
|
+
Requires-Dist: pytest-benchmark>=4.0; extra == "benchmark"
|
|
42
|
+
Provides-Extra: all
|
|
43
|
+
Requires-Dist: quantik-core[benchmark,cbor,dev,docs]; extra == "all"
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+
# Quantik Core
|
|
47
|
+
|
|
48
|
+
A high-performance Python library for manipulating Quantik game states, optimized for Monte Carlo simulations, game analysis, and AI engines.
|
|
49
|
+
|
|
50
|
+
## What is Quantik?
|
|
51
|
+
|
|
52
|
+
Quantik is an elegant 4×4 abstract strategy game where players compete to complete lines with all four unique shapes.
|
|
53
|
+
|
|
54
|
+
### Game Rules
|
|
55
|
+
|
|
56
|
+
- **Board**: 4×4 grid (16 squares)
|
|
57
|
+
- **Pieces**: 4 different shapes (A, B, C, D) in 2 colors (one per player)
|
|
58
|
+
- **Objective**: Be the first to complete a **row**, **column**, or **2×2 zone** containing all four different shapes
|
|
59
|
+
- **Gameplay**:
|
|
60
|
+
- Players alternate placing one of their remaining pieces on an empty square
|
|
61
|
+
- A piece cannot be placed if the opponent already has the same shape in the target square's row, column, or 2×2 zone
|
|
62
|
+
- Colors don't matter for winning - only the presence of all four shapes in a line
|
|
63
|
+
|
|
64
|
+
### Example Victory
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
A B C D ← Row with all 4 shapes = WIN!
|
|
68
|
+
. . . .
|
|
69
|
+
. . . .
|
|
70
|
+
. . . .
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Features
|
|
74
|
+
|
|
75
|
+
This library provides the core foundation for building:
|
|
76
|
+
|
|
77
|
+
- **Monte Carlo Tree Search (MCTS)** engines
|
|
78
|
+
- **Game analysis** and position evaluation systems
|
|
79
|
+
- **AI training** and recommendation engines
|
|
80
|
+
- **Opening book** generation and endgame databases
|
|
81
|
+
- **Statistical analysis** of game patterns
|
|
82
|
+
- **Game engines** and tournament systems
|
|
83
|
+
- **Research tools** for combinatorial game theory
|
|
84
|
+
|
|
85
|
+
**Current Implementation:**
|
|
86
|
+
- **State Representation**: Complete bitboard-based game state management
|
|
87
|
+
- **Serialization**: Binary, QFEN, and CBOR formats
|
|
88
|
+
- **Canonicalization**: Symmetry-aware position normalization
|
|
89
|
+
- **Move Generation**: Coming in next release
|
|
90
|
+
- **Game Logic**: Win detection and move validation (planned)
|
|
91
|
+
|
|
92
|
+
## Core Capabilities
|
|
93
|
+
|
|
94
|
+
- **Blazing Fast Operations**: Bitboard-based representation enables O(1) move generation and win detection
|
|
95
|
+
- **Compact Memory Footprint**: Game states fit in just 16 bytes with optional 18-byte canonical serialization
|
|
96
|
+
- **Symmetry Normalization**: Automatic canonicalization under rotations, reflections, color swaps, and shape relabeling
|
|
97
|
+
- **Cross-Language Compatibility**: Binary format designed for interoperability with Go, Rust, and other engines
|
|
98
|
+
- **Human-Readable Format**: QFEN (Quantik FEN) notation for debugging and documentation
|
|
99
|
+
- **Self-Describing Serialization**: CBOR-based format for robust data exchange
|
|
100
|
+
|
|
101
|
+
## Installation
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install quantik-core
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Quick Start
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from quantik_core import State
|
|
111
|
+
|
|
112
|
+
# Create an empty game state
|
|
113
|
+
state = State.empty()
|
|
114
|
+
|
|
115
|
+
# Create a position using QFEN notation
|
|
116
|
+
state = State.from_qfen("A.../..b./.c../...D")
|
|
117
|
+
|
|
118
|
+
# Convert to human-readable format
|
|
119
|
+
qfen = state.to_qfen()
|
|
120
|
+
print(f"Position: {qfen}") # Output: A.../..b./.c../...D
|
|
121
|
+
|
|
122
|
+
# Get canonical representation for symmetry analysis
|
|
123
|
+
canonical_key = state.canonical_key()
|
|
124
|
+
print(f"Canonical key: {canonical_key.hex()}")
|
|
125
|
+
|
|
126
|
+
# Serialize to binary format (18 bytes)
|
|
127
|
+
binary_data = state.pack()
|
|
128
|
+
restored_state = State.unpack(binary_data)
|
|
129
|
+
|
|
130
|
+
# Serialize to CBOR for cross-language compatibility
|
|
131
|
+
cbor_data = state.to_cbor(canon=True, meta={"game_id": 123})
|
|
132
|
+
restored_from_cbor = State.from_cbor(cbor_data)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Performance
|
|
136
|
+
|
|
137
|
+
- **State Operations**: Bitboard-based representation enables fast position manipulation
|
|
138
|
+
- **Canonicalization**: <1µs per position with precomputed lookup tables
|
|
139
|
+
- **Memory Usage**: 16 bytes per game state + 1MB for transformation LUTs
|
|
140
|
+
- **Serialization**: 18-byte binary format, human-readable QFEN, or self-describing CBOR
|
|
141
|
+
|
|
142
|
+
## Use Cases
|
|
143
|
+
|
|
144
|
+
### Position Analysis and Canonicalization
|
|
145
|
+
```python
|
|
146
|
+
from quantik_core import State
|
|
147
|
+
|
|
148
|
+
# Create different equivalent positions
|
|
149
|
+
pos1 = State.from_qfen("A.../..../..../....")
|
|
150
|
+
pos2 = State.from_qfen("..../..../..../.a..") # Rotated + color swapped
|
|
151
|
+
|
|
152
|
+
# Both have the same canonical representation
|
|
153
|
+
assert pos1.canonical_key() == pos2.canonical_key()
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Database Storage and Retrieval
|
|
157
|
+
```python
|
|
158
|
+
# Use canonical keys as database indices
|
|
159
|
+
positions_db = {}
|
|
160
|
+
canonical_key = state.canonical_key()
|
|
161
|
+
positions_db[canonical_key] = {"eval": 0.75, "visits": 1000}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Cross-Language Data Exchange
|
|
165
|
+
```python
|
|
166
|
+
# Save position with metadata for other engines
|
|
167
|
+
data = state.to_cbor(
|
|
168
|
+
canon=True,
|
|
169
|
+
mc=5000, # Monte Carlo simulations
|
|
170
|
+
meta={"depth": 12, "engine": "quantik-py-v1"}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Binary format for high-performance applications
|
|
174
|
+
binary = state.pack() # Just 18 bytes
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Technical Details
|
|
178
|
+
|
|
179
|
+
- **Representation**: 8 disjoint 16-bit bitboards (one per color-shape combination)
|
|
180
|
+
- **Symmetries**: Dihedral group D4 (8 rotations/reflections) × color swap × shape permutations = 384 total
|
|
181
|
+
- **Serialization**: Versioned binary format with little-endian 16-bit words
|
|
182
|
+
- **Canonicalization**: Lexicographically minimal representation across symmetry orbit
|
|
183
|
+
|
|
184
|
+
## Contributing
|
|
185
|
+
|
|
186
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
191
|
+
|
|
192
|
+
## Citation
|
|
193
|
+
|
|
194
|
+
If you use this library in research, please cite:
|
|
195
|
+
```bibtex
|
|
196
|
+
@software{quantik_core,
|
|
197
|
+
title={Quantik Core: High-Performance Game State Manipulation},
|
|
198
|
+
author={Mauro Berlanda},
|
|
199
|
+
year={2025},
|
|
200
|
+
url={https://github.com/mberlanda/quantik-core-py}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/quantik_core/__init__.py
|
|
5
|
+
src/quantik_core/constants.py
|
|
6
|
+
src/quantik_core/core.py
|
|
7
|
+
src/quantik_core/exceptions.py
|
|
8
|
+
src/quantik_core/move.py
|
|
9
|
+
src/quantik_core.egg-info/PKG-INFO
|
|
10
|
+
src/quantik_core.egg-info/SOURCES.txt
|
|
11
|
+
src/quantik_core.egg-info/dependency_links.txt
|
|
12
|
+
src/quantik_core.egg-info/requires.txt
|
|
13
|
+
src/quantik_core.egg-info/top_level.txt
|
|
14
|
+
tests/test_core.py
|
|
15
|
+
tests/test_game_state.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
[all]
|
|
3
|
+
quantik-core[benchmark,cbor,dev,docs]
|
|
4
|
+
|
|
5
|
+
[benchmark]
|
|
6
|
+
pytest-benchmark>=4.0
|
|
7
|
+
|
|
8
|
+
[cbor]
|
|
9
|
+
cbor2>=5.4.0
|
|
10
|
+
|
|
11
|
+
[dev]
|
|
12
|
+
pytest>=7.0
|
|
13
|
+
pytest-cov>=4.0
|
|
14
|
+
hypothesis>=6.0
|
|
15
|
+
black>=23.0
|
|
16
|
+
flake8>=6.0
|
|
17
|
+
mypy>=1.0
|
|
18
|
+
pre-commit>=3.0
|
|
19
|
+
|
|
20
|
+
[docs]
|
|
21
|
+
sphinx>=5.0
|
|
22
|
+
sphinx-rtd-theme>=1.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
quantik_core
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import itertools as it
|
|
2
|
+
import random
|
|
3
|
+
import struct
|
|
4
|
+
import pytest
|
|
5
|
+
from hypothesis import given, strategies as st
|
|
6
|
+
|
|
7
|
+
from quantik_core import State, D4, permute16, ALL_SHAPE_PERMS, VERSION, FLAG_CANON
|
|
8
|
+
|
|
9
|
+
# ---------- Helpers ----------
|
|
10
|
+
|
|
11
|
+
def apply_symmetry(bb8, d4_map, color_swap, shape_perm):
|
|
12
|
+
# bb8: tuple/list of 8 uint16 in order [C0S0..C0S3, C1S0..C1S3]
|
|
13
|
+
# returns transformed 8×uint16 in same order
|
|
14
|
+
assert len(bb8) == 8
|
|
15
|
+
# split [2][4]
|
|
16
|
+
b = [[bb8[c*4 + s] for s in range(4)] for c in range(2)]
|
|
17
|
+
# geometry
|
|
18
|
+
g = [[permute16(b[c][s], d4_map) for s in range(4)] for c in range(2)]
|
|
19
|
+
# color swap
|
|
20
|
+
if color_swap:
|
|
21
|
+
g[0], g[1] = g[1], g[0]
|
|
22
|
+
# shape perm
|
|
23
|
+
out = [0]*8
|
|
24
|
+
for s in range(4):
|
|
25
|
+
out[s] = g[0][shape_perm[s]]
|
|
26
|
+
out[4 + s] = g[1][shape_perm[s]]
|
|
27
|
+
return tuple(out)
|
|
28
|
+
|
|
29
|
+
def payload(bb8):
|
|
30
|
+
return struct.pack("<8H", *bb8)
|
|
31
|
+
|
|
32
|
+
# ---------- Golden/deterministic unit tests ----------
|
|
33
|
+
|
|
34
|
+
def test_pack_unpack_empty():
|
|
35
|
+
s = State.empty()
|
|
36
|
+
b = s.pack()
|
|
37
|
+
assert len(b) == 18
|
|
38
|
+
s2 = State.unpack(b)
|
|
39
|
+
assert s == s2
|
|
40
|
+
# canonical key for empty = 0x01 0x02 + 16 zero bytes
|
|
41
|
+
canon = s.canonical_key()
|
|
42
|
+
assert canon[:2] == bytes([VERSION, FLAG_CANON])
|
|
43
|
+
assert canon[2:] == b"\x00" * 16
|
|
44
|
+
|
|
45
|
+
def test_qfen_roundtrip_examples():
|
|
46
|
+
examples = [
|
|
47
|
+
".A../..b./.c../...D",
|
|
48
|
+
".... / .... / .... / ....",
|
|
49
|
+
"AbCd/aBcD/..../....",
|
|
50
|
+
"A.../B.../C.../D...",
|
|
51
|
+
"..a./.b../c.../...d",
|
|
52
|
+
]
|
|
53
|
+
for q in examples:
|
|
54
|
+
s = State.from_qfen(q)
|
|
55
|
+
assert State.from_qfen(s.to_qfen()) == s
|
|
56
|
+
|
|
57
|
+
def test_canonical_invariance_under_symmetry_examples():
|
|
58
|
+
# Any symmetry of a position must yield the same canonical key
|
|
59
|
+
q = ".A../..b./.c../...D"
|
|
60
|
+
base = State.from_qfen(q)
|
|
61
|
+
base_key = base.canonical_key()
|
|
62
|
+
bb8 = base.bb
|
|
63
|
+
for _, m in D4:
|
|
64
|
+
for cs in (False, True):
|
|
65
|
+
for sp in ALL_SHAPE_PERMS:
|
|
66
|
+
tbb8 = apply_symmetry(bb8, m, cs, sp)
|
|
67
|
+
ts = State(tbb8)
|
|
68
|
+
assert ts.canonical_key() == base_key
|
|
69
|
+
|
|
70
|
+
def test_single_piece_canonical_forms():
|
|
71
|
+
# Single pieces canonicalize to one of three possible forms depending on position symmetry class
|
|
72
|
+
expected_forms = {
|
|
73
|
+
struct.pack("<8H", 0, 0, 0, 0, 0, 0, 0, 256), # corners
|
|
74
|
+
struct.pack("<8H", 0, 0, 0, 0, 0, 0, 0, 512), # edges
|
|
75
|
+
struct.pack("<8H", 0, 0, 0, 0, 0, 0, 0, 4096), # center positions
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Collect all canonical forms for single pieces
|
|
79
|
+
canonical_forms = set()
|
|
80
|
+
for color in (0,1):
|
|
81
|
+
for shape in range(4):
|
|
82
|
+
for i in range(16):
|
|
83
|
+
bb = [0]*8
|
|
84
|
+
bb[color*4 + shape] = 1 << i
|
|
85
|
+
s = State(tuple(bb))
|
|
86
|
+
canonical_forms.add(s.canonical_payload())
|
|
87
|
+
|
|
88
|
+
# All canonical forms should be in our expected set
|
|
89
|
+
assert canonical_forms == expected_forms
|
|
90
|
+
|
|
91
|
+
def test_two_pieces_no_overlap():
|
|
92
|
+
# Ensure different configurations canonicalize consistently and pack/unpack survive
|
|
93
|
+
# Use a small set of arbitrary positions
|
|
94
|
+
positions = [(0,0),(0,5),(5,10),(10,15)]
|
|
95
|
+
for i,j in positions:
|
|
96
|
+
if i==j: continue
|
|
97
|
+
bb = [0]*8
|
|
98
|
+
bb[0] = 1 << i # C0S0
|
|
99
|
+
bb[5] = 1 << j # C1S1
|
|
100
|
+
s = State(tuple(bb))
|
|
101
|
+
key = s.canonical_key()
|
|
102
|
+
# Unpack back to state (not guaranteed same orientation) but format is valid
|
|
103
|
+
s2 = State.unpack(key[:2] + s.pack()[2:]) # reuse payload layout
|
|
104
|
+
assert isinstance(s2, State)
|
|
105
|
+
# Canonical key must be stable
|
|
106
|
+
assert s2.canonical_key() == key
|
|
107
|
+
|
|
108
|
+
# ---------- Property-based tests ----------
|
|
109
|
+
|
|
110
|
+
@st.composite
|
|
111
|
+
def states(draw):
|
|
112
|
+
# random board with the Quantik constraint: at most 1 piece per square
|
|
113
|
+
# (we won't enforce legality per Quantik rules here; just occupancy)
|
|
114
|
+
# pick up to N random pieces
|
|
115
|
+
n = draw(st.integers(min_value=0, max_value=8))
|
|
116
|
+
used = set()
|
|
117
|
+
bb = [0]*8
|
|
118
|
+
for _ in range(n):
|
|
119
|
+
i = draw(st.integers(min_value=0, max_value=15))
|
|
120
|
+
if i in used: continue
|
|
121
|
+
used.add(i)
|
|
122
|
+
color = draw(st.integers(min_value=0, max_value=1))
|
|
123
|
+
shape = draw(st.integers(min_value=0, max_value=3))
|
|
124
|
+
bb[color*4 + shape] |= 1 << i
|
|
125
|
+
return State(tuple(bb))
|
|
126
|
+
|
|
127
|
+
@given(states())
|
|
128
|
+
def test_pack_unpack_roundtrip_random(s):
|
|
129
|
+
data = s.pack()
|
|
130
|
+
s2 = State.unpack(data)
|
|
131
|
+
assert s == s2
|
|
132
|
+
|
|
133
|
+
@given(states())
|
|
134
|
+
def test_qfen_roundtrip_random(s):
|
|
135
|
+
q = s.to_qfen()
|
|
136
|
+
s2 = State.from_qfen(q)
|
|
137
|
+
assert s == s2
|
|
138
|
+
|
|
139
|
+
@given(states())
|
|
140
|
+
def test_canonical_is_min_over_symmetry_orbit(s):
|
|
141
|
+
# The canonical payload must equal the min over the full symmetry orbit
|
|
142
|
+
base = s.bb
|
|
143
|
+
payloads = []
|
|
144
|
+
for _, m in D4:
|
|
145
|
+
for cs in (False, True):
|
|
146
|
+
for sp in ALL_SHAPE_PERMS:
|
|
147
|
+
tbb8 = apply_symmetry(base, m, cs, sp)
|
|
148
|
+
payloads.append(payload(tbb8))
|
|
149
|
+
expected = min(payloads)
|
|
150
|
+
assert s.canonical_payload() == expected
|
|
151
|
+
|
|
152
|
+
@given(states())
|
|
153
|
+
def test_canonical_stability(s):
|
|
154
|
+
# canonicalizing twice is idempotent on the payload
|
|
155
|
+
k1 = s.canonical_payload()
|
|
156
|
+
s2 = State.unpack(bytes([VERSION, FLAG_CANON]) + k1) # reconstruct State from payload
|
|
157
|
+
k2 = s2.canonical_payload()
|
|
158
|
+
assert k1 == k2
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_cbor_roundtrip():
|
|
162
|
+
pytest.importorskip("cbor2") # Skip test if cbor2 not available
|
|
163
|
+
s = State.from_qfen(".A../..b./.c../...D")
|
|
164
|
+
blob = s.to_cbor(canon=False, mc=7, meta={"id":"X"})
|
|
165
|
+
s2 = State.from_cbor(blob)
|
|
166
|
+
assert s2 == s
|
|
167
|
+
|
|
168
|
+
def test_golden_empty():
|
|
169
|
+
s = State.empty()
|
|
170
|
+
# canonical key: 0x01 0x02 + 16 zero bytes
|
|
171
|
+
key = s.canonical_key()
|
|
172
|
+
assert key == bytes([VERSION, FLAG_CANON]) + b"\x00"*16
|
|
173
|
+
|
|
174
|
+
def test_canonical_single_piece_stability():
|
|
175
|
+
# Verify that canonicalization is stable - same input always gives same output
|
|
176
|
+
test_cases = [
|
|
177
|
+
(0, 0, 0), # C0S0 at position 0
|
|
178
|
+
(1, 3, 15), # C1S3 at position 15
|
|
179
|
+
(0, 2, 5), # C0S2 at position 5
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
for color, shape, pos in test_cases:
|
|
183
|
+
bb = [0]*8
|
|
184
|
+
bb[color*4 + shape] = 1 << pos
|
|
185
|
+
s = State(tuple(bb))
|
|
186
|
+
|
|
187
|
+
# Multiple calls should return same result
|
|
188
|
+
canonical1 = s.canonical_payload()
|
|
189
|
+
canonical2 = s.canonical_payload()
|
|
190
|
+
assert canonical1 == canonical2
|
|
191
|
+
|
|
192
|
+
# Optional: quick CBOR payload shape
|
|
193
|
+
def test_cbor_payload_shape():
|
|
194
|
+
pytest.importorskip("cbor2") # Skip test if cbor2 not available
|
|
195
|
+
s = State.empty()
|
|
196
|
+
blob = s.to_cbor(canon=True)
|
|
197
|
+
s2 = State.from_cbor(blob)
|
|
198
|
+
assert s2 == s
|
|
File without changes
|