This commit is contained in:
FantasyPvP
2024-12-10 00:36:45 +00:00
parent 8f3a96c282
commit d77ce2bc47
13 changed files with 1413 additions and 306 deletions
+407
View File
@@ -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 -2
View File
@@ -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;
+4
View File
@@ -0,0 +1,4 @@
mod player;
mod game;
pub use game::GameBoard;
+419
View File
@@ -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
}
}
+7 -1
View File
@@ -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?;