From 644c1426433ca9373402dc221faaacb7624e3d73 Mon Sep 17 00:00:00 2001 From: Max <35808469+maxblan@users.noreply.github.com> Date: Sun, 11 Feb 2024 20:18:44 +0100 Subject: [PATCH] fix bugs and add possible_moves method (#45) Revert "Refactor conversion functions and action handling" This reverts commit 3311035086e933579495625d7953912549f4b6ea. add posible moves method Refactor conversion functions and action handling Refactor action perform_unchecked methods Add new methods for performing unchecked actions --- Cargo.toml | 2 +- pyproject.toml | 2 +- python/socha/_socha.pyi | 65 +- python/socha/api/networking/network_socket.py | 2 +- python/socha/api/networking/utils.py | 64 +- src/plugin/actions.rs | 46 +- src/plugin/actions/accelerate.rs | 12 + src/plugin/actions/advance.rs | 65 +- src/plugin/actions/turn.rs | 16 +- src/plugin/board.rs | 88 +- src/plugin/constants.rs | 4 + src/plugin/coordinate.rs | 4 + src/plugin/game_state.rs | 1013 +++++++---------- src/plugin/segment.rs | 2 - src/plugin/ship.rs | 11 +- 15 files changed, 658 insertions(+), 738 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 872b5ca..04525b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "_socha" -version = "2.1.0" +version = "2.1.1" edition = "2021" [lib] diff --git a/pyproject.toml b/pyproject.toml index 9599d16..0c70168 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "socha" -version = "2.1.0" +version = "2.1.1" authors = [{ name = "maxblan", email = "stu222782@mail.uni-kiel.de" }] description = "This is the package for the Software-Challenge Germany 2023. This Season the game will be 'Hey, danke für den Fisch' a.k.a. 'Penguins' in short." readme = "README.md" diff --git a/python/socha/_socha.pyi b/python/socha/_socha.pyi index c40d00d..76fbd36 100644 --- a/python/socha/_socha.pyi +++ b/python/socha/_socha.pyi @@ -1,4 +1,4 @@ -from typing import Any, List, Optional, Set +from typing import Any, List, Optional, Set, Tuple class PluginConstants: @@ -120,10 +120,7 @@ class Accelerate: acc: int def __init__(self, acc: int) -> None: ... - def perform(self, state: Any) -> Ship | BaseException( - AccelerationProblem): ... - - def accelerate(self, ship: Any) -> Ship: ... + def perform(self, state: Any) -> Ship | BaseException: ... def __str__(self) -> str: ... @@ -142,8 +139,7 @@ class Advance: distance: int def __init__(self, distance: int) -> None: ... - def perform(self, state: GameState) -> Ship | BaseException( - AdvanceProblem): ... + def perform(self, state: GameState) -> Ship | BaseException: ... class PushProblem: @@ -161,8 +157,8 @@ class Push: direction: CubeDirection def __init__(self, direction: CubeDirection) -> None: ... - def perform(self, state: GameState) -> (Ship, Ship) | BaseException( - PushProblem): ... + def perform(self, state: GameState) -> Tuple[Ship, + Ship] | BaseException: ... class TurnProblem(): @@ -177,8 +173,7 @@ class Turn: direction: CubeDirection def __init__(self, direction: CubeDirection) -> None: ... - def perform(self, state: GameState) -> Ship | BaseException( - TurnProblem): ... + def perform(self, state: GameState) -> Ship | BaseException: ... def coal_cost(self, ship: Ship) -> int: ... @@ -267,10 +262,43 @@ class Board: def pickup_passenger(self, state: GameState) -> GameState: ... def pickup_passenger_at_position( - self, pos: CubeCoordinates) -> Optional[Field]: ... + self, pos: CubeCoordinates) -> Optional[Field]: + """ + Picks up a passenger at the specified position using the CubeCoordinates. + + Args: + pos (CubeCoordinates): The CubeCoordinates representing the position to check. + + Returns: + Optional[Field]: The Field containing a passenger with a passenger count greater than 0, + or None if no such Field is found in any adjacent direction. + """ def find_nearest_field_types(self, start_coordinates: CubeCoordinates, - field_type: FieldType) -> Set[CubeCoordinates]: ... + field_type: FieldType) -> Set[CubeCoordinates]: + """ + A function to find the nearest field(s) of a specific type from a starting point in a hexagonal grid. + + Args: + start_coordinates (CubeCoordinates): A CubeCoordinates object representing the starting point for the search. + field_type (FieldType): A FieldType object representing the type of field being searched for. + + Returns: + list of CubeCoordinates: A list of CubeCoordinates corresponding to the location of the nearest field(s) of the specified type. + + Note: + This function will always return the coordinates of the nearest field(s) of the specified type, if such a field(s) exist. + If multiple fields of the same type are at the same minimum distance, it returns all of them. + If there isn't a field of the specified type or path to it, it will return an empty list. + + Examples: + ```python + from plugin import Board, CubeCoordinates, FieldType + + board = Board() + board.find_nearest_field_types(CubeCoordinates(0, 0), FieldType.Water) + ``` + """ class TeamPoints: @@ -302,7 +330,11 @@ class GameState: def ship_advance_points(self, ship: Ship) -> int: ... def calculate_points(self, ship: Ship) -> int: ... def is_current_ship_on_current(self) -> bool: ... + def perform_action(self, action: Accelerate | Advance | + Push | Turn) -> GameState: ... + def perform_move(self, move: Move) -> GameState: ... + def perform_move_unchecked(self, move: Move) -> GameState: ... def advance_turn(self) -> GameState: ... def effective_speed(self, ship: Ship) -> int: ... def remove_passenger_at(self, coords: CubeCoordinates) -> bool: ... @@ -321,6 +353,13 @@ class GameState: def possible_advances(self) -> List[Advance]: ... def sandbank_advances_for(self, ship: Ship) -> Optional[List[Advance]]: ... + def possible_moves(self) -> List[Move]: ... + + def possible_action_comb(self, current_state: GameState, + current_actions: List[Accelerate | Advance | Push | Turn], + depth: int, + max_depth: int) -> List[List[Accelerate | Advance | Push | Turn]]: ... + def possible_actions(self, rank: int) -> List[Accelerate | Advance | Push | Turn]: ... diff --git a/python/socha/api/networking/network_socket.py b/python/socha/api/networking/network_socket.py index 694de7f..6a4fbba 100644 --- a/python/socha/api/networking/network_socket.py +++ b/python/socha/api/networking/network_socket.py @@ -61,7 +61,7 @@ def receive(self) -> Union[bytes, None]: If a timeout occurs or a connection reset error is encountered, the socket is closed and None is returned. """ regex = re.compile( - br"<((room[\s\S]+?)|errorpacket[\s\S]+?|prepared[\s\S]+?)") + br"<((room[\s\S]+?)|(errorpacket[\s\S]+?)|(prepared[\s\S]+?)|(joined|left|join|observe|pause|step|cancel|creatGame|authenticate)[\s\S]*?/>)") while True: try: chunk = self.socket.recv(16129) diff --git a/python/socha/api/networking/utils.py b/python/socha/api/networking/utils.py index 66090d9..e54d801 100644 --- a/python/socha/api/networking/utils.py +++ b/python/socha/api/networking/utils.py @@ -1,9 +1,8 @@ -from ast import List -import logging +from typing import List from socha import _socha from socha._socha import Field, FieldType, Move, TeamEnum, CubeCoordinates, GameState -from socha.api.protocol.protocol import Acceleration, Actions, Advance, Push, Turn, Board, Data, Water, Sandbank, Island, Passenger, Goal +from socha.api.protocol.protocol import Acceleration, Actions, Advance, Push, Ship, Turn, Board, Data, Water, Sandbank, Island, Passenger, Goal def _convert_board(protocol_board: Board) -> _socha.Board: @@ -106,7 +105,18 @@ def _merge_advances(actions): return new_actions -def if_last_game_state(message, last_game_state): +def if_last_game_state(message, last_game_state) -> GameState: + """ + Constructs a GameState from the provided message, ensuring to reflect the + current state based on the ships' positions, teams, and other attributes. + + Args: + message: The input message containing the current game state. + last_game_state: The last game state to be updated. + + Returns: + GameState: The constructed game state from the message. + """ try: last_game_state.board = _convert_board( message.data.class_binding.board) @@ -119,28 +129,40 @@ def if_last_game_state(message, last_game_state): last_move = Move(actions=new_actions) return last_game_state.perform_move(last_move) except Exception as e: - logging.warning( - f"An error occurred: {e}. Returning the last game state without changes.") - return last_game_state + return if_not_last_game_state(message) def if_not_last_game_state(message) -> GameState: - first_position = CubeCoordinates( - q=message.data.class_binding.ship[0].position.q, r=message.data.class_binding.ship[0].position.r) - first_team = TeamEnum.One if message.data.class_binding.ship[ - 0].team == "ONE" else TeamEnum.Two - first_team = _socha.Ship(position=first_position, team=first_team) - - second_position = CubeCoordinates( - q=message.data.class_binding.ship[1].position.q, r=message.data.class_binding.ship[1].position.r) - second_team = TeamEnum.One if message.data.class_binding.ship[ - 1].team == "ONE" else TeamEnum.Two - second_team = _socha.Ship(position=second_position, team=second_team) + """ + Constructs a GameState from the provided message, ensuring to reflect the + current state based on the ships' positions, teams, and other attributes. + + Args: + message: The input message containing the current game state. + + Returns: + GameState: The constructed game state from the message. + """ + def create_ship(ship_data, team_enum_value) -> _socha.Ship: + """Helper function to create a ship from the ship data.""" + position = CubeCoordinates( + q=ship_data.position.q, r=ship_data.position.r) + team = TeamEnum.One if team_enum_value == "ONE" else TeamEnum.Two + return _socha.Ship(position=position, team=team, coal=ship_data.coal, + passengers=ship_data.passengers, points=ship_data.points, + speed=ship_data.speed, free_turns=ship_data.free_turns, + direction=direction_from_string(ship_data.direction)) + + first_ship_data, second_ship_data = message.data.class_binding.ship + first_ship = create_ship(first_ship_data, first_ship_data.team) + second_ship = create_ship(second_ship_data, second_ship_data.team) + + current_team_enum = TeamEnum.One if message.data.class_binding.current_team == "ONE" else TeamEnum.Two return GameState( board=_convert_board(message.data.class_binding.board), turn=message.data.class_binding.turn, - current_ship=first_team, - other_ship=second_team, - last_move=None, + current_ship=first_ship if current_team_enum == TeamEnum.One else second_ship, + other_ship=second_ship if current_team_enum == TeamEnum.One else first_ship, + last_move=None ) diff --git a/src/plugin/actions.rs b/src/plugin/actions.rs index c192c8a..627bed4 100644 --- a/src/plugin/actions.rs +++ b/src/plugin/actions.rs @@ -1,11 +1,14 @@ -use pyo3::{IntoPy, PyObject}; +use pyo3::{ IntoPy, PyErr, PyObject }; use pyo3::FromPyObject; use pyo3::Python; use crate::plugin::actions::push::Push; use crate::plugin::actions::turn::Turn; -use self::{accelerate::Accelerate, advance::Advance}; +use self::{ accelerate::Accelerate, advance::Advance }; + +use super::game_state::GameState; +use super::ship::Ship; pub mod accelerate; pub mod advance; @@ -31,3 +34,42 @@ impl IntoPy for Action { } } +impl Action { + pub fn perform( + &self, + game_state: &mut GameState + ) -> Result<(Option, Option), PyErr> { + match self { + Action::Accelerate(accelerate) => + accelerate.perform(game_state).map(|ship| (Some(ship), None)), + Action::Advance(advance) => advance.perform(game_state).map(|ship| (Some(ship), None)), + Action::Push(push) => + push.perform(game_state).map(|(ship1, ship2)| (Some(ship1), Some(ship2))), + Action::Turn(turn) => turn.perform(game_state).map(|ship| (Some(ship), None)), + } + } +} + +impl From for Action { + fn from(acc: Accelerate) -> Self { + Action::Accelerate(acc) + } +} + +impl From for Action { + fn from(turn: Turn) -> Self { + Action::Turn(turn) + } +} + +impl From for Action { + fn from(advance: Advance) -> Self { + Action::Advance(advance) + } +} + +impl From for Action { + fn from(push: Push) -> Self { + Action::Push(push) + } +} diff --git a/src/plugin/actions/accelerate.rs b/src/plugin/actions/accelerate.rs index d7c8984..167df81 100644 --- a/src/plugin/actions/accelerate.rs +++ b/src/plugin/actions/accelerate.rs @@ -71,6 +71,7 @@ impl Accelerate { } } } + fn accelerate(&self, ship: &mut Ship) -> Ship { debug!("accelerate() called with ship: {:?}", ship); let used_coal: i32 = self.acc.abs() - ship.free_acc; @@ -80,6 +81,17 @@ impl Accelerate { debug!("Acceleration completed and ship status: {:?}", ship); return ship.clone(); } + + fn accelerate_unchecked(&self, ship: &mut Ship) -> Ship { + debug!("accelerate_unchecked() called with ship: {:?}", ship); + let used_coal: i32 = self.acc.abs() - ship.free_acc; + ship.coal -= used_coal.max(0); + ship.free_acc = (-used_coal).max(0); + ship.accelerate_by(self.acc); + debug!("Unchecked acceleration completed and ship status: {:?}", ship); + ship.clone() + } + fn __repr__(&self) -> PyResult { debug!("__repr__ method called for Accelerate with acc: {}", self.acc); Ok(format!("Accelerate({})", self.acc)) diff --git a/src/plugin/actions/advance.rs b/src/plugin/actions/advance.rs index 30d8637..511331e 100644 --- a/src/plugin/actions/advance.rs +++ b/src/plugin/actions/advance.rs @@ -5,10 +5,9 @@ use pyo3::prelude::*; use crate::plugin::{ constants::PluginConstants, errors::advance_errors::AdvanceProblem, - game_state::AdvanceInfo, game_state::GameState, }; -use crate::plugin::field::FieldType; + use crate::plugin::ship::Ship; #[pyclass] @@ -26,50 +25,48 @@ impl Advance { debug!("New Advance with distance: {}", distance); Advance { distance } } + pub fn perform(&self, state: &GameState) -> Result { debug!("Performing advance with distance: {}", self.distance); - let mut current_ship: Ship = state.current_ship.clone(); + let mut ship = state.current_ship.clone(); + let valid_distance = self.validate_distance(&state, &ship)?; + let advance_info = state.calculate_advance_info( + &ship.position, + &ship.direction(!valid_distance), + ship.movement + ); + let advance_possible = (advance_info.distance() as i32) >= self.distance.abs(); + + if !advance_possible { + debug!("Advance problem: {}", advance_info.problem.message()); + return Err(PyBaseException::new_err(advance_info.problem.message())); + } + + ship.update_position(self.distance, advance_info); + debug!("Advance completed: {:?}", ship); + Ok(ship) + } + + fn validate_distance(&self, state: &GameState, ship: &Ship) -> Result { if (self.distance < PluginConstants::MIN_SPEED && - state.board.get(¤t_ship.position).unwrap().field_type != - FieldType::Sandbank) || + !state.board.is_sandbank(&ship.position)) || self.distance > PluginConstants::MAX_SPEED { - debug!("Invalid distance error with distance: {}", self.distance); + debug!("Invalid distance: {}", self.distance); return Err(PyBaseException::new_err(AdvanceProblem::InvalidDistance.message())); } - if self.distance > current_ship.movement { + if self.distance > ship.movement { debug!( - "Movement points missing error with distance: {} and movement: {}", + "Movement points missing: {} needed, {} available", self.distance, - current_ship.movement + ship.movement ); return Err(PyBaseException::new_err(AdvanceProblem::MovementPointsMissing.message())); } - let result: AdvanceInfo = state.calculate_advance_info( - ¤t_ship.position, - &(if self.distance < 0 { - current_ship.direction.opposite() - } else { - current_ship.direction - }), - current_ship.movement - ); - if (result.distance() as i32) < self.distance.abs() { - debug!( - "Advance problem with available distance: {} and requested distance: {}", - result.distance(), - self.distance - ); - debug!("Advance problem reason: {}", result.problem.message()); - return Err(PyBaseException::new_err(result.problem.message())); - } - current_ship.position += current_ship.direction.vector() * self.distance; - current_ship.movement -= result.cost_until(self.distance as usize); - - debug!("Advance completed and ship status: {:?}", current_ship); - Ok(current_ship) + Ok(true) } + fn __repr__(&self) -> PyResult { Ok(format!("Advance({})", self.distance)) } @@ -78,10 +75,10 @@ impl Advance { #[cfg(test)] mod tests { use pyo3::prepare_freethreaded_python; - + use crate::plugin::board::Board; use crate::plugin::coordinate::{ CubeCoordinates, CubeDirection }; - use crate::plugin::field::Field; + use crate::plugin::field::{ Field, FieldType }; use crate::plugin::game_state::GameState; use crate::plugin::segment::Segment; use crate::plugin::ship::{ Ship, TeamEnum }; diff --git a/src/plugin/actions/turn.rs b/src/plugin/actions/turn.rs index a78a363..171392c 100644 --- a/src/plugin/actions/turn.rs +++ b/src/plugin/actions/turn.rs @@ -1,5 +1,5 @@ use log::debug; -use pyo3::{exceptions::PyBaseException, prelude::*}; +use pyo3::{ exceptions::PyBaseException, prelude::* }; use crate::plugin::{ coordinate::CubeDirection, @@ -68,13 +68,13 @@ impl Turn { #[cfg(test)] mod tests { use pyo3::prepare_freethreaded_python; - + use crate::plugin::board::Board; - use crate::plugin::coordinate::{CubeCoordinates, CubeDirection}; - use crate::plugin::field::{Field, FieldType}; + use crate::plugin::coordinate::{ CubeCoordinates, CubeDirection }; + use crate::plugin::field::{ Field, FieldType }; use crate::plugin::game_state::GameState; use crate::plugin::segment::Segment; - use crate::plugin::ship::{Ship, TeamEnum}; + use crate::plugin::ship::{ Ship, TeamEnum }; use super::*; @@ -101,7 +101,7 @@ mod tests { None, None, None, - None, + None ); team_one.speed = 5; team_one.movement = 5; @@ -115,7 +115,7 @@ mod tests { None, None, None, - None, + None ); team_two.speed = 5; team_two.movement = 5; @@ -125,7 +125,7 @@ mod tests { 0, team_one.clone(), team_two.clone(), - None, + None ); game_state } diff --git a/src/plugin/board.rs b/src/plugin/board.rs index 3cd973a..62d628b 100644 --- a/src/plugin/board.rs +++ b/src/plugin/board.rs @@ -1,4 +1,5 @@ -use std::collections::{ VecDeque, HashSet }; +use std::cmp::Ordering; +use std::collections::{ HashSet, BinaryHeap }; use pyo3::prelude::*; @@ -169,7 +170,7 @@ impl Board { .next() } - /// A function `find_nearest_field_types` to find the nearest field(s) of a specific type from a starting point in a 3D grid. + /// A function `find_nearest_field_types` to find the nearest field(s) of a specific type from a starting point in a hexagonal grid. /// /// # Arguments /// @@ -203,29 +204,26 @@ impl Board { /// ``` /// pub fn find_nearest_field_types( - &mut self, + &self, start_coordinates: &CubeCoordinates, field_type: FieldType ) -> HashSet { - let mut nearest_coordinates: HashSet = HashSet::new(); - let mut queue: VecDeque<(CubeCoordinates, i32)> = VecDeque::from( - vec![(start_coordinates.clone(), 0)] - ); - let mut last_distance: i32 = 0; - - let max_fields: i32 = - (self.segments.len() as i32) * + let max_fields: usize = ((self.segments.len() as i32) * PluginConstants::SEGMENT_FIELDS_HEIGHT * - PluginConstants::SEGMENT_FIELDS_WIDTH; - let mut visited: HashSet = HashSet::new(); - - while let Some((current_coords, distance)) = queue.pop_front() { - if !nearest_coordinates.is_empty() && distance > last_distance { + PluginConstants::SEGMENT_FIELDS_WIDTH) as usize; + let mut nearest_coordinates: HashSet = HashSet::with_capacity(max_fields); + let mut visited: HashSet = HashSet::with_capacity(max_fields); + let mut queue: BinaryHeap = BinaryHeap::new(); + queue.push(QueueItem { + coordinates: start_coordinates.clone(), + distance: 0, + }); + + while let Some(QueueItem { coordinates: current_coords, distance }) = queue.pop() { + if self.found_fields(start_coordinates, nearest_coordinates.clone(), distance) { break; } - last_distance = distance; - if let Some(field) = self.get(¤t_coords) { if field.field_type == field_type { nearest_coordinates.insert(current_coords.clone()); @@ -233,24 +231,62 @@ impl Board { } self.neighboring_coordinates(¤t_coords) - .iter() - .filter_map(|neighbor| neighbor.clone()) - .for_each(|coord| queue.push_back((coord, distance + 1))); - - visited.insert(current_coords.clone()); - if visited.len() >= (max_fields as usize) { - break; - } + .into_iter() + .filter_map(|coord| coord) + .filter(|coord| visited.insert(coord.clone())) + .for_each(|coord| + queue.push(QueueItem { + coordinates: coord, + distance: distance + 1, + }) + ); } nearest_coordinates } + fn found_fields( + &self, + start_coordinates: &CubeCoordinates, + nearest_coordinates: HashSet, + distance: i32 + ) -> bool { + !nearest_coordinates.is_empty() && + distance > + start_coordinates.distance_to( + nearest_coordinates.iter().next().unwrap_or(start_coordinates) + ) + } + fn __repr__(&self) -> PyResult { Ok(format!("Board(segments={:?}, next_direction={:?})", self.segments, self.next_direction)) } } +#[derive(Eq)] +struct QueueItem { + coordinates: CubeCoordinates, + distance: i32, +} + +impl Ord for QueueItem { + fn cmp(&self, other: &Self) -> Ordering { + other.distance.cmp(&self.distance) + } +} + +impl PartialOrd for QueueItem { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for QueueItem { + fn eq(&self, other: &Self) -> bool { + self.distance == other.distance + } +} + #[cfg(test)] mod tests { use crate::plugin::field::Passenger; diff --git a/src/plugin/constants.rs b/src/plugin/constants.rs index 4148f84..695b486 100644 --- a/src/plugin/constants.rs +++ b/src/plugin/constants.rs @@ -28,8 +28,12 @@ impl PluginConstants { // Board Fields pub const MAX_SPECIAL: i32 = 0; + // Sandbanks disabled pub const MIN_SPECIAL: i32 = 0; pub const MAX_ISLANDS: i32 = 3; pub const MIN_ISLANDS: i32 = 2; + + // Recursion depth + pub const MAX_DEPTH: usize = 10; } diff --git a/src/plugin/coordinate.rs b/src/plugin/coordinate.rs index bd2ab7c..c8ed5e9 100644 --- a/src/plugin/coordinate.rs +++ b/src/plugin/coordinate.rs @@ -284,6 +284,10 @@ impl CubeDirection { CubeDirection::UpRight => 5, } } + + pub fn __hash__(&self) -> i32 { + self.ordinal() + } } impl fmt::Display for CubeDirection { diff --git a/src/plugin/game_state.rs b/src/plugin/game_state.rs index 80a69af..765e8cc 100644 --- a/src/plugin/game_state.rs +++ b/src/plugin/game_state.rs @@ -14,12 +14,12 @@ use crate::plugin::board::Board; use crate::plugin::constants::PluginConstants; use crate::plugin::coordinate::{ CubeCoordinates, CubeDirection }; use crate::plugin::errors::movement_error::MoveMistake; -use crate::plugin::field::{ FieldType, Field }; +use crate::plugin::field::FieldType; use crate::plugin::r#move::Move; use crate::plugin::ship::Ship; use crate::plugin::errors::advance_errors::AdvanceProblem; -use super::field::Passenger; +use super::field::Field; use super::ship::TeamEnum; #[pyclass] @@ -83,13 +83,16 @@ mod advance_info_tests { costs: vec![1, 2, 3, 4, 5], problem: AdvanceProblem::FieldIsBlocked, }; - assert_eq!(advance_info.advances(None), vec![ - Advance { distance: 1 }, - Advance { distance: 2 }, - Advance { distance: 3 }, - Advance { distance: 4 }, - Advance { distance: 5 }, - ]); + assert_eq!( + advance_info.advances(None), + vec![ + Advance { distance: 1 }, + Advance { distance: 2 }, + Advance { distance: 3 }, + Advance { distance: 4 }, + Advance { distance: 5 } + ] + ); } #[test] @@ -182,84 +185,127 @@ impl GameState { self.board.does_field_have_stream(&self.current_ship.position) } - fn perform_move(&self, move_: Move) -> Result { - let mut new_state: GameState = self.clone(); - - debug!("Current ship before move: {:?}", new_state.current_ship); - debug!("Other ship before move: {:?}", new_state.other_ship); - - let actions: &Vec = &move_.actions; - if actions.is_empty() { - return Err(PyBaseException::new_err(MoveMistake::NoActions.message())); - } - - debug!("Actions: {:?}", actions); + pub fn perform_action(&self, action: Action) -> Result { + let mut new_state = self.clone(); - for (i, action) in actions.iter().enumerate() { - match action { - Action::Push(_) if !new_state.must_push() => { - return Err(PyBaseException::new_err(MoveMistake::PushActionRequired.message())); - } - Action::Accelerate(_) if i != 0 => { - return Err( - PyBaseException::new_err(MoveMistake::FirstActionAccelerate.message()) - ); - } - Action::Advance(ad) => { - let future_field = new_state.board.get( - &( - new_state.current_ship.position + - new_state.current_ship.direction.vector() * ad.distance - ) - ); - if - future_field.is_some() && - future_field.unwrap().field_type == FieldType::Sandbank - { - return Err(PyBaseException::new_err(MoveMistake::SandBankEnd.message())); + match action { + Action::Accelerate(accelerate) => { + match accelerate.perform(&new_state) { + Ok(updated_ship) => { + new_state.current_ship = updated_ship; + } + Err(e) => { + return Err(e); } } - _ => {} } - - let new_current_ship: Result; - let mut new_other_ship: Result = Ok(new_state.other_ship); - - match action { - Action::Accelerate(accelerate) => { - new_current_ship = accelerate.perform(&new_state); - } - Action::Advance(advance) => { - new_current_ship = advance.perform(&new_state); + Action::Advance(advance) => { + match advance.perform(&new_state) { + Ok(updated_ship) => { + new_state.current_ship = updated_ship; + } + Err(e) => { + return Err(e); + } } - Action::Push(push) => { - let ship_tuple: (Ship, Ship) = push.perform(&new_state)?; - new_current_ship = Ok(ship_tuple.0); - new_other_ship = Ok(ship_tuple.1); + } + Action::Turn(turn) => { + match turn.perform(&new_state) { + Ok(updated_ship) => { + new_state.current_ship = updated_ship; + } + Err(e) => { + return Err(e); + } } - Action::Turn(turn) => { - new_current_ship = turn.perform(&new_state); + } + Action::Push(push) => { + match push.perform(&new_state) { + Ok((updated_current_ship, updated_other_ship)) => { + new_state.current_ship = updated_current_ship; + new_state.other_ship = updated_other_ship; + } + Err(e) => { + return Err(e); + } } } - - new_state.current_ship = new_current_ship?; - new_state.other_ship = new_other_ship?; } - match new_state.current_ship.movement { - p if p > 0 => { - return Err(PyBaseException::new_err(MoveMistake::MovementPointsLeft.message())); + Ok(new_state) + } + + fn move_pre_check(&self, action: Action, action_idx: usize, ship: Ship) -> Result<(), PyErr> { + match action { + Action::Push(_) if !self.must_push() => { + return Err(PyBaseException::new_err(MoveMistake::PushActionRequired.message())); + } + Action::Accelerate(_) if action_idx != 0 => { + return Err(PyBaseException::new_err(MoveMistake::FirstActionAccelerate.message())); } - p if p < 0 => { - return Err(PyBaseException::new_err(MoveMistake::MovementPointsMissing.message())); + Action::Advance(ad) if + self.board + .get(&(ship.position + ship.direction.vector() * ad.distance)) + .map_or(false, |f| f.field_type == FieldType::Sandbank) + => { + return Err(PyBaseException::new_err(MoveMistake::SandBankEnd.message())); } _ => {} } + Ok(()) + } + + fn move_after_check(&self, ship: Ship) -> Result<(), PyErr> { + if ship.movement != 0 { + return Err( + PyBaseException::new_err( + if ship.movement > 0 { + MoveMistake::MovementPointsLeft.message() + } else { + MoveMistake::MovementPointsMissing.message() + } + ) + ); + } + + Ok(()) + } + + pub fn perform_move(&self, move_: Move) -> Result { + debug!("Current ship before move: {:?}", self.current_ship); + debug!("Other ship before move: {:?}", self.other_ship); + + if move_.actions.is_empty() { + return Err(PyBaseException::new_err(MoveMistake::NoActions.message())); + } + + let mut new_state = self.clone(); + debug!("Actions: {:?}", move_.actions); + + for (i, action) in move_.actions.iter().enumerate() { + new_state.move_pre_check(*action, i, self.current_ship).map_err(|e| { + return e; + })?; + match new_state.perform_action(*action) { + Ok(state) => { + new_state = state; + } + Err(e) => { + return Err(PyBaseException::new_err(e)); + } + } + } + + new_state.move_after_check(new_state.current_ship).map_err(|e| { + return e; + })?; + new_state.pick_up_passenger_current_ship(); new_state.current_ship.points = new_state .ship_points(new_state.current_ship) .expect("Could not calculate ship points"); + if move_.actions.iter().any(|a| matches!(a, Action::Push(_))) { if new_state.other_ship.speed == 1 { new_state.pick_up_passenger_other_ship(); @@ -279,19 +325,16 @@ impl GameState { } pub fn advance_turn(&mut self) { - let current_ship: &mut Ship = &mut self.current_ship; - - current_ship.free_acc = 1; - current_ship.free_turns = 1; - current_ship.movement = current_ship.speed; + self.current_ship.free_acc = 1; + self.current_ship.free_turns = 1; + self.current_ship.movement = self.current_ship.speed; self.turn += 1; - if self.turn % 2 == 0 { - if self.determine_ahead_team() != self.current_ship { - swap(&mut self.current_ship, &mut self.other_ship); - } - } else { + if + (self.turn % 2 == 0 && self.determine_ahead_team() != self.current_ship) || + self.turn % 2 != 0 + { swap(&mut self.current_ship, &mut self.other_ship); } @@ -306,18 +349,21 @@ impl GameState { } pub fn remove_passenger_at(&mut self, coord: CubeCoordinates) -> bool { - let mut passenger_removed = false; - for d in CubeDirection::VALUES { - if let Some(mut field) = self.board.get_field_in_direction(&d, &coord) { - if let Some(Passenger { passenger, direction }) = &mut field.passenger { - if *passenger > 0 && *direction == d.opposite() { - *passenger -= 1; - passenger_removed = true; - } - } - } - } - passenger_removed + CubeDirection::VALUES.iter().any(|&d| { + self.board + .get_field_in_direction(&d, &coord) + .and_then(|mut field| { + field.passenger.as_mut().and_then(|passenger| { + if passenger.passenger > 0 && passenger.direction == d.opposite() { + passenger.passenger -= 1; + Some(()) + } else { + None + } + }) + }) + .is_some() + }) } pub fn pick_up_passenger_current_ship(&mut self) { @@ -366,115 +412,196 @@ impl GameState { direction: &CubeDirection, max_movement_points: i32 ) -> AdvanceInfo { - let mut current_position: CubeCoordinates = *start; - let mut total_cost: i32 = 0; - let mut has_current: bool = false; - let max_movement: i32 = max_movement_points.clamp(0, PluginConstants::MAX_SPEED); + let max_movement = max_movement_points.clamp(0, PluginConstants::MAX_SPEED); + let mut current_position = *start; + let mut total_cost = 0; let mut costs: Vec = Vec::new(); - - macro_rules! result { - ($problem:expr) => { - AdvanceInfo { costs, problem: $problem } - }; - } + let mut has_current = false; while total_cost < max_movement { current_position += direction.vector(); total_cost += 1; - let current_field_option: Option = self.board.get(¤t_position); - if current_field_option.is_none() || !current_field_option.unwrap().is_empty() { - return result!(AdvanceProblem::FieldIsBlocked); - } + match self.board.get(¤t_position) { + Some(field) if field.is_empty() => { + if self.board.does_field_have_stream(¤t_position) && !has_current { + has_current = true; + if total_cost < max_movement { + total_cost += 1; + } else { + break; + } + } - if !has_current && self.board.does_field_have_stream(¤t_position) { - has_current = true; - if total_cost >= max_movement { - break; - } - total_cost += 1; - } + if + self.current_ship.position == current_position || + self.other_ship.position == current_position + { + if total_cost < max_movement { + costs.push(total_cost); + return AdvanceInfo { + costs, + problem: AdvanceProblem::ShipAlreadyInTarget, + }; + } + return AdvanceInfo { costs, problem: AdvanceProblem::InsufficientPush }; + } - if - self.current_ship.position == current_position || - self.other_ship.position == current_position - { - if total_cost < max_movement { + if let FieldType::Sandbank = field.field_type { + return AdvanceInfo { costs, problem: AdvanceProblem::MoveEndOnSandbank }; + } costs.push(total_cost); - return result!(AdvanceProblem::ShipAlreadyInTarget); } - return result!(AdvanceProblem::InsufficientPush); + _ => { + return AdvanceInfo { costs, problem: AdvanceProblem::FieldIsBlocked }; + } } + } + + return AdvanceInfo { costs, problem: AdvanceProblem::MovementPointsMissing }; + } + + fn merge_consecutive_advances(&self, actions: Vec) -> Vec { + let mut merged_actions = vec![]; + let mut iter = actions.into_iter().peekable(); - if let FieldType::Sandbank = current_field_option.unwrap().field_type { - return result!(AdvanceProblem::MoveEndOnSandbank); + while let Some(action) = iter.next() { + match action { + Action::Advance(advance) => { + let mut total_distance = advance.distance; + while matches!(iter.peek(), Some(Action::Advance(_))) { + if let Some(Action::Advance(a)) = iter.next() { + total_distance += a.distance; + } + } + merged_actions.push(Action::Advance(Advance { distance: total_distance })); + } + _ => merged_actions.push(action), } + } + + merged_actions + } + + pub fn possible_moves(&self, depth: Option) -> Vec { + self.possible_action_comb(&self, vec![], 0, depth.unwrap_or(PluginConstants::MAX_DEPTH)) + .into_iter() + .map(|actions| Move { actions }) + .collect() + } - costs.push(total_cost); + pub fn possible_action_comb( + &self, + current_state: &GameState, + current_actions: Vec, + depth: usize, + max_depth: usize + ) -> Vec> { + if depth > max_depth || (!current_state.can_move() && !current_state.must_push()) { + return current_state + .move_after_check(current_state.current_ship) + .map_or(vec![], |_| + vec![current_state.merge_consecutive_advances(current_actions)] + ); } - result!(AdvanceProblem::MovementPointsMissing) + current_state + .possible_actions(depth, None) + .iter() + .filter_map(|&action| { + current_state + .perform_action(action) + .ok() + .and_then(|new_state| { + let mut new_actions = current_actions.clone(); + new_actions.push(action); + Some( + self.possible_action_comb(&new_state, new_actions, depth + 1, max_depth) + ) + }) + }) + .flatten() + .collect() } - pub fn possible_accelerations(&self) -> Vec { + pub fn possible_accelerations(&self, max_coal: Option) -> Vec { if self.must_push() { return Vec::new(); } let ship = self.current_ship; - return (1..=self.current_ship.coal + ship.free_acc) - .flat_map(|i| [i, -i]) - .filter(|&i| ( - if i > 0 { - PluginConstants::MAX_SPEED <= ship.speed + i + let max_coal = max_coal.unwrap_or(ship.coal.try_into().unwrap()); + let max_possible_acceleration = max_coal + (ship.free_acc as usize); + + (1..=max_possible_acceleration as i32) + .flat_map(|i| { + let positive = if PluginConstants::MAX_SPEED >= ship.speed + i { + Some(Accelerate { acc: i }) } else { - PluginConstants::MIN_SPEED >= ship.speed - i - } - )) - .map(Accelerate::new) - .collect(); + None + }; + let negative = if PluginConstants::MIN_SPEED <= ship.speed - i { + Some(Accelerate { acc: -i }) + } else { + None + }; + positive.into_iter().chain(negative) + }) + .collect() } pub fn possible_pushes(&self) -> Vec { - let ship = self.current_ship; - if !self.must_push() || self.board.is_sandbank(&ship.position) || ship.movement < 1 { + if + self.board.get_field_in_direction( + &self.current_ship.direction, + &self.current_ship.position + ) == Some(Field::new(FieldType::Sandbank, None)) || + !self.must_push() || + self.current_ship.movement < 1 + { return Vec::new(); } CubeDirection::VALUES.into_iter() - .filter( - |&d| - d != ship.direction.opposite() && - self.board.get(&(ship.position + d.vector())).is_some() && + .filter(|&dir| { + dir != self.current_ship.direction.opposite() && self.board - .get(&(ship.position + d.vector())) - .unwrap() - .is_empty() - ) - .map(Push::new) + .get_field_in_direction(&dir, &self.current_ship.position) + .map_or(false, |f| f.is_empty()) + }) + .map(|dir| Push { direction: dir }) .collect() } - pub fn possible_turns(&self) -> Vec { - let ship = self.current_ship; - if self.must_push() || self.board.is_sandbank(&ship.position) { + pub fn possible_turns(&self, max_coal: Option) -> Vec { + if + self.board.get(&self.current_ship.position) == + Some(Field::new(FieldType::Sandbank, None)) || + self.must_push() + { return Vec::new(); } - let max_turn_count = (ship.coal + ship.free_turns).min(3) as i32; + + let max_coal = max_coal.unwrap_or(self.current_ship.coal as usize); + let max_turn_count = std::cmp::min(max_coal + (self.current_ship.free_turns as usize), 3); + (1..=max_turn_count) - .flat_map(|i| [i, -i]) - .map(|turns| Turn::new(ship.direction.rotated_by(turns))) + .flat_map(|i| { + vec![ + Turn::new(self.current_ship.direction.rotated_by(i as i32)), + Turn::new(self.current_ship.direction.rotated_by(-(i as i32))) + ] + }) .take(5) .collect() } pub fn possible_advances(&self) -> Vec { - let ship = self.current_ship; - if ship.movement < 1 || self.must_push() { + if self.current_ship.movement < 1 || self.must_push() { return Vec::new(); } - self.check_ship_advance_limit(&ship).advances(None) + self.check_ship_advance_limit(&self.current_ship).advances(None) } #[allow(unused_variables)] @@ -485,13 +612,16 @@ impl GameState { ); } - pub fn possible_actions(&self, rank: i32) -> Vec { + pub fn possible_actions(&self, rank: usize, max_coal: Option) -> Vec { + let max_coal = max_coal.unwrap_or(self.current_ship.coal.try_into().unwrap()); let mut actions: Vec = Vec::new(); if rank == 0 { - actions.extend(self.possible_accelerations().into_iter().map(Action::Accelerate)); + actions.extend( + self.possible_accelerations(Some(max_coal)).into_iter().map(Action::Accelerate) + ); } - actions.extend(self.possible_turns().into_iter().map(Action::Turn)); + actions.extend(self.possible_turns(Some(max_coal)).into_iter().map(Action::Turn)); actions.extend(self.possible_advances().into_iter().map(Action::Advance)); if rank != 0 { actions.extend(self.possible_pushes().into_iter().map(Action::Push)); @@ -503,9 +633,9 @@ impl GameState { pub fn can_move(&self) -> bool { let current_ship_can_advance: bool = !self.possible_advances().is_empty(); - let current_ship_can_turn: bool = !self.possible_turns().is_empty(); + let current_ship_can_turn: bool = !self.possible_turns(None).is_empty(); - let current_ship_can_accelerate: bool = !self.possible_accelerations().is_empty(); + let current_ship_can_accelerate: bool = !self.possible_accelerations(None).is_empty(); current_ship_can_advance || current_ship_can_turn || current_ship_can_accelerate } @@ -573,78 +703,46 @@ mod tests { use super::*; - #[test] - fn test_check_advance_limit() { - let segment: Vec = vec![Segment { - direction: CubeDirection::Right, - center: CubeCoordinates::new(0, 0), + fn create_water_segment(center: CubeCoordinates, direction: CubeDirection) -> Segment { + Segment { + direction, + center, fields: vec![ - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ] + vec![Field::new(FieldType::Water, None); 4]; + 5 ], - }]; - let board: Board = Board::new(segment, CubeDirection::Right); - let team_one: &mut Ship = &mut Ship::new( - CubeCoordinates::new(0, 0), - TeamEnum::One, - None, - None, - None, - None, - None, - None, - None - ); + } + } + + fn create_ship(position: CubeCoordinates, team: TeamEnum) -> Ship { + Ship::new(position, team, None, None, None, None, None, None, None) + } + + fn create_game_state(segment: Vec, team_one: Ship, team_two: Ship) -> GameState { + GameState::new(Board::new(segment, CubeDirection::Right), 0, team_one, team_two, None) + } + + #[test] + fn find_possible_moves_returns_correct_count() { + let segment = vec![create_water_segment(CubeCoordinates::new(0, 0), CubeDirection::Right)]; + let team_one = create_ship(CubeCoordinates::new(0, -1), TeamEnum::One); + let team_two = create_ship(CubeCoordinates::new(-1, 1), TeamEnum::Two); + let game_state = create_game_state(segment, team_one, team_two); + + let possible_moves = game_state.possible_action_comb(&game_state, vec![], 0, 5); + assert_eq!(possible_moves.len(), 6725); + } + + #[test] + fn test_check_advance_limit() { + let segment = vec![create_water_segment(CubeCoordinates::new(0, 0), CubeDirection::Right)]; + let mut team_one = create_ship(CubeCoordinates::new(0, 0), TeamEnum::One); team_one.speed = 5; team_one.movement = 5; - let team_two: &mut Ship = &mut Ship::new( - CubeCoordinates::new(-1, 1), - TeamEnum::Two, - None, - None, - None, - None, - None, - None, - None - ); + let mut team_two = create_ship(CubeCoordinates::new(-1, 1), TeamEnum::Two); team_two.speed = 5; team_two.movement = 5; - let game_state: GameState = GameState::new( - board, - 0, - team_one.clone(), - team_two.clone(), - None - ); + let game_state = create_game_state(segment, team_one, team_two); let advances: AdvanceInfo = game_state.check_ship_advance_limit(&team_one); @@ -653,234 +751,67 @@ mod tests { assert_eq!(advances.distance(), 3); } + #[test] + fn test_check_advance_limit_to_upperleft_end() { + let segment = vec![create_water_segment(CubeCoordinates::new(0, 0), CubeDirection::Right)]; + let mut team_one = create_ship(CubeCoordinates::new(0, -1), TeamEnum::One); + team_one.speed = 5; + team_one.movement = 5; + team_one.direction = CubeDirection::Left; + let mut team_two = create_ship(CubeCoordinates::new(-1, 1), TeamEnum::Two); + team_two.speed = 5; + team_two.movement = 5; + let game_state = create_game_state(segment, team_one, team_two); + + let advances: AdvanceInfo = game_state.check_ship_advance_limit(&team_one); + + assert_eq!(advances.costs, vec![1]); + assert_eq!(advances.problem, AdvanceProblem::FieldIsBlocked); + assert_eq!(advances.distance(), 1); + } + #[test] fn test_get_accelerations() { - let segment: Vec = vec![Segment { - direction: CubeDirection::Right, - center: CubeCoordinates::new(0, 0), - fields: vec![ - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ] - ], - }]; - let board: Board = Board::new(segment, CubeDirection::Right); - let team_one: &mut Ship = &mut Ship::new( - CubeCoordinates::new(0, -1), - TeamEnum::One, - None, - None, - None, - None, - None, - None, - None - ); + let segment = vec![create_water_segment(CubeCoordinates::new(0, 0), CubeDirection::Right)]; + let mut team_one = create_ship(CubeCoordinates::new(0, 0), TeamEnum::One); team_one.speed = 5; team_one.movement = 5; - let team_two: &mut Ship = &mut Ship::new( - CubeCoordinates::new(-1, 1), - TeamEnum::Two, - None, - None, - None, - None, - None, - None, - None - ); + let mut team_two = create_ship(CubeCoordinates::new(-1, 1), TeamEnum::Two); team_two.speed = 5; team_two.movement = 5; - let game_state: GameState = GameState::new( - board, - 0, - team_one.clone(), - team_two.clone(), - None - ); + let game_state = create_game_state(segment, team_one, team_two); - let accelerations: Vec = game_state.possible_accelerations(); - assert_eq!(accelerations.len(), 7); - assert_eq!(accelerations[4].acc, 5); + let accelerations: Vec = game_state.possible_accelerations(None); + assert_eq!(accelerations.len(), 5); + assert_eq!(accelerations[4].acc, -4); } #[test] fn test_get_turns() { - let segment: Vec = vec![Segment { - direction: CubeDirection::Right, - center: CubeCoordinates::new(0, 0), - fields: vec![ - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ] - ], - }]; - let board: Board = Board::new(segment, CubeDirection::Right); - let team_one: &mut Ship = &mut Ship::new( - CubeCoordinates::new(0, -1), - TeamEnum::One, - None, - None, - None, - None, - None, - None, - None - ); + let segment = vec![create_water_segment(CubeCoordinates::new(0, 0), CubeDirection::Right)]; + let mut team_one = create_ship(CubeCoordinates::new(0, 0), TeamEnum::One); team_one.speed = 5; team_one.movement = 5; - let team_two: &mut Ship = &mut Ship::new( - CubeCoordinates::new(-1, 1), - TeamEnum::Two, - None, - None, - None, - None, - None, - None, - None - ); + let mut team_two = create_ship(CubeCoordinates::new(-1, 1), TeamEnum::Two); team_two.speed = 5; team_two.movement = 5; - let game_state: GameState = GameState::new( - board, - 0, - team_one.clone(), - team_two.clone(), - None - ); + let game_state = create_game_state(segment, team_one, team_two); - let turns: Vec = game_state.possible_turns(); + let turns: Vec = game_state.possible_turns(None); assert_eq!(turns.len(), 5); assert_eq!(turns[4].direction, CubeDirection::Left); } #[test] fn test_get_advances() { - let segment: Vec = vec![Segment { - direction: CubeDirection::Right, - center: CubeCoordinates::new(0, 0), - fields: vec![ - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ] - ], - }]; - let board: Board = Board::new(segment, CubeDirection::Right); - let team_one: &mut Ship = &mut Ship::new( - CubeCoordinates::new(0, -1), - TeamEnum::One, - None, - None, - None, - None, - None, - None, - None - ); + let segment = vec![create_water_segment(CubeCoordinates::new(0, 0), CubeDirection::Right)]; + let mut team_one = create_ship(CubeCoordinates::new(0, 0), TeamEnum::One); team_one.speed = 5; team_one.movement = 5; - let team_two: &mut Ship = &mut Ship::new( - CubeCoordinates::new(-1, 1), - TeamEnum::Two, - None, - None, - None, - None, - None, - None, - None - ); + let mut team_two = create_ship(CubeCoordinates::new(-1, 1), TeamEnum::Two); team_two.speed = 5; team_two.movement = 5; - let game_state: GameState = GameState::new( - board, - 0, - team_one.clone(), - team_two.clone(), - None - ); + let game_state = create_game_state(segment, team_one, team_two); let advances: Vec = game_state.possible_advances(); assert_eq!(advances.len(), 3); @@ -889,82 +820,36 @@ mod tests { #[test] fn test_get_pushes() { - let segment: Vec = vec![Segment { - direction: CubeDirection::Right, - center: CubeCoordinates::new(0, 0), - fields: vec![ - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ] - ], - }]; - let board: Board = Board::new(segment, CubeDirection::Right); - let team_one: &mut Ship = &mut Ship::new( - CubeCoordinates::new(0, 0), - TeamEnum::One, - None, - None, - None, - None, - None, - None, - None - ); + let segment = vec![create_water_segment(CubeCoordinates::new(0, 0), CubeDirection::Right)]; + let mut team_one = create_ship(CubeCoordinates::new(0, 0), TeamEnum::One); team_one.speed = 5; team_one.movement = 5; - let team_two: &mut Ship = &mut Ship::new( - CubeCoordinates::new(0, 0), - TeamEnum::Two, - None, - None, - None, - None, - None, - None, - None - ); + let mut team_two = create_ship(CubeCoordinates::new(0, 0), TeamEnum::Two); team_two.speed = 5; team_two.movement = 5; - let game_state: GameState = GameState::new( - board, - 0, - team_one.clone(), - team_two.clone(), - None - ); + let game_state = create_game_state(segment, team_one, team_two); let pushes: Vec = game_state.possible_pushes(); assert_eq!(pushes.len(), 5); assert_eq!(pushes[0].direction, CubeDirection::Right); } + #[test] + fn test_only_pushes_if_must_push() { + let segment = vec![create_water_segment(CubeCoordinates::new(0, 0), CubeDirection::Right)]; + let mut team_one = create_ship(CubeCoordinates::new(0, 0), TeamEnum::One); + team_one.speed = 5; + team_one.movement = 5; + let mut team_two = create_ship(CubeCoordinates::new(0, 0), TeamEnum::Two); + team_two.speed = 5; + team_two.movement = 5; + let game_state = create_game_state(segment, team_one, team_two); + + let actions: Vec = game_state.possible_actions(1, None); + assert_eq!(actions.len(), 5); + assert!(actions.iter().all(|a| matches!(a, Action::Push(_)))); + } + #[test] fn test_performe_move() { let segment: Vec = vec![ @@ -1102,76 +987,10 @@ mod tests { #[test] fn test_advance_turn() { - let segment: Vec = vec![Segment { - direction: CubeDirection::Right, - center: CubeCoordinates::new(0, 0), - fields: vec![ - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ] - ], - }]; - let board: Board = Board::new(segment, CubeDirection::Right); - let team_one: &mut Ship = &mut Ship::new( - CubeCoordinates::new(0, -1), - TeamEnum::One, - None, - None, - None, - None, - None, - None, - None - ); - team_one.speed = 5; - team_one.movement = 5; - let team_two: &mut Ship = &mut Ship::new( - CubeCoordinates::new(-1, 1), - TeamEnum::Two, - None, - None, - None, - None, - None, - None, - None - ); - team_two.speed = 5; - team_two.movement = 5; - let game_state: &mut GameState = &mut GameState::new( - board, - 0, - team_one.clone(), - team_two.clone(), - None - ); + let segment = vec![create_water_segment(CubeCoordinates::new(0, 0), CubeDirection::Right)]; + let team_one = create_ship(CubeCoordinates::new(0, -1), TeamEnum::One); + let team_two = create_ship(CubeCoordinates::new(-1, 1), TeamEnum::Two); + let mut game_state = create_game_state(segment, team_one, team_two); assert_eq!(game_state.current_ship.team, TeamEnum::One); assert_eq!(game_state.other_ship.team, TeamEnum::Two); @@ -1189,72 +1008,10 @@ mod tests { #[test] fn test_team_ahead() { - let segment: Vec = vec![Segment { - direction: CubeDirection::Right, - center: CubeCoordinates::new(0, 0), - fields: vec![ - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ], - vec![ - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None), - Field::new(FieldType::Water, None) - ] - ], - }]; - let board: Board = Board::new(segment, CubeDirection::Right); - let team_one: &mut Ship = &mut Ship::new( - CubeCoordinates::new(0, -1), - TeamEnum::One, - None, - None, - None, - None, - None, - None, - None - ); - let team_two: &mut Ship = &mut Ship::new( - CubeCoordinates::new(-1, 1), - TeamEnum::Two, - None, - None, - None, - None, - None, - None, - None - ); - let game_state: &mut GameState = &mut GameState::new( - board, - 0, - team_one.clone(), - team_two.clone(), - None - ); + let segment = vec![create_water_segment(CubeCoordinates::new(0, 0), CubeDirection::Right)]; + let team_one = create_ship(CubeCoordinates::new(0, -1), TeamEnum::One); + let team_two = create_ship(CubeCoordinates::new(-1, 1), TeamEnum::Two); + let game_state = create_game_state(segment, team_one, team_two); assert_eq!(game_state.determine_ahead_team().team, TeamEnum::One); diff --git a/src/plugin/segment.rs b/src/plugin/segment.rs index 714ab1c..eba503a 100644 --- a/src/plugin/segment.rs +++ b/src/plugin/segment.rs @@ -119,8 +119,6 @@ mod tests { segment.fields[0][0] = Field::new(FieldType::Island, None); segment.fields[3][4] = Field::new(FieldType::Island, None); - println!("{:?}", segment.fields); - assert_eq!( segment.get(CubeCoordinates::new(-1, -2)), Some(Field::new(FieldType::Island, None)) diff --git a/src/plugin/ship.rs b/src/plugin/ship.rs index 8f0bfe8..8bdccf7 100644 --- a/src/plugin/ship.rs +++ b/src/plugin/ship.rs @@ -1,6 +1,6 @@ use pyo3::prelude::*; -use super::{ constants::PluginConstants, coordinate::{ CubeCoordinates, CubeDirection } }; +use super::{ constants::PluginConstants, coordinate::{ CubeCoordinates, CubeDirection }, game_state::AdvanceInfo }; #[derive(PartialEq, Eq, PartialOrd, Clone, Debug, Hash, Copy)] #[pyclass] @@ -89,6 +89,15 @@ impl Ship { self.movement = self.speed; } + pub fn direction(&self, reverse: bool) -> CubeDirection { + if reverse { self.direction.opposite() } else { self.direction } + } + + pub fn update_position(&mut self, distance: i32, advance_info: AdvanceInfo) { + self.position += self.direction.vector() * distance; + self.movement -= advance_info.cost_until(distance as usize); + } + fn __repr__(&self) -> PyResult { Ok( format!(