Skip to main content

xxdc_oss_tictactoe/
lib.rs

1use std::{
2    ffi::{c_char, c_int, CString},
3    slice,
4};
5
6/// ----------------------------------------------------------------------------
7/// # TicTacToe Library FFI bindings
8/// ----------------------------------------------------------------------------
9
10/// Writes the current package version as a null-terminated C-style string to the provided buffer.
11///
12/// # Arguments
13/// * `buffer` - A mutable pointer to a buffer that will receive the version string.
14/// * `len` - The length of the buffer in bytes.
15///
16/// # Returns
17/// - If `buffer` is `null`, returns the required length of the buffer (including the null terminator).
18/// - If the buffer is too small, returns `-1`.
19/// - Otherwise, writes the version string to the buffer and returns the length of the version string (including the null terminator).
20#[no_mangle]
21pub extern "C" fn version(buffer: *mut u8, len: usize) -> isize {
22    let version = env!("CARGO_PKG_VERSION");
23    let version_bytes = version.as_bytes();
24    let required_len = version_bytes.len() + 1; // +1 for null terminator
25
26    if buffer.is_null() {
27        return required_len as isize;
28    }
29
30    if len < version_bytes.len() {
31        return -1;
32    }
33
34    unsafe {
35        let buffer_slice = slice::from_raw_parts_mut(buffer, required_len);
36        buffer_slice[..version_bytes.len()].copy_from_slice(version_bytes);
37        buffer_slice[version_bytes.len()] = 0; // null terminator
38    }
39
40    required_len as isize
41}
42
43type Callback = unsafe extern "C" fn(*const c_char, c_int);
44
45/// Calls the provided callback function with the current package version as a C-style string.
46///
47/// The callback function will be called with the following arguments:
48/// - `*const c_char`: A pointer to a null-terminated C-style string containing the package version.
49/// - `c_int`: The length of the version string, including the null terminator.
50///
51/// This function is marked as `unsafe` because it calls the provided callback function, which may perform unsafe operations.
52#[no_mangle]
53pub unsafe extern "C" fn version_string(callback: Callback) {
54    let version = env!("CARGO_PKG_VERSION");
55    let c_string = CString::new(version).expect("CString::new failed");
56    callback(c_string.as_ptr(), (version.len() + 1) as c_int);
57}
58
59/// ----------------------------------------------------------------------------
60/// # TicTacToe GameBoard FFI bindings
61/// ----------------------------------------------------------------------------
62
63#[no_mangle]
64pub extern "C" fn new_game_board(dimension: u32) -> *mut tictactoe::GameBoard {
65    Box::into_raw(Box::new(tictactoe::GameBoard::new(dimension)))
66}
67
68#[no_mangle]
69pub unsafe extern "C" fn free_game_board(game_board: *mut tictactoe::GameBoard) {
70    drop(Box::from_raw(game_board));
71}
72
73#[no_mangle]
74pub unsafe extern "C" fn get_game_board_dimension(game_board: *mut tictactoe::GameBoard) -> u32 {
75    (*game_board).get_dimension()
76}
77
78#[no_mangle]
79pub unsafe extern "C" fn get_game_board_value_at_index(
80    game_board: *mut tictactoe::GameBoard,
81    index: u32,
82) -> u32 {
83    (*game_board).get_with_index(index)
84}
85
86#[no_mangle]
87pub unsafe extern "C" fn get_game_board_with_value_at_index(
88    game_board: *mut tictactoe::GameBoard,
89    index: u32,
90    value: u32,
91) -> *mut tictactoe::GameBoard {
92    Box::into_raw(Box::new((*game_board).with_value_at_index(index, value)))
93}
94
95#[no_mangle]
96pub unsafe extern "C" fn get_game_board_is_full(game_board: *mut tictactoe::GameBoard) -> bool {
97    (*game_board).is_full()
98}
99
100#[no_mangle]
101pub unsafe extern "C" fn get_game_board_has_chain(
102    game_board: *mut tictactoe::GameBoard,
103    value: u32,
104) -> bool {
105    (*game_board).has_chain(value)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_ffi_can_manage_game_board_lifecycle_with_raw_pointer() {
114        let board_ptr = new_game_board(3);
115        assert!(!board_ptr.is_null());
116        unsafe {
117            free_game_board(board_ptr);
118        }
119    }
120
121    #[test]
122    fn test_ffi_can_get_game_board_value_at_index() {
123        let board_ptr = new_game_board(3);
124        assert!(!board_ptr.is_null());
125        unsafe {
126            assert_eq!(get_game_board_value_at_index(board_ptr, 0), 0);
127            free_game_board(board_ptr);
128        }
129    }
130
131    #[test]
132    fn test_ffi_can_update_a_gameboard_immutably() {
133        let board_ptr = new_game_board(3);
134        unsafe {
135            let updated_board_ptr = get_game_board_with_value_at_index(board_ptr, 4, 2);
136            assert_eq!(get_game_board_value_at_index(board_ptr, 4), 0);
137            assert_eq!(get_game_board_value_at_index(updated_board_ptr, 4), 2);
138            free_game_board(board_ptr);
139            free_game_board(updated_board_ptr);
140        }
141    }
142
143    #[test]
144    fn test_ffi_can_check_empty_game_board_has_available_moves() {
145        let board_ptr = new_game_board(3);
146        unsafe {
147            assert!(!get_game_board_is_full(board_ptr));
148            free_game_board(board_ptr);
149        }
150    }
151
152    #[test]
153    fn test_ffi_can_check_full_game_board_has_no_available_moves() {
154        let mut board_ptr = new_game_board(3);
155        unsafe {
156            for i in 0..9 {
157                board_ptr = get_game_board_with_value_at_index(board_ptr, i, i % 2 + 1);
158            }
159            assert!(get_game_board_is_full(board_ptr));
160        }
161    }
162
163    #[test]
164    fn test_ffi_can_check_for_winning_chain() {
165        let mut board_ptr = new_game_board(3);
166        unsafe {
167            for i in 0..3 {
168                board_ptr = get_game_board_with_value_at_index(board_ptr, i, 1);
169            }
170            assert!(get_game_board_has_chain(board_ptr, 1));
171            free_game_board(board_ptr);
172        }
173    }
174}
175
176/// ----------------------------------------------------------------------------
177/// # TicTacToe
178/// ----------------------------------------------------------------------------
179
180mod tictactoe {
181
182    #[derive(Clone)]
183    pub struct GameBoard {
184        dimension: u32,
185        content: Vec<Vec<u32>>,
186    }
187
188    impl GameBoard {
189        pub fn new(dimension: u32) -> Self {
190            let mut content = Vec::new();
191            for _ in 0..dimension {
192                let mut row = Vec::new();
193                for _ in 0..dimension {
194                    row.push(0);
195                }
196                content.push(row);
197            }
198            GameBoard { dimension, content }
199        }
200
201        pub fn get_dimension(&self) -> u32 {
202            self.dimension
203        }
204
205        pub fn get(&self, row: u32, col: u32) -> u32 {
206            self.content[row as usize][col as usize]
207        }
208
209        pub fn get_with_index(&self, index: u32) -> u32 {
210            let row = index / self.dimension;
211            let col = index % self.dimension;
212            self.get(row, col)
213        }
214
215        pub fn with_value_at(&self, row: u32, col: u32, value: u32) -> Self {
216            let mut new_board = self.clone();
217            new_board.set(row, col, value);
218            new_board
219        }
220
221        pub fn with_value_at_index(&self, index: u32, value: u32) -> Self {
222            let mut new_board = self.clone();
223            new_board.set_with_index(index, value);
224            new_board
225        }
226
227        pub fn is_empty_at(&self, row: u32, col: u32) -> bool {
228            self.get(row, col) == 0
229        }
230
231        pub fn is_empty_at_index(&self, index: u32) -> bool {
232            self.get_with_index(index) == 0
233        }
234
235        fn set(&mut self, row: u32, col: u32, value: u32) {
236            self.content[row as usize][col as usize] = value;
237        }
238
239        fn set_with_index(&mut self, index: u32, value: u32) {
240            let row = index / self.dimension;
241            let col = index % self.dimension;
242            self.set(row, col, value)
243        }
244
245        pub fn is_full(&self) -> bool {
246            for row in &self.content {
247                for value in row {
248                    if *value == 0 {
249                        return false;
250                    }
251                }
252            }
253            true
254        }
255
256        pub fn has_chain(&self, player: u32) -> bool {
257            // rows
258            let mut chain: u32;
259            for row_index in 0..self.dimension {
260                chain = 0;
261                for col_index in 0..self.dimension {
262                    if self.get(row_index, col_index) == player {
263                        chain += 1;
264                    } else {
265                        chain = 0;
266                    }
267                    if chain >= self.dimension {
268                        return true;
269                    }
270                }
271            }
272            // cols
273            for col_index in 0..self.dimension {
274                chain = 0;
275                for row_index in 0..self.dimension {
276                    if self.get(row_index, col_index) == player {
277                        chain += 1;
278                    } else {
279                        chain = 0;
280                    }
281                    if chain >= self.dimension {
282                        return true;
283                    }
284                }
285            }
286            // diagonals
287            chain = 0;
288            for offset in 0..self.dimension {
289                if self.get(offset, offset) == player {
290                    chain += 1;
291                } else {
292                    chain = 0;
293                }
294                if chain >= self.dimension {
295                    return true;
296                }
297            }
298            chain = 0;
299            for offset in 0..self.dimension {
300                if self.get(offset, self.dimension - offset - 1) == player {
301                    chain += 1;
302                } else {
303                    chain = 0;
304                }
305                if chain >= self.dimension {
306                    return true;
307                }
308            }
309            false
310        }
311    }
312
313    #[cfg(test)]
314    mod tests {
315        use super::*;
316
317        #[test]
318        fn test_new_game_board_creation() {
319            let board = GameBoard::new(3);
320            assert_eq!(board.dimension, 3);
321            assert_eq!(board.content.len(), 3);
322            for row in board.content {
323                assert_eq!(row.len(), 3);
324                for cell in row {
325                    assert_eq!(cell, 0);
326                }
327            }
328        }
329
330        #[test]
331        fn test_new_game_board_empty() {
332            let board = GameBoard::new(0);
333            assert_eq!(board.dimension, 0);
334            assert!(board.content.is_empty());
335        }
336
337        #[test]
338        fn test_new_game_board_large() {
339            let dimension = 100;
340            let board = GameBoard::new(dimension);
341            assert_eq!(board.dimension, dimension);
342            assert_eq!(board.content.len(), dimension as usize);
343            for row in board.content {
344                assert_eq!(row.len(), dimension as usize);
345                for cell in row {
346                    assert_eq!(cell, 0);
347                }
348            }
349        }
350
351        #[test]
352        fn test_set_and_retrieve_with_index() {
353            let mut board = GameBoard::new(3);
354            board.set_with_index(0, 99);
355            assert_eq!(board.get_with_index(0), 99);
356        }
357
358        #[test]
359        fn test_immutably_set_with_index() {
360            let board = GameBoard::new(3);
361            let new_board = board.with_value_at_index(0, 99);
362            assert_eq!(board.get_with_index(0), 0);
363            assert_eq!(new_board.get_with_index(0), 99);
364        }
365
366        #[test]
367        fn test_can_check_for_empty() {
368            let board = GameBoard::new(1);
369            assert_eq!(board.is_empty_at(0, 0), true);
370            assert_eq!(board.is_empty_at_index(0), true);
371        }
372
373        #[test]
374        fn test_can_check_for_full() {
375            let mut board = GameBoard::new(1);
376            board.set(0, 0, 1);
377            assert_eq!(board.is_full(), true);
378        }
379
380        #[test]
381        fn test_empty_board_has_no_chain() {
382            let board = GameBoard::new(3);
383            assert_eq!(board.has_chain(1), false);
384        }
385
386        #[test]
387        fn test_can_detect_winning_row_chain() {
388            let mut board = GameBoard::new(3);
389            for r in 0..3 {
390                board.set(r, 0, 1);
391            }
392            assert_eq!(board.has_chain(1), true);
393        }
394
395        #[test]
396        fn test_can_detect_winning_column_chain() {
397            let mut board = GameBoard::new(3);
398            for c in 0..3 {
399                board.set(1, c, 1);
400            }
401            assert_eq!(board.has_chain(1), true);
402        }
403
404        #[test]
405        fn test_can_detect_diagonal_winning_column_chain() {
406            let mut board = GameBoard::new(3);
407            for o in 0..3 {
408                board.set(o, o, 1);
409            }
410            assert_eq!(board.has_chain(1), true);
411        }
412    }
413}