1use crate::bitboard::Bitboard;
2use crate::constants::MAX_PIECES_PER_SHAPE;
3use crate::game::{check_winner, current_player, WinStatus};
4use crate::moves::{apply_move, generate_legal_moves, is_move_legal, Move};
5use crate::qfen::{bb_from_qfen, bb_to_qfen};
6use crate::state::State;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum GameResult {
10 Ongoing,
11 Player0Wins,
12 Player1Wins,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct PlayerInventory {
17 pub remaining: [u8; 4], }
19
20impl PlayerInventory {
21 pub fn full() -> Self {
22 Self {
23 remaining: [MAX_PIECES_PER_SHAPE; 4],
24 }
25 }
26
27 pub fn total(&self) -> u8 {
28 self.remaining.iter().sum()
29 }
30
31 pub fn has_shape(&self, shape: u8) -> bool {
32 self.remaining[shape as usize] > 0
33 }
34
35 pub fn use_shape(&mut self, shape: u8) {
36 debug_assert!(self.remaining[shape as usize] > 0);
37 self.remaining[shape as usize] -= 1;
38 }
39
40 pub fn return_shape(&mut self, shape: u8) {
41 debug_assert!(self.remaining[shape as usize] < MAX_PIECES_PER_SHAPE);
42 self.remaining[shape as usize] += 1;
43 }
44}
45
46struct MoveRecord {
47 mv: Move,
48 prev_bb: Bitboard,
49 prev_inventories: [PlayerInventory; 2],
50}
51
52pub struct QuantikBoard {
54 bb: Bitboard,
55 inventories: [PlayerInventory; 2],
56 current_player: u8,
57 history: Vec<MoveRecord>,
58}
59
60impl QuantikBoard {
61 pub fn new() -> Self {
62 Self {
63 bb: Bitboard::EMPTY,
64 inventories: [PlayerInventory::full(), PlayerInventory::full()],
65 current_player: 0,
66 history: Vec::new(),
67 }
68 }
69
70 pub fn from_bitboard(bb: Bitboard) -> Result<Self, String> {
71 let cp = current_player(&bb).ok_or("Invalid turn balance")?;
72 let mut invs = [PlayerInventory::full(), PlayerInventory::full()];
73 for player in 0..2u8 {
74 for shape in 0..4u8 {
75 let used = bb.shape_piece_count(player, shape) as u8;
76 if used > MAX_PIECES_PER_SHAPE {
77 return Err(format!(
78 "Player {} has {} pieces of shape {} (max {})",
79 player, used, shape, MAX_PIECES_PER_SHAPE
80 ));
81 }
82 invs[player as usize].remaining[shape as usize] = MAX_PIECES_PER_SHAPE - used;
83 }
84 }
85 Ok(Self {
86 bb,
87 inventories: invs,
88 current_player: cp,
89 history: Vec::new(),
90 })
91 }
92
93 pub fn from_qfen(qfen: &str) -> Result<Self, String> {
94 let bb = bb_from_qfen(qfen)?;
95 Self::from_bitboard(bb)
96 }
97
98 pub fn bitboard(&self) -> &Bitboard {
101 &self.bb
102 }
103
104 pub fn state(&self) -> State {
105 State::new(self.bb)
106 }
107
108 pub fn current_player(&self) -> u8 {
109 self.current_player
110 }
111
112 pub fn inventories(&self) -> &[PlayerInventory; 2] {
113 &self.inventories
114 }
115
116 pub fn move_count(&self) -> usize {
117 self.history.len()
118 }
119
120 pub fn last_move(&self) -> Option<&Move> {
121 self.history.last().map(|r| &r.mv)
122 }
123
124 pub fn to_qfen(&self) -> String {
125 bb_to_qfen(&self.bb)
126 }
127
128 pub fn game_result(&self) -> GameResult {
131 match check_winner(&self.bb) {
132 WinStatus::Player0Wins => return GameResult::Player0Wins,
133 WinStatus::Player1Wins => return GameResult::Player1Wins,
134 WinStatus::NoWin => {}
135 }
136 if self.legal_moves().is_empty() {
137 if self.current_player == 0 {
139 GameResult::Player1Wins
140 } else {
141 GameResult::Player0Wins
142 }
143 } else {
144 GameResult::Ongoing
145 }
146 }
147
148 pub fn is_game_over(&self) -> bool {
149 self.game_result() != GameResult::Ongoing
150 }
151
152 pub fn legal_moves(&self) -> Vec<Move> {
155 let raw = generate_legal_moves(&self.bb);
156 raw.into_iter()
157 .filter(|m| self.inventories[m.player as usize].has_shape(m.shape))
158 .collect()
159 }
160
161 pub fn is_legal(&self, mv: &Move) -> bool {
162 mv.player == self.current_player
163 && self.inventories[mv.player as usize].has_shape(mv.shape)
164 && is_move_legal(&self.bb, mv.player, mv.shape, mv.position)
165 }
166
167 pub fn play_move(&mut self, mv: Move) -> Result<(), String> {
170 if self.is_game_over() {
171 return Err("Game is already over".into());
172 }
173 if !self.is_legal(&mv) {
174 return Err("Illegal move".into());
175 }
176
177 let record = MoveRecord {
178 mv,
179 prev_bb: self.bb,
180 prev_inventories: self.inventories,
181 };
182
183 self.bb = apply_move(&self.bb, &mv);
184 self.inventories[mv.player as usize].use_shape(mv.shape);
185 self.current_player = 1 - self.current_player;
186 self.history.push(record);
187 Ok(())
188 }
189
190 pub fn undo_move(&mut self) -> bool {
191 if let Some(record) = self.history.pop() {
192 self.bb = record.prev_bb;
193 self.inventories = record.prev_inventories;
194 self.current_player = record.mv.player;
195 true
196 } else {
197 false
198 }
199 }
200
201 pub fn undo_moves(&mut self, count: usize) -> usize {
202 let mut undone = 0;
203 for _ in 0..count {
204 if self.undo_move() {
205 undone += 1;
206 } else {
207 break;
208 }
209 }
210 undone
211 }
212}
213
214impl Default for QuantikBoard {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220impl std::fmt::Display for QuantikBoard {
221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222 writeln!(f, "QFEN: {}", self.to_qfen())?;
223 writeln!(f, "Current player: {}", self.current_player)?;
224 writeln!(f, "Move count: {}", self.move_count())?;
225 for p in 0..2 {
226 let inv = &self.inventories[p];
227 writeln!(
228 f,
229 "Player {} inventory: A={} B={} C={} D={}",
230 p, inv.remaining[0], inv.remaining[1], inv.remaining[2], inv.remaining[3]
231 )?;
232 }
233 let result = self.game_result();
234 if result != GameResult::Ongoing {
235 writeln!(f, "Game result: {:?}", result)?;
236 }
237 Ok(())
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn new_board_is_empty() {
247 let board = QuantikBoard::new();
248 assert_eq!(board.to_qfen(), "..../..../..../....");
249 assert_eq!(board.current_player(), 0);
250 assert_eq!(board.move_count(), 0);
251 assert!(!board.is_game_over());
252 }
253
254 #[test]
255 fn play_and_undo() {
256 let mut board = QuantikBoard::new();
257 let mv = Move::new(0, 0, 0);
258 board.play_move(mv).unwrap();
259 assert_eq!(board.current_player(), 1);
260 assert_eq!(board.move_count(), 1);
261 assert!(board.undo_move());
262 assert_eq!(board.current_player(), 0);
263 assert_eq!(board.move_count(), 0);
264 }
265
266 #[test]
267 fn inventory_decreases_on_play() {
268 let mut board = QuantikBoard::new();
269 assert_eq!(board.inventories()[0].remaining[0], 2);
270 board.play_move(Move::new(0, 0, 0)).unwrap();
271 assert_eq!(board.inventories()[0].remaining[0], 1);
272 }
273
274 #[test]
275 fn illegal_move_rejected() {
276 let mut board = QuantikBoard::new();
277 let mv = Move::new(1, 0, 0);
279 assert!(board.play_move(mv).is_err());
280 }
281
282 #[test]
283 fn from_qfen_round_trip() {
284 let board = QuantikBoard::from_qfen("A.../..../..../....").unwrap();
285 assert_eq!(board.current_player(), 1);
286 assert_eq!(board.inventories()[0].remaining[0], 1);
287 }
288
289 #[test]
290 fn win_detection() {
291 let mut board = QuantikBoard::new();
292 board.play_move(Move::new(0, 0, 0)).unwrap(); board.play_move(Move::new(1, 1, 1)).unwrap(); board.play_move(Move::new(0, 2, 2)).unwrap(); board.play_move(Move::new(1, 3, 3)).unwrap(); assert!(board.is_game_over());
300 }
301}