idk
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
use alloc::{string::String, vec::Vec, boxed::Box};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{std::{application::{Application, Error}, io::{Color, Display, KeyStroke, Stdin}, random::Random, render::{ColorCode, ColouredChar, Dimensions, Frame, Position, RenderError}, time}, user::lib::libgui::cg_core::CgComponent};
|
||||
|
||||
pub struct Game {
|
||||
pub board: [[Cell; 7]; 6],
|
||||
pub turn: u8,
|
||||
pub vs_ai: bool,
|
||||
pub game_over: bool,
|
||||
pub winner: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub enum Cell {
|
||||
Empty,
|
||||
Player1,
|
||||
Player2,
|
||||
Victory,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Application for Game {
|
||||
fn new() -> Self {
|
||||
Game {
|
||||
board: [[Cell::Empty; 7]; 6],
|
||||
turn: 1,
|
||||
vs_ai: false,
|
||||
game_over: false,
|
||||
winner: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(&mut self, _: Vec<String>) -> Result<(), Error> {
|
||||
let _display = Display::borrow();
|
||||
|
||||
self.get_next_mode().await;
|
||||
|
||||
// Main game loop
|
||||
loop {
|
||||
if self.game_over {
|
||||
self.render_end_screen().await.unwrap();
|
||||
let c = Stdin::keystroke().await;
|
||||
match c {
|
||||
KeyStroke::Char('`') => break,
|
||||
_ => {
|
||||
self.reset();
|
||||
if !self.get_next_mode().await {
|
||||
break;
|
||||
}
|
||||
self.game_over = false;
|
||||
},
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
self.render().unwrap().write_to_screen().unwrap();
|
||||
|
||||
if self.vs_ai && self.turn == 2 {
|
||||
// AI's turn
|
||||
self.make_ai_move();
|
||||
if !self.game_over {
|
||||
self.turn = 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(key) = Stdin::last_keystroke() {
|
||||
match key {
|
||||
KeyStroke::Char('`') => break,
|
||||
KeyStroke::Char(c) => {
|
||||
if let Some(col) = c.to_digit(10) {
|
||||
if col > 0 && col <= 7 {
|
||||
self.add_cell(col - 1, self.turn);
|
||||
self.turn = if self.turn == 1 { 2 } else { 1 };
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.apply_gravity();
|
||||
self.check_victory();
|
||||
time::wait(0.1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Game {
|
||||
fn apply_gravity(&mut self) {
|
||||
// Process each column
|
||||
for col in 0..7 {
|
||||
// Start from second-to-last row and move up
|
||||
// We don't need to check the bottom row since nothing can fall below it
|
||||
for row in (0..5).rev() {
|
||||
// If current cell is empty or the cell below is not empty, skip
|
||||
if let Cell::Empty = self.board[row][col] {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if cell can fall one space
|
||||
if let Cell::Empty = self.board[row + 1][col] {
|
||||
// Move cell down one space
|
||||
self.board[row + 1][col] = self.board[row][col];
|
||||
self.board[row][col] = Cell::Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_next_mode(&mut self) -> bool {
|
||||
// Game mode selection
|
||||
|
||||
let mut frame = Frame::new(Position::new(0, 0), Dimensions::new(80, 25)).unwrap();
|
||||
let title = "Connect 4";
|
||||
let mode1 = "1. Player vs Player";
|
||||
let mode2 = "2. Player vs AI";
|
||||
|
||||
// Center coordinates
|
||||
let center_y = 10;
|
||||
let title_x = 40 - (title.len() / 2) as u16;
|
||||
let mode1_x = 40 - (mode1.len() / 2) as u16;
|
||||
let mode2_x = 40 - (mode2.len() / 2) as u16;
|
||||
|
||||
// Draw title
|
||||
for (i, c) in title.chars().enumerate() {
|
||||
frame[center_y][title_x as usize + i] = ColouredChar {
|
||||
character: c,
|
||||
colour: ColorCode::new(Color::Yellow, Color::Black),
|
||||
};
|
||||
}
|
||||
|
||||
// Draw mode options
|
||||
for (i, c) in mode1.chars().enumerate() {
|
||||
frame[center_y + 2][mode1_x as usize + i] = ColouredChar {
|
||||
character: c,
|
||||
colour: ColorCode::new(Color::White, Color::Black),
|
||||
};
|
||||
}
|
||||
|
||||
for (i, c) in mode2.chars().enumerate() {
|
||||
frame[center_y + 3][mode2_x as usize + i] = ColouredChar {
|
||||
character: c,
|
||||
colour: ColorCode::new(Color::White, Color::Black),
|
||||
};
|
||||
}
|
||||
|
||||
frame.write_to_screen().unwrap();
|
||||
|
||||
loop {
|
||||
let key = Stdin::keystroke().await;
|
||||
match key {
|
||||
KeyStroke::Char('1') => {
|
||||
self.vs_ai = false;
|
||||
break true;
|
||||
}
|
||||
KeyStroke::Char('2') => {
|
||||
self.vs_ai = true;
|
||||
break true;
|
||||
}
|
||||
KeyStroke::Char('`') => break false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_cell(&mut self, col: u32, player: u8) {
|
||||
if let Cell::Empty = self.board[0][col as usize] {
|
||||
self.board[0][col as usize] = match player {
|
||||
1 => Cell::Player1,
|
||||
2 => Cell::Player2,
|
||||
_ => panic!("Invalid player number"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn has_floating_tiles(&self) -> bool {
|
||||
for col in 0..7 {
|
||||
// Start from second-to-last row and move up
|
||||
for row in (0..5).rev() {
|
||||
// If current cell is not empty and cell below is empty, it's floating
|
||||
if self.board[row][col] != Cell::Empty && self.board[row + 1][col] == Cell::Empty {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn check_victory(&mut self) {
|
||||
// Only check for victory if there are no floating tiles
|
||||
if self.has_floating_tiles() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check horizontal
|
||||
for row in 0..6 {
|
||||
for col in 0..4 {
|
||||
let cell = self.board[row][col];
|
||||
if let Cell::Empty = cell { continue; }
|
||||
|
||||
if (0..4).all(|i| self.board[row][col + i] == cell) {
|
||||
// Mark winning cells
|
||||
for i in 0..4 {
|
||||
self.board[row][col + i] = Cell::Victory;
|
||||
}
|
||||
self.game_over = true;
|
||||
self.winner = Some(if cell == Cell::Player1 { 1 } else { 2 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check vertical
|
||||
for row in 0..3 {
|
||||
for col in 0..7 {
|
||||
let cell = self.board[row][col];
|
||||
if let Cell::Empty = cell { continue; }
|
||||
|
||||
if (0..4).all(|i| self.board[row + i][col] == cell) {
|
||||
// Mark winning cells
|
||||
for i in 0..4 {
|
||||
self.board[row + i][col] = Cell::Victory;
|
||||
}
|
||||
self.game_over = true;
|
||||
self.winner = Some(if cell == Cell::Player1 { 1 } else { 2 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check diagonal (down-right)
|
||||
for row in 0..3 {
|
||||
for col in 0..4 {
|
||||
let cell = self.board[row][col];
|
||||
if let Cell::Empty = cell { continue; }
|
||||
|
||||
if (0..4).all(|i| self.board[row + i][col + i] == cell) {
|
||||
// Mark winning cells
|
||||
for i in 0..4 {
|
||||
self.board[row + i][col + i] = Cell::Victory;
|
||||
}
|
||||
self.game_over = true;
|
||||
self.winner = Some(if cell == Cell::Player1 { 1 } else { 2 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check diagonal (down-left)
|
||||
for row in 0..3 {
|
||||
for col in 3..7 {
|
||||
let cell = self.board[row][col];
|
||||
if let Cell::Empty = cell { continue; }
|
||||
|
||||
if (0..4).all(|i| self.board[row + i][col - i] == cell) {
|
||||
// Mark winning cells
|
||||
for i in 0..4 {
|
||||
self.board[row + i][col - i] = Cell::Victory;
|
||||
}
|
||||
self.game_over = true;
|
||||
self.winner = Some(if cell == Cell::Player1 { 1 } else { 2 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for draw
|
||||
if !self.game_over {
|
||||
if (0..6).all(|row| (0..7).all(|col| self.board[row][col] != Cell::Empty)) {
|
||||
self.game_over = true;
|
||||
self.winner = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_ai_move(&mut self) {
|
||||
loop {
|
||||
let col = (Random::int(0, 6)) as u32;
|
||||
// Check if column is not full
|
||||
if self.board[0][col as usize] == Cell::Empty {
|
||||
self.add_cell(col, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.board = [[Cell::Empty; 7]; 6];
|
||||
self.turn = 1;
|
||||
self.game_over = false;
|
||||
self.winner = None;
|
||||
}
|
||||
|
||||
async fn render_end_screen(&mut self) -> Result<(), RenderError> {
|
||||
let mut frame = Frame::new(Position::new(0, 0), Dimensions::new(80, 25))?;
|
||||
|
||||
// Game Over message
|
||||
let game_over = "Game Over!";
|
||||
let winner_text = match self.winner {
|
||||
Some(1) => "Player 1 Wins!",
|
||||
Some(2) => if self.vs_ai { "AI Wins!" } else { "Player 2 Wins!" },
|
||||
None => "Draw!",
|
||||
_ => panic!("this shouldn't be possible"),
|
||||
};
|
||||
let restart_text = "Press any key to play again";
|
||||
|
||||
let center_y = 12;
|
||||
let game_over_x = 40 - (game_over.len() / 2) as u16;
|
||||
let winner_x = 40 - (winner_text.len() / 2) as u16;
|
||||
let restart_x = 40 - (restart_text.len() / 2) as u16;
|
||||
|
||||
// Draw game over
|
||||
for (i, c) in game_over.chars().enumerate() {
|
||||
frame[center_y][game_over_x as usize + i] = ColouredChar {
|
||||
character: c,
|
||||
colour: ColorCode::new(Color::White, Color::Black),
|
||||
};
|
||||
}
|
||||
|
||||
// Draw winner
|
||||
for (i, c) in winner_text.chars().enumerate() {
|
||||
frame[center_y + 1][winner_x as usize + i] = ColouredChar {
|
||||
character: c,
|
||||
colour: ColorCode::new(Color::Yellow, Color::Black),
|
||||
};
|
||||
}
|
||||
|
||||
// Draw restart instruction
|
||||
for (i, c) in restart_text.chars().enumerate() {
|
||||
frame[center_y + 2][restart_x as usize + i] = ColouredChar {
|
||||
character: c,
|
||||
colour: ColorCode::new(Color::White, Color::Black),
|
||||
};
|
||||
}
|
||||
|
||||
frame.write_to_screen()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self) -> Result<Frame, RenderError> {
|
||||
let mut frame = Frame::new(Position::new(0, 0), Dimensions::new(80, 25))?;
|
||||
|
||||
// Calculate center position to align board
|
||||
let cell_width = 4; // 3 for cell + 1 for separator
|
||||
let cell_height = 3; // 2 for cell + 1 for gap
|
||||
let board_width = 7 * cell_width - 1; // -1 because last separator not needed
|
||||
let board_height = 6 * cell_height - 1; // -1 because last gap not needed
|
||||
let start_x = (80 - board_width) / 2;
|
||||
let start_y = (25 - board_height) / 2;
|
||||
|
||||
// Draw column numbers
|
||||
for col in 0..7 {
|
||||
let x = start_x + (col * cell_width);
|
||||
// Center the 3-char number display "[X]" in the 3-space cell width
|
||||
frame[start_y - 3][x] = ColouredChar::coloured('[', ColorCode::new(Color::White, Color::Black));
|
||||
frame[start_y - 3][x + 1] = ColouredChar::coloured((1 + col as u8 + b'0') as char, ColorCode::new(Color::White, Color::Black));
|
||||
frame[start_y - 3][x + 2] = ColouredChar::coloured(']', ColorCode::new(Color::White, Color::Black));
|
||||
}
|
||||
|
||||
// Draw each cell
|
||||
for row in 0..6 {
|
||||
let y = start_y + (row * cell_height);
|
||||
|
||||
for col in 0..7 {
|
||||
let x = start_x + (col * cell_width);
|
||||
|
||||
// Draw vertical separator after each cell (except last column)
|
||||
if col < 6 {
|
||||
let separator_x = x + 3;
|
||||
for dy in 0..3 {
|
||||
frame[y -1 + dy][separator_x] = ColouredChar::coloured('│', ColorCode::new(Color::White, Color::Black));
|
||||
}
|
||||
}
|
||||
|
||||
// Set color based on cell state
|
||||
let color = match self.board[row][col] {
|
||||
Cell::Empty => continue,
|
||||
Cell::Player1 => ColorCode::new(Color::Red, Color::Red),
|
||||
Cell::Player2 => ColorCode::new(Color::Yellow, Color::Yellow),
|
||||
Cell::Victory => ColorCode::new(Color::Green, Color::Green),
|
||||
};
|
||||
|
||||
// Draw a 3x2 block for each cell
|
||||
for dy in 0..2 {
|
||||
for dx in 0..3 {
|
||||
frame[y + dy][x + dx] = ColouredChar::coloured('█', color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Draw horizontal gap after each row (except last row)
|
||||
if row < 5 {
|
||||
let gap_y = y + 2;
|
||||
for x in start_x..start_x + board_width {
|
||||
frame[gap_y][x] = ColouredChar::coloured(' ', ColorCode::new(Color::White, Color::Black));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn core::any::Any {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,6 @@ pub mod gameoflife;
|
||||
pub mod crystalrpg;
|
||||
pub mod pong;
|
||||
pub mod snake;
|
||||
pub mod paper;
|
||||
pub mod tetris;
|
||||
pub mod paper_rs;
|
||||
pub mod tetris;
|
||||
pub mod connect4;
|
||||
@@ -3,26 +3,27 @@ use core::any::Any;
|
||||
use alloc::{boxed::Box, format, string::String, vec::Vec};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{std::{self, application::{Application, Error}, io::{Color, ColorCode, Display, KeyStroke, Stdin}, render::{ColouredChar, Dimensions, Frame, Position, RenderError}, time}, user::lib::libgui::cg_core::CgComponent};
|
||||
use crate::{
|
||||
std::{
|
||||
self,
|
||||
application::{Application, Error},
|
||||
io::{Color, ColorCode, Display, KeyStroke, Stdin},
|
||||
render::{ColouredChar, Dimensions, Frame, Position, RenderError},
|
||||
time
|
||||
},
|
||||
user::lib::libgui::cg_core::CgComponent
|
||||
};
|
||||
|
||||
use super::player::Player;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum Cell {
|
||||
pub enum Cell {
|
||||
Empty,
|
||||
Solid(u8, bool),
|
||||
Tail(u8, bool),
|
||||
Head(u8, bool),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct Player {
|
||||
id: u8,
|
||||
alive: bool,
|
||||
position: (i32, i32),
|
||||
ai_ticks: u32, // Ticks until next direction change
|
||||
ai_direction: (i32, i32), // Current direction for AI
|
||||
}
|
||||
|
||||
pub struct GameBoard {
|
||||
board: [[Cell; 80]; 25],
|
||||
players: [Player; 6],
|
||||
@@ -36,12 +37,12 @@ impl Application for GameBoard {
|
||||
GameBoard {
|
||||
board: [[Cell::Empty; 80]; 25],
|
||||
players: [
|
||||
Player { id: 0, alive: true, position: (10, 10), ai_ticks: 0, ai_direction: (1, 0) },
|
||||
Player { id: 1, alive: true, position: (70, 10), ai_ticks: 5, ai_direction: (-1, 0) },
|
||||
Player { id: 2, alive: true, position: (10, 15), ai_ticks: 10, ai_direction: (1, 0) },
|
||||
Player { id: 3, alive: true, position: (70, 15), ai_ticks: 15, ai_direction: (-1, 0) },
|
||||
Player { id: 4, alive: true, position: (35, 5), ai_ticks: 20, ai_direction: (0, 1) },
|
||||
Player { id: 5, alive: true, position: (35, 20), ai_ticks: 25, ai_direction: (0, -1) },
|
||||
Player::new(0, (10, 10), false),
|
||||
Player::new(1, (70, 10), true),
|
||||
Player::new(2, (10, 15), true),
|
||||
Player::new(3, (70, 15), true),
|
||||
Player::new(4, (35, 5), true),
|
||||
Player::new(5, (35, 20), true),
|
||||
],
|
||||
max_territory: 0,
|
||||
current_territory: 0,
|
||||
@@ -52,15 +53,17 @@ impl Application for GameBoard {
|
||||
let _display = Display::borrow();
|
||||
|
||||
'outer: loop {
|
||||
|
||||
self.players.clone().into_iter().for_each(|p| {
|
||||
self.move_player(p.id, 0, 0);
|
||||
});
|
||||
// Set initial positions as solid territory
|
||||
for player in &self.players {
|
||||
let (x, y) = player.position;
|
||||
self.board[y as usize][x as usize] = Cell::Head(player.id, true);
|
||||
}
|
||||
|
||||
let mut player_direction: (i32, i32) = (0, 0);
|
||||
|
||||
// player controls player 1.
|
||||
loop {
|
||||
self.render().unwrap().write_to_screen().unwrap();
|
||||
time::wait(0.1);
|
||||
|
||||
// first get player input
|
||||
@@ -86,7 +89,6 @@ impl Application for GameBoard {
|
||||
|
||||
self.run_fill_algorithm();
|
||||
|
||||
self.render().unwrap().write_to_screen().unwrap();
|
||||
|
||||
if !self.players[0].alive {
|
||||
self.render_end_screen().await.unwrap();
|
||||
@@ -118,9 +120,9 @@ impl CgComponent for GameBoard {
|
||||
|
||||
match self.board[i][j] {
|
||||
Cell::Empty => { character = ' '; pid = None; }
|
||||
Cell::Solid(p, _) => { character = '░'; pid = Some(p); }
|
||||
Cell::Solid(p, _) => { character = '█'; pid = Some(p); }
|
||||
Cell::Tail(p, _) => { character = '▒'; pid = Some(p); }
|
||||
Cell::Head(p, _) => { character = '▓'; pid = Some(p); }
|
||||
Cell::Head(p, _) => { character = '▓'; pid = Some(p); }
|
||||
}
|
||||
|
||||
let colour = if let Some(p) = pid {
|
||||
@@ -157,7 +159,7 @@ impl GameBoard {
|
||||
const LEFT: (i32, i32) = (-1, 0);
|
||||
const RIGHT: (i32, i32) = (1, 0);
|
||||
const DIRS: [(i32, i32); 8] = [
|
||||
Self::UP, Self::DOWN, Self::LEFT, Self::RIGHT, // only cardinal directions
|
||||
Self::UP, Self::DOWN, Self::LEFT, Self::RIGHT,
|
||||
(1, 1), (1, -1), (-1, 1), (-1, -1),
|
||||
];
|
||||
|
||||
@@ -165,13 +167,20 @@ impl GameBoard {
|
||||
fn reset(&mut self) {
|
||||
self.board = [[Cell::Empty; 80]; 25];
|
||||
self.players = [
|
||||
Player { id: 0, alive: true, position: (10, 10), ai_ticks: 0, ai_direction: (1, 0) },
|
||||
Player { id: 1, alive: true, position: (70, 10), ai_ticks: 5, ai_direction: (-1, 0) },
|
||||
Player { id: 2, alive: true, position: (10, 15), ai_ticks: 10, ai_direction: (1, 0) },
|
||||
Player { id: 3, alive: true, position: (70, 15), ai_ticks: 15, ai_direction: (-1, 0) },
|
||||
Player { id: 4, alive: true, position: (35, 5), ai_ticks: 20, ai_direction: (0, 1) },
|
||||
Player { id: 5, alive: true, position: (35, 20), ai_ticks: 25, ai_direction: (0, -1) },
|
||||
Player::new(0, (10, 10), false),
|
||||
Player::new(1, (70, 10), true),
|
||||
Player::new(2, (10, 15), true),
|
||||
Player::new(3, (70, 15), true),
|
||||
Player::new(4, (35, 5), true),
|
||||
Player::new(5, (35, 20), true),
|
||||
];
|
||||
|
||||
// Set initial positions as solid territory
|
||||
for player in &self.players {
|
||||
let (x, y) = player.position;
|
||||
self.board[y as usize][x as usize] = Cell::Head(player.id, true);
|
||||
}
|
||||
|
||||
self.max_territory = 0;
|
||||
self.current_territory = 0;
|
||||
}
|
||||
@@ -185,15 +194,9 @@ impl GameBoard {
|
||||
// Check if position is empty or not in someone's territory
|
||||
if let Cell::Empty = self.board[y as usize][x as usize] {
|
||||
let dir_idx = std::random::Random::int(0, 3) as usize;
|
||||
self.players[player_id as usize] = Player {
|
||||
id: player_id,
|
||||
alive: true,
|
||||
position: (x, y),
|
||||
ai_ticks: std::random::Random::int(5, 10) as u32,
|
||||
ai_direction: Self::DIRS[dir_idx],
|
||||
};
|
||||
self.players[player_id as usize] = Player::new(player_id, (x, y), true);
|
||||
// Place the head at spawn position
|
||||
self.board[y as usize][x as usize] = Cell::Head(player_id, false);
|
||||
self.board[y as usize][x as usize] = Cell::Head(player_id, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -259,58 +262,12 @@ impl GameBoard {
|
||||
}
|
||||
|
||||
fn move_player(&mut self, playerid: u8, dx: i32, dy: i32) {
|
||||
let (ox, oy) = self.players[playerid as usize].position;
|
||||
let nx = ox + dx;
|
||||
let ny = oy + dy;
|
||||
|
||||
// Bounds checking
|
||||
if nx < 0 || nx >= 80 || ny < 0 || ny >= 25 {
|
||||
if playerid == 0 {
|
||||
self.players[playerid as usize].alive = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for collisions with tails
|
||||
let mut player_to_eliminate = None;
|
||||
|
||||
// check if player hit an enemy's tail
|
||||
match self.board[ny as usize][nx as usize] {
|
||||
Cell::Tail(id, _) => {
|
||||
if id != playerid {
|
||||
// Hit other player's tail
|
||||
player_to_eliminate = Some(id);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Convert old head to tail
|
||||
if let Cell::Head(id, owned) = self.board[oy as usize][ox as usize] {
|
||||
if owned {
|
||||
self.board[oy as usize][ox as usize] = Cell::Solid(id, true);
|
||||
} else {
|
||||
self.board[oy as usize][ox as usize] = Cell::Tail(id, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Place new head
|
||||
if let Cell::Solid(p, _) = self.board[ny as usize][nx as usize] {
|
||||
self.board[ny as usize][nx as usize] = Cell::Head(playerid, p == playerid);
|
||||
} else {
|
||||
self.board[ny as usize][nx as usize] = Cell::Head(playerid, false);
|
||||
}
|
||||
|
||||
// Update the player's position
|
||||
self.players[playerid as usize].position = (nx, ny);
|
||||
|
||||
// Handle elimination if needed
|
||||
if let Some(pid) = player_to_eliminate {
|
||||
self.players[pid as usize].alive = false;
|
||||
if let Some(eliminated_id) = self.players[playerid as usize].move_player(dx, dy, &mut self.board) {
|
||||
self.players[eliminated_id as usize].alive = false;
|
||||
// Clear territory and respawn if AI
|
||||
if pid != 0 {
|
||||
self.clear_player_territory(pid);
|
||||
self.respawn_ai_player(pid);
|
||||
if eliminated_id != 0 {
|
||||
self.clear_player_territory(eliminated_id);
|
||||
self.respawn_ai_player(eliminated_id);
|
||||
} else {
|
||||
// Update max territory before game over
|
||||
self.current_territory = self.count_territory();
|
||||
@@ -322,23 +279,10 @@ impl GameBoard {
|
||||
fn update_ai_players(&mut self) {
|
||||
// Skip player 0 (human player)
|
||||
for i in 1..6 {
|
||||
if !self.players[i].alive {
|
||||
continue;
|
||||
let dir = self.players[i].update_ai(&self.board);
|
||||
if dir != (0, 0) {
|
||||
self.move_player(i as u8, dir.0, dir.1);
|
||||
}
|
||||
|
||||
// Decrease tick counter
|
||||
if self.players[i].ai_ticks > 0 {
|
||||
self.players[i].ai_ticks -= 1;
|
||||
} else {
|
||||
// Time to change direction
|
||||
let random_dir = std::random::Random::int(0, 3);
|
||||
self.players[i].ai_direction = Self::DIRS[random_dir as usize]; //
|
||||
self.players[i].ai_ticks = 5 + (random_dir % 5) as u32; // Random ticks between 5-10
|
||||
}
|
||||
|
||||
// Move in current direction
|
||||
let dir = self.players[i].ai_direction;
|
||||
self.move_player(i as u8, dir.0, dir.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +295,30 @@ impl GameBoard {
|
||||
for player in self.players.iter().filter(|p| p.alive) {
|
||||
let pid = player.id;
|
||||
|
||||
// Check if player's head is touching their territory
|
||||
let (px, py) = player.position;
|
||||
let mut head_touching_territory = false;
|
||||
|
||||
// Check all adjacent cells to head
|
||||
for (dy, dx) in Self::DIRS.iter().take(4) { // Only check cardinal directions
|
||||
let ny = py + dy;
|
||||
let nx = px + dx;
|
||||
|
||||
if ny >= 0 && ny < 25 && nx >= 0 && nx < 80 {
|
||||
if let Cell::Solid(p, _) = self.board[ny as usize][nx as usize] {
|
||||
if p == pid {
|
||||
head_touching_territory = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if head is not touching territory
|
||||
if !head_touching_territory {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset grid
|
||||
for row in fill_grid.iter_mut() {
|
||||
row.fill(false);
|
||||
@@ -397,7 +365,7 @@ impl GameBoard {
|
||||
// Mark current cell as visited in the fill grid
|
||||
fill_grid[y][x] = true;
|
||||
|
||||
// Check all four directions
|
||||
// Check all 8 directions
|
||||
for (dy, dx) in Self::DIRS {
|
||||
let ny = (y as i32 + dy) as usize;
|
||||
let nx = (x as i32 + dx) as usize;
|
||||
@@ -0,0 +1,4 @@
|
||||
mod player;
|
||||
|
||||
mod game;
|
||||
pub use game::GameBoard;
|
||||
@@ -0,0 +1,419 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::std::random::Random;
|
||||
use crate::serial_println;
|
||||
use super::game::Cell;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MoveQueue {
|
||||
points: Vec<(i32, i32)>,
|
||||
}
|
||||
|
||||
impl MoveQueue {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
points: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.points.len() == 0
|
||||
}
|
||||
|
||||
fn current(&self) -> Option<(i32, i32)> {
|
||||
self.points.first().cloned()
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Option<(i32, i32)> {
|
||||
if let Some(point) = self.current() {
|
||||
self.points.remove(0);
|
||||
self.current()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.points.clear();
|
||||
}
|
||||
|
||||
fn push(&mut self, point: (i32, i32)) {
|
||||
self.points.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AiBehavior {
|
||||
Expand,
|
||||
Hunt,
|
||||
Defend,
|
||||
Escape,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Player {
|
||||
pub id: u8,
|
||||
pub alive: bool,
|
||||
pub position: (i32, i32),
|
||||
pub ai_direction: (i32, i32), // Current direction for AI
|
||||
pub ai_controlled: bool, // Whether this player is AI controlled
|
||||
pub ai_behavior: AiBehavior, // Current AI behavior state
|
||||
pub moves: MoveQueue,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
const DIRS: [(i32, i32); 8] = [
|
||||
(0, -1), (0, 1), (-1, 0), (1, 0), // Cardinal
|
||||
(1, 1), (1, -1), (-1, 1), (-1, -1) // Diagonal
|
||||
];
|
||||
|
||||
pub fn new(id: u8, position: (i32, i32), ai_controlled: bool) -> Self {
|
||||
// Set initial direction based on position to encourage better expansion
|
||||
let ai_direction = if position.0 < 40 {
|
||||
(1, 0) // If on left side, move right
|
||||
} else {
|
||||
(-1, 0) // If on right side, move left
|
||||
};
|
||||
|
||||
Self {
|
||||
id,
|
||||
alive: true,
|
||||
position,
|
||||
ai_direction,
|
||||
ai_controlled,
|
||||
ai_behavior: AiBehavior::Expand,
|
||||
moves: MoveQueue::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ai(&mut self, board: &[[Cell; 80]; 25]) -> (i32, i32) {
|
||||
if !self.ai_controlled || !self.alive {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
|
||||
match self.ai_behavior {
|
||||
AiBehavior::Expand => self.run_expand_behavior(board),
|
||||
AiBehavior::Hunt => self.run_hunt_behavior(board),
|
||||
AiBehavior::Defend => self.run_defend_behavior(board),
|
||||
AiBehavior::Escape => self.run_escape_behavior(board),
|
||||
}
|
||||
}
|
||||
|
||||
// BEHAVIOR METHOD
|
||||
fn run_expand_behavior(&mut self, board: &[[Cell; 80]; 25]) -> (i32, i32) {
|
||||
if self.moves.is_empty() {
|
||||
let points = self.generate_expand_points(board);
|
||||
for point in points {
|
||||
self.moves.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(target) = self.moves.current() {
|
||||
if self.position == target {
|
||||
// Only get next point if we've reached the current target
|
||||
self.moves.next();
|
||||
|
||||
// If we've completed all points, generate new ones
|
||||
if self.moves.is_empty() {
|
||||
let points = self.generate_expand_points(board);
|
||||
for point in points {
|
||||
self.moves.push(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current target and move towards it
|
||||
self.moves.current()
|
||||
.map(|target| self.get_move_to_position(target))
|
||||
.unwrap_or((0, 0))
|
||||
}
|
||||
|
||||
fn run_hunt_behavior(&mut self, board: &[[Cell; 80]; 25]) -> (i32, i32) {
|
||||
if let Some(tail_pos) = self.find_nearest_enemy_tail(board, 20) {
|
||||
self.get_move_to_position(tail_pos)
|
||||
} else if let Some(territory_pos) = self.find_nearest_territory_point(board, self.position) {
|
||||
self.get_move_to_position(territory_pos)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_defend_behavior(&mut self, board: &[[Cell; 80]; 25]) -> (i32, i32) {
|
||||
if let Some(territory_pos) = self.find_nearest_territory_point(board, self.position) {
|
||||
self.get_move_to_position(territory_pos)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_escape_behavior(&mut self, board: &[[Cell; 80]; 25]) -> (i32, i32) {
|
||||
if let Some(territory_pos) = self.find_nearest_territory_point(board, self.position) {
|
||||
self.get_move_to_position(territory_pos)
|
||||
} else {
|
||||
// Move away from center if no territory found
|
||||
let (x, y) = self.position;
|
||||
let center = (40, 12);
|
||||
let away_x = if x < center.0 { -1 } else { 1 };
|
||||
let away_y = if y < center.1 { -1 } else { 1 };
|
||||
(away_x, away_y)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// HELPER FUNCTIONS
|
||||
//
|
||||
|
||||
// Calculate Manhattan distance to nearest owned territory
|
||||
fn distance_to_territory(&self, board: &[[Cell; 80]; 25]) -> i32 {
|
||||
let (x, y) = self.position;
|
||||
let mut min_dist = i32::MAX;
|
||||
|
||||
for (y2, row) in board.iter().enumerate() {
|
||||
for (x2, cell) in row.iter().enumerate() {
|
||||
if let Cell::Solid(id, _) = cell {
|
||||
if *id == self.id {
|
||||
let dist = (x2 as i32 - x).abs() + (y2 as i32 - y).abs();
|
||||
min_dist = min_dist.min(dist);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
min_dist
|
||||
}
|
||||
|
||||
// Find nearest territory point to a given position
|
||||
fn find_nearest_territory_point(&self, board: &[[Cell; 80]; 25], from: (i32, i32)) -> Option<(i32, i32)> {
|
||||
let (x, y) = from;
|
||||
let mut nearest_point = None;
|
||||
let mut min_dist = i32::MAX;
|
||||
|
||||
for (y2, row) in board.iter().enumerate() {
|
||||
for (x2, cell) in row.iter().enumerate() {
|
||||
if let Cell::Solid(id, _) = cell {
|
||||
if *id == self.id {
|
||||
let dist = (x2 as i32 - x).abs() + (y2 as i32 - y).abs();
|
||||
if dist < min_dist {
|
||||
min_dist = dist;
|
||||
nearest_point = Some((x2 as i32, y2 as i32));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
nearest_point
|
||||
}
|
||||
|
||||
// Get optimal move direction to reach a target position
|
||||
fn get_move_to_position(&self, target: (i32, i32)) -> (i32, i32) {
|
||||
let (x, y) = self.position;
|
||||
let (target_x, target_y) = target;
|
||||
|
||||
let dx = (target_x - x).signum();
|
||||
let dy = (target_y - y).signum();
|
||||
|
||||
// If we're already at the target, return no movement
|
||||
if dx == 0 && dy == 0 {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// Move both horizontally and vertically if possible
|
||||
if dx != 0 && dy != 0 {
|
||||
return (dx, dy);
|
||||
}
|
||||
|
||||
// Otherwise move in the available direction
|
||||
(dx, dy)
|
||||
}
|
||||
|
||||
// Get locations of enemy tails adjacent to our territory
|
||||
fn get_adjacent_enemy_tails(&self, board: &[[Cell; 80]; 25]) -> Vec<(i32, i32)> {
|
||||
let mut tails = Vec::new();
|
||||
|
||||
// First find all our territory cells
|
||||
for (y, row) in board.iter().enumerate() {
|
||||
for (x, cell) in row.iter().enumerate() {
|
||||
if let Cell::Solid(id, _) = cell {
|
||||
if *id == self.id {
|
||||
// Check adjacent cells for enemy tails
|
||||
for &(dx, dy) in Self::DIRS.iter() {
|
||||
let nx = x as i32 + dx;
|
||||
let ny = y as i32 + dy;
|
||||
if nx >= 0 && nx < 80 && ny >= 0 && ny < 25 {
|
||||
if let Cell::Tail(id, _) = board[ny as usize][nx as usize] {
|
||||
if id != self.id {
|
||||
tails.push((nx, ny));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tails
|
||||
}
|
||||
|
||||
// Find nearest enemy tail within specified range
|
||||
fn find_nearest_enemy_tail(&self, board: &[[Cell; 80]; 25], range: i32) -> Option<(i32, i32)> {
|
||||
let (x, y) = self.position;
|
||||
let mut nearest_tail = None;
|
||||
let mut min_dist = range + 1; // Initialize to just over range to find only tails within range
|
||||
|
||||
// Search in a square area around the position
|
||||
for dy in -range..=range {
|
||||
for dx in -range..=range {
|
||||
let nx = x + dx;
|
||||
let ny = y + dy;
|
||||
|
||||
if nx >= 0 && nx < 80 && ny >= 0 && ny < 25 {
|
||||
if let Cell::Tail(id, _) = board[ny as usize][nx as usize] {
|
||||
if id != self.id {
|
||||
let dist = dx.abs() + dy.abs(); // Manhattan distance
|
||||
if dist < min_dist {
|
||||
min_dist = dist;
|
||||
nearest_tail = Some((nx, ny));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nearest_tail
|
||||
}
|
||||
|
||||
fn find_territory_edge(&self, board: &[[Cell; 80]; 25], dir_x: i32, dir_y: i32) -> (i32, i32) {
|
||||
let (x, y) = self.position;
|
||||
let mut edge_x = x;
|
||||
let mut edge_y = y;
|
||||
let mut found_x = false;
|
||||
let mut found_y = false;
|
||||
|
||||
// Search horizontally
|
||||
let mut search_x = x;
|
||||
while search_x >= 0 && search_x < 80 && !found_x {
|
||||
if let Cell::Solid(id, _) = board[y as usize][search_x as usize] {
|
||||
if id == self.id {
|
||||
edge_x = search_x + dir_x;
|
||||
found_x = true;
|
||||
}
|
||||
}
|
||||
search_x -= dir_x;
|
||||
}
|
||||
|
||||
// Search vertically
|
||||
let mut search_y = y;
|
||||
while search_y >= 0 && search_y < 25 && !found_y {
|
||||
if let Cell::Solid(id, _) = board[search_y as usize][x as usize] {
|
||||
if id == self.id {
|
||||
edge_y = search_y + dir_y;
|
||||
found_y = true;
|
||||
}
|
||||
}
|
||||
search_y -= dir_y;
|
||||
}
|
||||
|
||||
// If no territory found, use map boundaries
|
||||
if !found_x {
|
||||
edge_x = if dir_x > 0 { 0 } else { 79 };
|
||||
}
|
||||
if !found_y {
|
||||
edge_y = if dir_y > 0 { 0 } else { 24 };
|
||||
}
|
||||
|
||||
(edge_x, edge_y)
|
||||
}
|
||||
|
||||
fn generate_expand_points(&self, board: &[[Cell; 80]; 25]) -> Vec<(i32, i32)> {
|
||||
let mut points = Vec::new();
|
||||
let (x, y) = self.position;
|
||||
|
||||
// Distance to expand beyond territory
|
||||
let x_distance = 8;
|
||||
let y_distance = 8;
|
||||
|
||||
// Determine horizontal direction based on position
|
||||
let dir_x = if x < 40 { 1 } else { -1 };
|
||||
// Random vertical direction
|
||||
let dir_y = if Random::int(0, 1) == 0 { 1 } else { -1 };
|
||||
|
||||
// Find territory edge in both directions
|
||||
let (edge_x, edge_y) = self.find_territory_edge(board, dir_x, dir_y);
|
||||
|
||||
// Move beyond territory edge
|
||||
let p1 = (edge_x + (dir_x * x_distance), y);
|
||||
points.push(p1);
|
||||
|
||||
// Move perpendicular
|
||||
let p2 = (p1.0, p1.1 + (dir_y * y_distance));
|
||||
points.push(p2);
|
||||
|
||||
// Move back parallel to territory
|
||||
let p3 = (p2.1, y);
|
||||
points.push(p3);
|
||||
|
||||
// Complete rectangle
|
||||
if let Some(p4) = self.find_nearest_territory_point(board, p3) {
|
||||
serial_println!("START: [{}, {}][{:?} {:?} {:?} {:?}]", x, y, p1, p2, p3, p4);
|
||||
points.push(p4);
|
||||
}
|
||||
|
||||
points
|
||||
}
|
||||
|
||||
// MOVEMENT AND COLLISION
|
||||
|
||||
pub fn move_player(&mut self, dx: i32, dy: i32, board: &mut [[Cell; 80]; 25]) -> Option<u8> {
|
||||
if !self.alive {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (ox, oy) = self.position;
|
||||
let nx = ox + dx;
|
||||
let ny = oy + dy;
|
||||
|
||||
// Bounds checking
|
||||
if nx < 0 || nx >= 80 || ny < 0 || ny >= 25 {
|
||||
if !self.ai_controlled {
|
||||
self.alive = false;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check for collisions with tails
|
||||
let mut player_to_eliminate = None;
|
||||
|
||||
// Check if player hit an enemy's tail
|
||||
if let Cell::Tail(id, _) = board[ny as usize][nx as usize] {
|
||||
if id != self.id {
|
||||
// Hit other player's tail
|
||||
player_to_eliminate = Some(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert old head to tail
|
||||
if let Cell::Head(id, owned) = board[oy as usize][ox as usize] {
|
||||
if owned {
|
||||
board[oy as usize][ox as usize] = Cell::Solid(id, true);
|
||||
} else {
|
||||
board[oy as usize][ox as usize] = Cell::Tail(id, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Place new head
|
||||
if let Cell::Solid(p, _) = board[ny as usize][nx as usize] {
|
||||
board[ny as usize][nx as usize] = Cell::Head(self.id, p == self.id);
|
||||
} else {
|
||||
board[ny as usize][nx as usize] = Cell::Head(self.id, false);
|
||||
}
|
||||
|
||||
// Update the player's position
|
||||
self.position = (nx, ny);
|
||||
|
||||
player_to_eliminate
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,9 @@ use crate::{
|
||||
},
|
||||
games::{
|
||||
asteroids::Game as AsteroidsGame,
|
||||
connect4::Game as Connect4Game,
|
||||
gameoflife::GameOfLife,
|
||||
paper::GameBoard,
|
||||
paper_rs::GameBoard,
|
||||
pong::Game as PongGame,
|
||||
snake::Game as SnakeGame,
|
||||
// tetris::TetrisEngine,
|
||||
@@ -127,6 +128,11 @@ async fn exec() -> Result<(), Error> {
|
||||
cmd.run(args).await?;
|
||||
}
|
||||
|
||||
"games/connect4" => {
|
||||
let mut cmd = Connect4Game::new();
|
||||
cmd.run(args).await?;
|
||||
}
|
||||
|
||||
"rickroll" => {
|
||||
let mut cmd = Rickroll::new();
|
||||
cmd.run(args).await?;
|
||||
|
||||
Reference in New Issue
Block a user