rusty2048_core/
board.rs

1use crate::error::{GameError, GameResult};
2use serde::{Deserialize, Serialize};
3
4/// Represents a single tile on the game board
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub struct Tile {
7    /// Tile value (0 for empty, 2^n for filled tiles)
8    pub value: u32,
9}
10
11impl Tile {
12    /// Create a new empty tile
13    pub fn empty() -> Self {
14        Self { value: 0 }
15    }
16
17    /// Create a new tile with a specific value
18    pub fn new(value: u32) -> Self {
19        Self { value }
20    }
21
22    /// Check if the tile is empty
23    pub fn is_empty(&self) -> bool {
24        self.value == 0
25    }
26
27    /// Check if the tile can merge with another tile
28    pub fn can_merge_with(&self, other: &Tile) -> bool {
29        !self.is_empty() && !other.is_empty() && self.value == other.value
30    }
31
32    /// Merge this tile with another tile
33    pub fn merge_with(&mut self, other: &Tile) -> u32 {
34        if self.can_merge_with(other) {
35            self.value *= 2;
36            self.value
37        } else {
38            0
39        }
40    }
41
42    /// Get the color index for UI rendering
43    pub fn color_index(&self) -> usize {
44        if self.is_empty() {
45            0
46        } else {
47            self.value.trailing_zeros() as usize
48        }
49    }
50}
51
52/// Game board representation
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct Board {
55    /// 2D grid of tiles
56    tiles: Vec<Vec<Tile>>,
57    /// Board size (width = height)
58    size: usize,
59}
60
61impl Board {
62    /// Create a new empty board
63    pub fn new(size: usize) -> GameResult<Self> {
64        if size == 0 {
65            return Err(GameError::InvalidBoardSize { size });
66        }
67
68        let tiles = vec![vec![Tile::empty(); size]; size];
69        Ok(Self { tiles, size })
70    }
71
72    /// Get board size
73    pub fn size(&self) -> usize {
74        self.size
75    }
76
77    /// Get tile at position
78    pub fn get_tile(&self, row: usize, col: usize) -> GameResult<Tile> {
79        if row >= self.size || col >= self.size {
80            return Err(GameError::InvalidPosition { row, col });
81        }
82        Ok(self.tiles[row][col])
83    }
84
85    /// Set tile at position
86    pub fn set_tile(&mut self, row: usize, col: usize, tile: Tile) -> GameResult<()> {
87        if row >= self.size || col >= self.size {
88            return Err(GameError::InvalidPosition { row, col });
89        }
90        self.tiles[row][col] = tile;
91        Ok(())
92    }
93
94    /// Check if position is empty
95    pub fn is_empty(&self, row: usize, col: usize) -> GameResult<bool> {
96        Ok(self.get_tile(row, col)?.is_empty())
97    }
98
99    /// Get all empty positions
100    pub fn empty_positions(&self) -> Vec<(usize, usize)> {
101        let mut positions = Vec::new();
102        for row in 0..self.size {
103            for col in 0..self.size {
104                if self.tiles[row][col].is_empty() {
105                    positions.push((row, col));
106                }
107            }
108        }
109        positions
110    }
111
112    /// Check if board is full
113    pub fn is_full(&self) -> bool {
114        self.empty_positions().is_empty()
115    }
116
117    /// Check if any moves are possible
118    pub fn has_valid_moves(&self) -> bool {
119        // Check for empty tiles
120        if !self.is_full() {
121            return true;
122        }
123
124        // Check for possible merges
125        for row in 0..self.size {
126            for col in 0..self.size {
127                let current = self.tiles[row][col];
128
129                // Check right neighbor
130                if col + 1 < self.size && current.can_merge_with(&self.tiles[row][col + 1]) {
131                    return true;
132                }
133
134                // Check bottom neighbor
135                if row + 1 < self.size && current.can_merge_with(&self.tiles[row + 1][col]) {
136                    return true;
137                }
138            }
139        }
140
141        false
142    }
143
144    /// Get a copy of the current board state
145    pub fn clone_board(&self) -> Self {
146        Self {
147            tiles: self.tiles.clone(),
148            size: self.size,
149        }
150    }
151
152    /// Create board from tile data
153    pub fn from_tiles(tiles: Vec<Vec<Tile>>) -> GameResult<Self> {
154        if tiles.is_empty() || tiles[0].is_empty() {
155            return Err(GameError::InvalidBoardSize { size: 0 });
156        }
157
158        let size = tiles.len();
159        if tiles.iter().any(|row| row.len() != size) {
160            return Err(GameError::InvalidBoardSize { size });
161        }
162
163        Ok(Self { tiles, size })
164    }
165
166    /// Get the maximum tile value on the board
167    pub fn max_tile(&self) -> u32 {
168        self.tiles
169            .iter()
170            .flat_map(|row| row.iter())
171            .map(|tile| tile.value)
172            .max()
173            .unwrap_or(0)
174    }
175
176    /// Count tiles with a specific value
177    pub fn count_tiles(&self, value: u32) -> usize {
178        self.tiles
179            .iter()
180            .flat_map(|row| row.iter())
181            .filter(|tile| tile.value == value)
182            .count()
183    }
184
185    /// Convert board to 2D vector of u32 values
186    pub fn to_vec(&self) -> Vec<Vec<u32>> {
187        self.tiles
188            .iter()
189            .map(|row| row.iter().map(|tile| tile.value).collect())
190            .collect()
191    }
192
193    /// Create board from 2D vector of u32 values
194    pub fn from_vec(values: Vec<Vec<u32>>) -> GameResult<Self> {
195        if values.is_empty() || values[0].is_empty() {
196            return Err(GameError::InvalidBoardSize { size: 0 });
197        }
198
199        let size = values.len();
200        if values.iter().any(|row| row.len() != size) {
201            return Err(GameError::InvalidBoardSize { size });
202        }
203
204        let tiles = values
205            .iter()
206            .map(|row| row.iter().map(|&value| Tile::new(value)).collect())
207            .collect();
208
209        Ok(Self { tiles, size })
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_board_creation() {
219        let board = Board::new(4).unwrap();
220        assert_eq!(board.size(), 4);
221        assert!(board.is_empty(0, 0).unwrap());
222    }
223
224    #[test]
225    fn test_invalid_board_size() {
226        assert!(Board::new(0).is_err());
227    }
228
229    #[test]
230    fn test_tile_operations() {
231        let mut tile = Tile::empty();
232        assert!(tile.is_empty());
233
234        tile = Tile::new(2);
235        assert!(!tile.is_empty());
236        assert_eq!(tile.value, 2);
237
238        let other = Tile::new(2);
239        assert!(tile.can_merge_with(&other));
240        assert_eq!(tile.merge_with(&other), 4);
241    }
242
243    #[test]
244    fn test_board_operations() {
245        let mut board = Board::new(4).unwrap();
246
247        // Test setting and getting tiles
248        board.set_tile(0, 0, Tile::new(2)).unwrap();
249        assert_eq!(board.get_tile(0, 0).unwrap().value, 2);
250
251        // Test invalid positions
252        assert!(board.get_tile(4, 0).is_err());
253        assert!(board.set_tile(0, 4, Tile::new(2)).is_err());
254
255        // Test empty positions
256        let empty = board.empty_positions();
257        assert_eq!(empty.len(), 15); // 16 - 1 = 15 empty positions
258
259        // Test max tile
260        assert_eq!(board.max_tile(), 2);
261    }
262}