Skip to content

Commit 9be52f9

Browse files
committed
connect4: Implement connect4 game
This was a joint effort of ChatGPT and me. I'm not super interested in plain algorithms. Therefore, I got myself some help from ChatGPT. Other than that, everything is implemented by myself.
1 parent f9b57d2 commit 9be52f9

File tree

3 files changed

+398
-0
lines changed

3 files changed

+398
-0
lines changed

userspace/Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ common = { path = "../common" }
1818
test = false
1919
bench = false
2020

21+
[[bin]]
22+
name = "connect4"
23+
test = false
24+
bench = false
25+
2126
[[bin]]
2227
name = "init"
2328
test = false
+304
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
use userspace::{print, println};
2+
3+
const COLUMNS: u8 = 7;
4+
const ROWS: u8 = 6;
5+
6+
#[derive(Debug, Clone, Copy)]
7+
pub enum Player {
8+
C,
9+
H,
10+
}
11+
12+
impl Player {
13+
fn opponent(&self) -> Self {
14+
match self {
15+
Self::C => Self::H,
16+
Self::H => Self::C,
17+
}
18+
}
19+
20+
pub fn switch(&mut self) {
21+
*self = self.opponent();
22+
}
23+
}
24+
25+
#[derive(Clone, Copy, PartialEq, Eq)]
26+
enum Position {
27+
Empty,
28+
C,
29+
H,
30+
}
31+
32+
impl From<Player> for Position {
33+
fn from(value: Player) -> Self {
34+
match value {
35+
Player::C => Position::C,
36+
Player::H => Position::H,
37+
}
38+
}
39+
}
40+
41+
#[derive(Clone)]
42+
pub struct GameBoard {
43+
board: [[Position; COLUMNS as usize]; ROWS as usize],
44+
}
45+
46+
impl GameBoard {
47+
pub fn new() -> Self {
48+
Self {
49+
board: [[Position::Empty; COLUMNS as usize]; ROWS as usize],
50+
}
51+
}
52+
53+
pub fn print(&self) {
54+
for row in 0..ROWS {
55+
for column in 0..COLUMNS {
56+
match self.board[row as usize][column as usize] {
57+
Position::Empty => print!(" - "),
58+
Position::C => print!(" C "),
59+
Position::H => print!(" H "),
60+
}
61+
}
62+
println!("");
63+
}
64+
println!(" 1 2 3 4 5 6 7");
65+
println!("");
66+
}
67+
68+
pub fn put(&mut self, player: Player, column: u8) -> Result<(), ()> {
69+
for row in (0..ROWS).rev() {
70+
if self.board[row as usize][column as usize] == Position::Empty {
71+
self.board[row as usize][column as usize] = player.into();
72+
return Ok(());
73+
}
74+
}
75+
Err(())
76+
}
77+
78+
fn calculate_score(&self, player: Player) -> i64 {
79+
let opponent = player.opponent();
80+
81+
let mut score = 0;
82+
83+
// Evaluate all possible directions for scoring
84+
let directions = [(1, 0), (0, 1), (1, 1), (1, -1)]; // Right, Down, Diagonal-right-down, Diagonal-left-down
85+
86+
for row in 0..ROWS {
87+
for col in 0..COLUMNS {
88+
for &(dr, dc) in &directions {
89+
score += self.evaluate_line(row, col, dr, dc, player, opponent);
90+
}
91+
}
92+
}
93+
94+
score
95+
}
96+
97+
fn evaluate_line(
98+
&self,
99+
start_row: u8,
100+
start_col: u8,
101+
dr: isize,
102+
dc: isize,
103+
player: Player,
104+
opponent: Player,
105+
) -> i64 {
106+
let mut player_count = 0;
107+
let mut opponent_count = 0;
108+
let mut empty_count = 0;
109+
110+
// Iterate through up to 4 positions in the specified direction
111+
for i in 0..4 {
112+
let r = start_row as isize + i * dr;
113+
let c = start_col as isize + i * dc;
114+
115+
// Check bounds
116+
if r < 0 || r >= ROWS as isize || c < 0 || c >= COLUMNS as isize {
117+
return 0;
118+
}
119+
120+
match self.board[r as usize][c as usize] {
121+
pos if pos == player.into() => player_count += 1,
122+
pos if pos == opponent.into() => opponent_count += 1,
123+
Position::Empty => empty_count += 1,
124+
_ => {}
125+
}
126+
}
127+
128+
// Scoring rules
129+
match (player_count, opponent_count, empty_count) {
130+
(4, 0, _) => 100000, // Winning line for the player
131+
(3, 0, 1) => 100, // Strong threat for the player
132+
(2, 0, 2) => 10, // Moderate threat for the player
133+
(1, 0, 3) => 1, // Weak threat for the player
134+
(0, 4, _) => -100000, // Opponent's winning line
135+
(0, 3, 1) => -100, // Strong threat for the opponent
136+
(0, 2, 2) => -10, // Moderate threat for the opponent
137+
(0, 1, 3) => -1, // Weak threat for the opponent
138+
_ => 0, // Neutral
139+
}
140+
}
141+
142+
// Checks if the game is over
143+
pub fn is_game_over(&self) -> Option<Player> {
144+
// Check if all columns are full
145+
if self
146+
.board
147+
.iter()
148+
.all(|row| row.iter().all(|&pos| pos != Position::Empty))
149+
{
150+
return None; // Draw
151+
}
152+
153+
self.check_winner()
154+
}
155+
156+
// Checks if there is a winner
157+
fn check_winner(&self) -> Option<Player> {
158+
for row in 0..ROWS {
159+
for col in 0..COLUMNS {
160+
if let Some(player) = self.check_four_in_a_row(row, col) {
161+
return Some(player);
162+
}
163+
}
164+
}
165+
None
166+
}
167+
168+
// Checks for four in a row starting from a specific position
169+
fn check_four_in_a_row(&self, row: u8, col: u8) -> Option<Player> {
170+
let directions = [
171+
(0, 1), // Horizontal
172+
(1, 0), // Vertical
173+
(1, 1), // Diagonal down-right
174+
(1, -1), // Diagonal down-left
175+
];
176+
177+
if let Position::C | Position::H = self.board[row as usize][col as usize] {
178+
let current_position = self.board[row as usize][col as usize];
179+
for (dr, dc) in directions {
180+
let mut count = 1;
181+
182+
for step in 1..4 {
183+
let new_row = row as isize + dr * step;
184+
let new_col = col as isize + dc * step;
185+
186+
if new_row < 0
187+
|| new_row >= ROWS as isize
188+
|| new_col < 0
189+
|| new_col >= COLUMNS as isize
190+
{
191+
break;
192+
}
193+
194+
if self.board[new_row as usize][new_col as usize] == current_position {
195+
count += 1;
196+
} else {
197+
break;
198+
}
199+
}
200+
201+
if count == 4 {
202+
return match current_position {
203+
Position::C => Some(Player::C),
204+
Position::H => Some(Player::H),
205+
_ => None,
206+
};
207+
}
208+
}
209+
}
210+
None
211+
}
212+
213+
fn for_valid_moves(&self, mut f: impl FnMut(u8) -> bool) {
214+
for column in 0..COLUMNS {
215+
if self.board[0][column as usize] == Position::Empty && !f(column) {
216+
break;
217+
}
218+
}
219+
}
220+
221+
/// Perform the minimax algorithm with alpha-beta pruning.
222+
fn minimax(
223+
&self,
224+
depth: u8,
225+
alpha: i64,
226+
beta: i64,
227+
maximizing_player: bool,
228+
player: Player,
229+
counter: &mut usize,
230+
) -> i64 {
231+
*counter += 1;
232+
233+
// Check for terminal states or maximum depth
234+
if depth == 0 || self.is_game_over().is_some() {
235+
return self.calculate_score(player);
236+
}
237+
238+
let mut alpha = alpha;
239+
let mut beta = beta;
240+
241+
if maximizing_player {
242+
let mut max_eval = i64::MIN;
243+
244+
self.for_valid_moves(|column| {
245+
let mut new_state = self.clone();
246+
new_state.put(player, column).unwrap();
247+
248+
let eval = new_state.minimax(depth - 1, alpha, beta, false, player, counter);
249+
max_eval = max_eval.max(eval);
250+
alpha = alpha.max(eval);
251+
252+
// Alpha-beta pruning
253+
if beta <= alpha {
254+
return false;
255+
}
256+
true
257+
});
258+
259+
max_eval
260+
} else {
261+
let opponent = player.opponent();
262+
let mut min_eval = i64::MAX;
263+
264+
self.for_valid_moves(|column| {
265+
let mut new_state = self.clone();
266+
new_state.put(opponent, column).unwrap();
267+
268+
let eval = new_state.minimax(depth - 1, alpha, beta, true, player, counter);
269+
min_eval = min_eval.min(eval);
270+
beta = beta.min(eval);
271+
272+
// Alpha-beta pruning
273+
if beta <= alpha {
274+
return false;
275+
}
276+
true
277+
});
278+
279+
min_eval
280+
}
281+
}
282+
283+
/// Get the best move using minimax with alpha-beta pruning.
284+
pub fn find_best_move(&self, depth: u8, player: Player, counter: &mut usize) -> Option<u8> {
285+
let mut best_move = None;
286+
let mut best_score = i64::MIN;
287+
288+
self.for_valid_moves(|column| {
289+
let mut new_state = self.clone();
290+
new_state.put(player, column).unwrap();
291+
292+
let score = new_state.minimax(depth - 1, i64::MIN, i64::MAX, false, player, counter);
293+
294+
if score > best_score {
295+
best_score = score;
296+
best_move = Some(column);
297+
}
298+
299+
true
300+
});
301+
302+
best_move
303+
}
304+
}

0 commit comments

Comments
 (0)