generated from GarandPLG/rust-flake-template
Rename iron to metal, add mining and recruitment
- Updated CLI, README and documentation to use `--starting-metal` with `--starting-iron` retained as an alias. - Introduced `Resources` struct and constants for wood delivery and mine production. - Added new structures `Mine` and `MineConstruction` with cost, production and cancel logic. - Implemented building view for constructing mines and recruitment view for spawning miners. - Extended keybindings with `Building` and `Recruitment` actions and updated action availability logic. - Modified game state, board, units and UI widgets to handle metal resources and new construction mechanics. - Updated tests and increased total test count to 82, covering resources, construction, recruitment and context‑aware keybindings.
This commit is contained in:
1 parent
305cc2c4a2
commit
a981663152
35 files changed
+2160
-74
No files matched your search
@@ -26,6 +26,10 @@ fn map_has_opposing_bases_ore_and_two_miners() {
|
||||
board.get_ref_cell(5, ore_col).get_structure(),
|
||||
Structures::Ore(_)
|
||||
));
|
||||
assert_eq!(
|
||||
board.get_ref_cell(5, ore_col).get_structure().get_owner(),
|
||||
Players::Unclaimed
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
board
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ fn numeric_options_accept_boundaries_and_reject_out_of_range_values() {
|
||||
("--map-width", 36, 108),
|
||||
("--map-height", 11, 39),
|
||||
("--starting-wood", 1, 150),
|
||||
("--starting-iron", 1, 150),
|
||||
("--starting-metal", 1, 150),
|
||||
("--supply-limit", 49, 149),
|
||||
("--skill-points-limit", 90, 690),
|
||||
] {
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Cell,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{
|
||||
Action, App, GameStates, View, available_skirmish_actions, handle_keybindings,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
BoardState, MoveFocusedCell, Players, Resources, ZoomLevel,
|
||||
resources::MINE_PRODUCTION_TICKS,
|
||||
structures::{
|
||||
BaseBuilding, BuildingTerrain, Mine, MineConstruction, Ore, Structure, Structures, Tunnel,
|
||||
},
|
||||
tasks::Tasks,
|
||||
units::{MinerUnit, Unit, Units},
|
||||
},
|
||||
},
|
||||
threads::{AudioCmd, TICK_MS},
|
||||
},
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn board() -> BoardState {
|
||||
BoardState::new(&Rect::new(0, 0, 160, 50), 42, 11, ZoomLevel::Default, false)
|
||||
}
|
||||
|
||||
fn complete_mine(board: &mut BoardState, coords: (usize, usize)) -> () {
|
||||
assert!(board.start_mine_construction(coords, Players::Player));
|
||||
for _ in 0..MineConstruction::BUILD_TICKS {
|
||||
board.advance_construction();
|
||||
}
|
||||
assert!(matches!(
|
||||
board.cells[coords.0][coords.1].get_structure(),
|
||||
Structures::Mine(_)
|
||||
));
|
||||
}
|
||||
|
||||
fn app() -> App {
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.window_area = Rect::new(0, 0, 160, 50);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
app
|
||||
}
|
||||
|
||||
fn travel(board: &mut BoardState, direction: MoveFocusedCell) -> () {
|
||||
board.toggle_marking();
|
||||
board.change_focused_cell(direction);
|
||||
board.toggle_marking();
|
||||
for _ in 0..3000_u16.div_ceil(u16::from(TICK_MS)) {
|
||||
board.advance_turn();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miner_has_builder_permission_through_all_unit_wrappers() -> () {
|
||||
let miner: MinerUnit = MinerUnit::new(Players::Player, (5, 2));
|
||||
assert!(miner.get_can_build());
|
||||
let unit: Units = Units::Miner(miner);
|
||||
assert!(unit.get_can_build());
|
||||
assert!(Some(unit).get_can_build());
|
||||
let no_unit: Option<Units> = None;
|
||||
assert!(!no_unit.get_can_build());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mine_requires_a_cardinal_friendly_builder_and_preserves_target_on_failure() -> () {
|
||||
let mut board: BoardState = board();
|
||||
let original: Structures = board.cells[5][1].get_structure();
|
||||
board.cells[5][2].get_mut_option_unit().take();
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
*board.cells[5][2].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Enemy, (5, 2))));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
board.cells[5][2].get_mut_option_unit().take();
|
||||
*board.cells[4][2].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (4, 2))));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(board.cells[5][1].get_structure(), original);
|
||||
assert!(!board.start_mine_construction((usize::MAX, 1), Players::Player));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Unclaimed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ore_exception_does_not_require_a_base_or_tunnel_connection() -> () {
|
||||
let mut board: BoardState = board();
|
||||
board.cells[0][0].set_structure(Structures::Ore(Ore::new(Players::Unclaimed, 1)));
|
||||
*board.cells[0][1].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (0, 1))));
|
||||
assert!(board.can_build_at((0, 0), Players::Player, BuildingTerrain::Ore));
|
||||
complete_mine(&mut board, (0, 0));
|
||||
assert_eq!(
|
||||
board.cells[0][0].get_structure().get_owner(),
|
||||
Players::Player
|
||||
);
|
||||
assert!(!board.start_mine_construction((0, 0), Players::Player));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_building_sites_require_stone_connected_to_base_or_tunnel() -> () {
|
||||
let mut board: BoardState = board();
|
||||
assert!(board.can_build_at((5, 3), Players::Player, BuildingTerrain::Stone));
|
||||
assert!(!board.can_build_at((5, 3), Players::Player, BuildingTerrain::Ore));
|
||||
*board.cells[5][5].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (5, 5))));
|
||||
assert!(!board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone));
|
||||
board.cells[5][5].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
assert!(board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone));
|
||||
board.cells[5][5].set_structure(Structures::Base(BaseBuilding::new(Players::Enemy)));
|
||||
assert!(!board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone));
|
||||
board.cells[5][3].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
assert!(!board.can_build_at((5, 3), Players::Player, BuildingTerrain::Stone));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn occupied_depleted_or_claimed_deposit_cannot_be_built_on() -> () {
|
||||
let mut board: BoardState = board();
|
||||
*board.cells[5][1].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (5, 1))));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
board.cells[5][1].get_mut_option_unit().take();
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(5000);
|
||||
board.cells[5][1].set_structure(Structures::Ore(ore));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
board.cells[5][1].set_structure(Structures::Ore(Ore::new(Players::Enemy, 1)));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_view_starts_paid_build_on_enter_and_stays_open() -> () {
|
||||
let mut app: App = app();
|
||||
for _ in 0..2 {
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
|
||||
}
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL),
|
||||
);
|
||||
assert!(available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter));
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 50)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Ready to build: Enter"));
|
||||
assert!(text.contains("Enter - Build Mine"));
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
let state: &SkirmishState = &app.states_ref().unwrap().skirmish;
|
||||
assert!(state.building_view);
|
||||
assert!(matches!(
|
||||
state.board.cells[5][1].get_structure(),
|
||||
Structures::MineConstruction(_)
|
||||
));
|
||||
assert_eq!(state.player_resources.wood, 30);
|
||||
assert_eq!(state.player_resources.metal, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miner_can_travel_into_and_out_of_friendly_mine_with_moving_tasks() -> () {
|
||||
let mut board: BoardState = board();
|
||||
complete_mine(&mut board, (5, 1));
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
board.toggle_marking();
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
board.toggle_marking();
|
||||
let unit: &Units = board.cells[5][2].get_ref_option_unit().as_ref().unwrap();
|
||||
assert!(matches!(
|
||||
unit.get_ref_tasks().front(),
|
||||
Some(Tasks::Moving(_))
|
||||
));
|
||||
for _ in 1..3000_u16.div_ceil(u16::from(TICK_MS)) {
|
||||
board.advance_turn();
|
||||
}
|
||||
assert!(board.cells[5][1].get_ref_option_unit().is_none());
|
||||
board.advance_turn();
|
||||
assert_eq!(board.cells[5][1].get_ref_option_unit().get_coords(), (5, 1));
|
||||
assert!(board.cells[5][2].get_ref_option_unit().is_none());
|
||||
travel(&mut board, MoveFocusedCell::Right);
|
||||
assert!(board.cells[5][1].get_ref_option_unit().is_none());
|
||||
assert_eq!(board.cells[5][2].get_ref_option_unit().get_coords(), (5, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staffed_mine_doubles_production_and_bonus_stops_after_departure() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
complete_mine(&mut game.board, (5, 1));
|
||||
game.turn_counter = MINE_PRODUCTION_TICKS - 1;
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.metal, 26);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
travel(&mut game.board, MoveFocusedCell::Left);
|
||||
game.turn_counter = MINE_PRODUCTION_TICKS - 1;
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.metal, 28);
|
||||
travel(&mut game.board, MoveFocusedCell::Right);
|
||||
game.turn_counter = MINE_PRODUCTION_TICKS - 1;
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.metal, 29);
|
||||
let Structures::Mine(mine) = game.board.cells[5][1].get_structure() else {
|
||||
panic!("mine missing");
|
||||
};
|
||||
assert_eq!(mine.get_ore_amount(), 4996);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mine_rejects_enemy_miners_and_staffing_does_not_overdraw_ore() -> () {
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(4999);
|
||||
let mut mine: Mine = Mine::new(Players::Player, ore).unwrap();
|
||||
let player: Units = Units::Miner(MinerUnit::new(Players::Player, (5, 2)));
|
||||
let enemy: Units = Units::Miner(MinerUnit::new(Players::Enemy, (5, 2)));
|
||||
assert!(mine.accepts_unit(&player));
|
||||
assert!(!mine.accepts_unit(&enemy));
|
||||
assert!(!Structures::Mine(mine).can_enter(&enemy));
|
||||
assert_eq!(mine.produce_with_miner(true), 1);
|
||||
assert_eq!(mine.produce_with_miner(true), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_ore_cannot_start_construction_or_consume_resources() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
let ore: Structures = Structures::Ore(Ore::new(Players::Unclaimed, 1));
|
||||
game.board.cells[5][21].set_structure(ore);
|
||||
let before: Resources = game.player_resources;
|
||||
assert!(!game.start_mine_construction((5, 21), Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
assert_eq!(game.board.cells[5][21].get_structure(), ore);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_payment_is_atomic_and_exact_funds_are_enough() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
for resources in [
|
||||
Resources {
|
||||
wood: 19,
|
||||
metal: 10,
|
||||
},
|
||||
Resources { wood: 20, metal: 9 },
|
||||
] {
|
||||
game.player_resources = resources;
|
||||
assert!(!game.can_start_mine_construction((5, 1), Players::Player));
|
||||
assert!(!game.start_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, resources);
|
||||
assert!(matches!(
|
||||
game.board.cells[5][1].get_structure(),
|
||||
Structures::Ore(_)
|
||||
));
|
||||
}
|
||||
game.player_resources = Mine::COST;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, Resources { wood: 0, metal: 0 });
|
||||
assert!(!game.start_mine_construction((5, 1), Players::Player));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_takes_ten_seconds_and_cannot_produce_or_accept_miners_early() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
for _ in 1..MineConstruction::BUILD_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
let structure: Structures = game.board.cells[5][1].get_structure();
|
||||
let Structures::MineConstruction(site) = structure else {
|
||||
panic!("completed too early");
|
||||
};
|
||||
assert_eq!(site.progress_percent(), 99);
|
||||
assert!(site.into_mine().is_none());
|
||||
assert_eq!(site.cancel().get_ore_amount(), 5000);
|
||||
assert!(
|
||||
!structure.can_enter(
|
||||
game.board.cells[5][2]
|
||||
.get_ref_option_unit()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
)
|
||||
);
|
||||
assert_eq!(game.player_resources.metal, 15);
|
||||
game.tick_update();
|
||||
assert!(matches!(
|
||||
game.board.cells[5][1].get_structure(),
|
||||
Structures::Mine(_)
|
||||
));
|
||||
assert_eq!(game.player_resources.metal, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_pauses_without_nearby_builder_and_resumes_after_return() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
for _ in 0..100 {
|
||||
game.tick_update();
|
||||
}
|
||||
let before: Structures = game.board.cells[5][1].get_structure();
|
||||
let builder: Option<Units> = game.board.cells[5][2].get_mut_option_unit().take();
|
||||
for _ in 0..MineConstruction::BUILD_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.board.cells[5][1].get_structure(), before);
|
||||
assert_eq!(game.player_resources.metal, 15);
|
||||
*game.board.cells[5][2].get_mut_option_unit() = builder;
|
||||
for _ in 100..MineConstruction::BUILD_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(matches!(
|
||||
game.board.cells[5][1].get_structure(),
|
||||
Structures::Mine(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancellation_restores_original_ore_and_refunds_cost_exactly_once() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(123);
|
||||
game.board.cells[5][1].set_structure(Structures::Ore(ore));
|
||||
let before: Resources = game.player_resources;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
for _ in 0..100 {
|
||||
game.tick_update();
|
||||
}
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
assert!(game.cancel_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
assert_eq!(game.board.cells[5][1].get_structure(), Structures::Ore(ore));
|
||||
assert!(!game.cancel_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_shows_progress_and_delete_cancels_while_escape_only_closes_panel() -> () {
|
||||
let mut app: App = app();
|
||||
for _ in 0..2 {
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
|
||||
}
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL),
|
||||
);
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert!(
|
||||
available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Delete)
|
||||
);
|
||||
assert!(
|
||||
!available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter)
|
||||
);
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(app.states_ref().unwrap().skirmish.player_resources.wood, 30);
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 50)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Cost: 20 Wood + 10 Metal"));
|
||||
assert!(text.contains("Construction: 0%"));
|
||||
assert!(text.contains("Delete - Cancel construction"));
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert!(matches!(
|
||||
app.states_ref().unwrap().skirmish.board.cells[5][1].get_structure(),
|
||||
Structures::MineConstruction(_)
|
||||
));
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL),
|
||||
);
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE));
|
||||
assert!(matches!(
|
||||
app.states_ref().unwrap().skirmish.board.cells[5][1].get_structure(),
|
||||
Structures::Ore(_)
|
||||
));
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.player_resources,
|
||||
Resources::new(50, 25)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_cannot_cancel_enemy_site_or_refund_a_completed_mine() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.start_mine_construction((5, 40), Players::Enemy));
|
||||
assert!(!game.cancel_mine_construction((5, 40), Players::Player));
|
||||
assert_eq!(game.player_resources, Resources::new(50, 25));
|
||||
for _ in 0..MineConstruction::BUILD_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(!game.cancel_mine_construction((5, 40), Players::Enemy));
|
||||
assert_eq!(game.enemy_resources.metal, 15);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Cell,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{
|
||||
Action, App, GameStates, View, available_skirmish_actions, handle_keybindings,
|
||||
states::skirmish_states::structures::Structures, threads::AudioCmd,
|
||||
},
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn app() -> App {
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.window_area = Rect::new(0, 0, 160, 40);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
app
|
||||
}
|
||||
|
||||
fn press(app: &mut App, code: KeyCode, modifiers: KeyModifiers) -> () {
|
||||
handle_keybindings(app, KeyEvent::new(code, modifiers));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selecting_miner_switches_help_and_escape_cancels_only_the_route() -> () {
|
||||
let mut app: App = app();
|
||||
assert!(
|
||||
!available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Space)
|
||||
);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
assert!(available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Space));
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
let actions: Vec<Action> = available_skirmish_actions(&app.states_ref().unwrap().skirmish);
|
||||
assert!(actions.contains(&Action::Backspace));
|
||||
assert!(actions.contains(&Action::Delete));
|
||||
assert!(!actions.contains(&Action::Building));
|
||||
assert!(!actions.contains(&Action::ZoomIn));
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
assert!(
|
||||
!app
|
||||
.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.marked_cells
|
||||
.marking_cells
|
||||
);
|
||||
assert!(
|
||||
app
|
||||
.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.get_marked_cells()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn building_view_has_its_own_help_and_does_not_build_on_invalid_tile() -> () {
|
||||
let mut app: App = app();
|
||||
let before: war_in_tunnels::app::states::skirmish_states::BoardState =
|
||||
app.states_ref().unwrap().skirmish.board.clone();
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL);
|
||||
assert!(app.states_ref().unwrap().skirmish.building_view);
|
||||
assert_eq!(
|
||||
available_skirmish_actions(&app.states_ref().unwrap().skirmish),
|
||||
[Action::Building, Action::Esc, Action::Quit2]
|
||||
);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
assert_eq!(app.states_ref().unwrap().skirmish.board, before);
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 40)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Construction unavailable"));
|
||||
assert!(text.contains("Close buildings"));
|
||||
assert!(!text.contains("Marking cells"));
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
for row in &app.states_ref().unwrap().skirmish.board.cells {
|
||||
for cell in row {
|
||||
assert!(!matches!(cell.get_structure(), Structures::Mine(_)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_control_space_and_quit_work_in_building_view() -> () {
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Null, KeyModifiers::NONE);
|
||||
assert!(app.states_ref().unwrap().skirmish.building_view);
|
||||
press(&mut app, KeyCode::Null, KeyModifiers::CONTROL);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
press(&mut app, KeyCode::Null, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL);
|
||||
assert!(app.exit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_help_fits_normal_terminal_and_changes_when_route_is_selected() -> () {
|
||||
let mut app: App = app();
|
||||
app.window_area = Rect::new(0, 0, 120, 40);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(120, 40)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Ctrl + Space - Build"));
|
||||
assert!(text.contains("Space - Select miner"));
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Space - Confirm route"));
|
||||
assert!(text.contains("Esc - Cancel route"));
|
||||
assert!(!text.contains("Ctrl + Space"));
|
||||
assert!(!text.contains("Zoom in"));
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Cell,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{
|
||||
Action, App, GameStates, View, available_skirmish_actions, handle_keybindings,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
MoveFocusedCell, Players, Resources,
|
||||
structures::{BaseBuilding, Structures, Tunnel},
|
||||
units::{MinerUnit, Unit, Units},
|
||||
},
|
||||
},
|
||||
threads::{AudioCmd, TICK_MS},
|
||||
},
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn app() -> App {
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.window_area = Rect::new(0, 0, 160, 50);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
app
|
||||
}
|
||||
|
||||
fn base(game: &SkirmishState, coords: (usize, usize)) -> BaseBuilding {
|
||||
let Structures::Base(base) = game.board.cells[coords.0][coords.1].get_structure() else {
|
||||
panic!("base missing");
|
||||
};
|
||||
base
|
||||
}
|
||||
|
||||
fn press(app: &mut App, code: KeyCode, modifiers: KeyModifiers) -> () {
|
||||
handle_keybindings(app, KeyEvent::new(code, modifiers));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recruitment_is_owned_by_base_and_invalid_requests_do_not_charge() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
let before: Resources = game.player_resources;
|
||||
for coords in [(5, 39), (0, 0), (usize::MAX, 2)] {
|
||||
assert!(!game.recruit_miner(coords, Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
}
|
||||
assert!(!game.recruit_miner((5, 2), Players::Unclaimed));
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert_eq!(game.player_resources, Resources::new(40, 10));
|
||||
assert_eq!(game.enemy_resources, Resources::new(50, 25));
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recruitment_requires_both_resources_and_charges_each_queue_entry() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
for resources in [
|
||||
Resources { wood: 9, metal: 15 },
|
||||
Resources {
|
||||
wood: 10,
|
||||
metal: 14,
|
||||
},
|
||||
] {
|
||||
game.player_resources = resources;
|
||||
assert!(!game.can_recruit_miner((5, 2), Players::Player));
|
||||
assert!(!game.recruit_miner((5, 2), Players::Player));
|
||||
assert_eq!(game.player_resources, resources);
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
}
|
||||
game.player_resources = Resources::new(20, 30);
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 2);
|
||||
assert_eq!(game.player_resources, Resources::new(0, 0));
|
||||
assert!(!game.recruit_miner((5, 2), Players::Player));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_recruit_waits_at_full_progress_and_following_recruits_pause_until_departure() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
game.player_resources = Resources::new(100, 100);
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
let original: Option<Units> = game.board.cells[5][2].get_option_unit();
|
||||
for _ in 0..2 * BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 2);
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 100);
|
||||
assert_eq!(game.board.cells[5][2].get_option_unit(), original);
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
game.tick_update();
|
||||
let spawned: Option<Units> = game.board.cells[5][2].get_option_unit();
|
||||
assert_eq!(spawned.get_owner(), Players::Player);
|
||||
assert_eq!(spawned.get_coords(), (5, 2));
|
||||
assert!(spawned.get_ref_tasks().is_empty());
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 1);
|
||||
for _ in 0..2 * BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 1);
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 0);
|
||||
assert_eq!(game.board.cells[5][2].get_option_unit(), spawned);
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
for _ in 1..BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 99);
|
||||
game.tick_update();
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[5][2].get_ref_option_unit().get_owner(),
|
||||
Players::Player
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walking_out_of_base_resumes_recruitment_and_new_miner_can_receive_tasks() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
game.board.cells[5][3].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.toggle_marking();
|
||||
for _ in 0..3000_u16.div_ceil(u16::from(TICK_MS)) {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
for _ in 3000_u16.div_ceil(u16::from(TICK_MS))..BaseBuilding::RECRUITMENT_TICKS - 1 {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 99);
|
||||
game.tick_update();
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[5][2].get_ref_option_unit().get_coords(),
|
||||
(5, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
game.board.cells[5][3].get_ref_option_unit().get_coords(),
|
||||
(5, 3)
|
||||
);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Down);
|
||||
game.board.toggle_marking();
|
||||
assert_eq!(
|
||||
game.board.cells[5][2]
|
||||
.get_ref_option_unit()
|
||||
.get_ref_tasks()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrival_during_training_does_not_overwrite_visitor_and_ready_recruit_spawns_after_departure()
|
||||
-> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
for _ in 0..100 {
|
||||
game.tick_update();
|
||||
}
|
||||
let visitor: Units = Units::Miner(MinerUnit::new(Players::Enemy, (5, 2)));
|
||||
*game.board.cells[5][2].get_mut_option_unit() = Some(visitor.clone());
|
||||
for _ in 0..BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 100);
|
||||
assert_eq!(game.board.cells[5][2].get_option_unit(), Some(visitor));
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
game.tick_update();
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[5][2].get_ref_option_unit().get_owner(),
|
||||
Players::Player
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bases_have_independent_queues_costs_and_spawn_ownership() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
for (coords, owner) in [((5, 2), Players::Player), ((5, 39), Players::Enemy)] {
|
||||
game.board.cells[coords.0][coords.1]
|
||||
.get_mut_option_unit()
|
||||
.take();
|
||||
assert!(game.recruit_miner(coords, owner));
|
||||
}
|
||||
assert_eq!(game.player_resources.metal, 10);
|
||||
assert_eq!(game.enemy_resources.metal, 10);
|
||||
for _ in 0..BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
for (coords, owner) in [((5, 2), Players::Player), ((5, 39), Players::Enemy)] {
|
||||
assert_eq!(base(game, coords).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[coords.0][coords.1]
|
||||
.get_ref_option_unit()
|
||||
.get_owner(),
|
||||
owner
|
||||
);
|
||||
assert_eq!(
|
||||
game.board.cells[coords.0][coords.1]
|
||||
.get_ref_option_unit()
|
||||
.get_coords(),
|
||||
coords
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recruitment_panel_queues_with_enter_shows_ready_state_and_closes_without_losing_queue() -> () {
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
assert!(
|
||||
available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Recruitment)
|
||||
);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
assert_eq!(
|
||||
base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
!available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter)
|
||||
);
|
||||
press(&mut app, KeyCode::Right, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL);
|
||||
assert_eq!(
|
||||
app
|
||||
.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.get_focused_cell()
|
||||
.get_coords(),
|
||||
(5, 2)
|
||||
);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
for _ in 0..BaseBuilding::RECRUITMENT_TICKS {
|
||||
app.states_mut().unwrap().skirmish.tick_update();
|
||||
}
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 50)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Cost: 10 Wood + 15 Metal"));
|
||||
assert!(text.contains("Queued Miners: 1"));
|
||||
assert!(text.contains("Current progress: 100%"));
|
||||
assert!(text.contains("Ready: waiting for free base cell"));
|
||||
assert!(text.contains("Close recruitment"));
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert_eq!(
|
||||
base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recruitment_panel_is_unavailable_off_base_and_during_route_selection() -> () {
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert!(
|
||||
app
|
||||
.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.marked_cells
|
||||
.marking_cells
|
||||
);
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL);
|
||||
assert!(app.exit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recruitment_fallback_opens_queues_and_closes_on_own_base() -> () {
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert_eq!(
|
||||
base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.player_resources,
|
||||
Resources::new(40, 10)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
use clap::Parser;
|
||||
use ratatui::{Terminal, backend::TestBackend, buffer::Cell, layout::Rect};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{
|
||||
App, GameStates, View,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
Players, Resources,
|
||||
resources::{MINE_PRODUCTION_TICKS, WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_TICKS},
|
||||
structures::{Mine, Ore, Stone, Structure, Structures},
|
||||
},
|
||||
},
|
||||
threads::AudioCmd,
|
||||
},
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn game() -> SkirmishState {
|
||||
let args: Cli =
|
||||
Cli::try_parse_from(["game", "--starting-wood", "70", "--starting-metal", "40"]).unwrap();
|
||||
GameStates::new(&args, &Rect::new(0, 0, 160, 40)).skirmish
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resources_start_from_cli_and_legacy_iron_flag_remains_an_alias() -> () {
|
||||
let game: SkirmishState = game();
|
||||
assert_eq!(game.player_resources, Resources::new(70, 40));
|
||||
assert_eq!(game.enemy_resources, Resources::new(70, 40));
|
||||
let args: Cli = Cli::try_parse_from(["game", "--starting-iron", "33"]).unwrap();
|
||||
assert_eq!(args.starting_metal, 33);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passive_wood_arrives_once_per_interval_without_free_metal() -> () {
|
||||
let mut game: SkirmishState = game();
|
||||
for _ in 1..WOOD_DELIVERY_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.wood, 70);
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.wood, 70 + WOOD_DELIVERY_AMOUNT);
|
||||
for _ in 0..WOOD_DELIVERY_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.wood, 70 + 2 * WOOD_DELIVERY_AMOUNT);
|
||||
assert_eq!(game.player_resources.metal, 40);
|
||||
assert_eq!(game.enemy_resources, Resources::new(70, 40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ore_is_neutral_and_no_mines_spawn_on_the_board() -> () {
|
||||
let game: SkirmishState = game();
|
||||
let mut deposits: usize = 0;
|
||||
for row in &game.board.cells {
|
||||
for cell in row {
|
||||
let structure: Structures = cell.get_structure();
|
||||
assert!(!matches!(structure, Structures::Mine(_)));
|
||||
if matches!(structure, Structures::Ore(_)) {
|
||||
deposits += 1;
|
||||
assert_eq!(structure.get_owner(), Players::Unclaimed);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(deposits, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mine_can_only_replace_neutral_ore_and_preserves_its_reserve() -> () {
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
assert_eq!(ore.extract(4999), 4999);
|
||||
let mut structure: Structures = Structures::Ore(ore);
|
||||
assert!(structure.build_mine(Players::Player));
|
||||
let Structures::Mine(mut mine) = structure else {
|
||||
panic!("expected mine");
|
||||
};
|
||||
assert_eq!(mine.get_owner(), Players::Player);
|
||||
assert_eq!(mine.get_ore_amount(), 1);
|
||||
assert_eq!(mine.produce(), 1);
|
||||
assert_eq!(mine.produce(), 0);
|
||||
assert_eq!(mine.get_ore_amount(), 0);
|
||||
assert!(!structure.build_mine(Players::Enemy));
|
||||
let mut stone: Structures = Structures::Stone(Stone::new());
|
||||
assert!(!stone.build_mine(Players::Player));
|
||||
assert_eq!(stone, Structures::Stone(Stone::new()));
|
||||
assert!(Mine::new(Players::Unclaimed, ore).is_none());
|
||||
assert!(Mine::new(Players::Player, Ore::new(Players::Enemy, 1)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mining_credits_each_owner_and_stops_when_deposit_is_empty() -> () {
|
||||
let mut game: SkirmishState = game();
|
||||
for (col, owner) in [(1, Players::Player), (40, Players::Enemy)] {
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(4999);
|
||||
let mut structure: Structures = Structures::Ore(ore);
|
||||
assert!(structure.build_mine(owner));
|
||||
game.board.cells[5][col].set_structure(structure);
|
||||
}
|
||||
for _ in 1..MINE_PRODUCTION_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.metal, 40);
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.metal, 41);
|
||||
assert_eq!(game.enemy_resources.metal, 41);
|
||||
for _ in 0..MINE_PRODUCTION_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.metal, 41);
|
||||
assert_eq!(game.enemy_resources.metal, 41);
|
||||
let structure: Structures = game.board.cells[5][1].get_structure();
|
||||
assert!(
|
||||
structure
|
||||
.text()
|
||||
.iter()
|
||||
.any(|line: &ratatui::text::Line<'_>| -> bool { line.to_string().contains("Ore: 0/5000") })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_additions_do_not_overflow() -> () {
|
||||
let mut resources: Resources = Resources {
|
||||
wood: u32::MAX - 1,
|
||||
metal: u32::MAX - 1,
|
||||
};
|
||||
resources.add_wood(5);
|
||||
resources.add_metal(5);
|
||||
assert_eq!(resources.wood, u32::MAX);
|
||||
assert_eq!(resources.metal, u32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_reads_current_resources_instead_of_starting_settings() -> () {
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.states = Some(GameStates::new(&app.args, &Rect::new(0, 0, 200, 40)));
|
||||
app.states_mut().unwrap().skirmish.player_resources = Resources {
|
||||
wood: 777,
|
||||
metal: 888,
|
||||
};
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(200, 40)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Wood: 777 (+5/5s)"));
|
||||
assert!(text.contains("Metal: 888"));
|
||||
assert!(!text.contains("Iron:"));
|
||||
}
|
||||
Reference in new issue
Block a user