//! Board-to-board transitions via [`GameState::enter_board`] and portal stepping. //! //! Portals live in `Board::portals`, parallel to the grid rather than in it — the //! player shares a cell with one — so `try_move` checks `portal_at` only after the //! player has actually relocated. use crate::board::Board; use crate::board::tests::open_board; use crate::game::GameState; use crate::portal::Portal; use crate::utils::{Direction, Point}; use crate::world::World; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; /// Builds a 3×3 board with the player at `player` and the given portals. fn make_board(player: (usize, usize), portals: Vec) -> Board { let mut board = open_board(3, 3, player); board.portals = portals; board } /// A glyphless portal at `(x, y)` pointing at `target_name` on `target_board`. fn portal( x: usize, y: usize, name: &str, target_board: &str, target_name: &str, ) -> Portal { Portal { location: (x, y).into(), name: name.to_string(), target_board: target_board.to_string(), target_name: target_name.to_string(), glyph: None, } } /// Two-board world used by all tests in this module, with b1's player at `b1_player`. /// /// - `"b1"`: portal `"to_b2"` at `(2, 0)` → `"b2"` / `"from_b1"` /// - `"b2"`: player at `(0, 0)`, portal `"from_b1"` at `(1, 1)` → `"b1"` / `"to_b2"` fn two_board_world_with_player(b1_player: (usize, usize)) -> World { let b1 = make_board(b1_player, vec![portal(2, 0, "to_b2", "b2", "from_b1")]); let b2 = make_board((0, 0), vec![portal(1, 1, "from_b1", "b1", "to_b2")]); World { name: "test".into(), start: "b1".into(), torch: 10, scripts: HashMap::new(), boards: HashMap::from([ ("b1".to_string(), Rc::new(RefCell::new(b1))), ("b2".to_string(), Rc::new(RefCell::new(b2))), ]), } } /// The two-board world with b1's player at the origin. fn two_board_world() -> World { two_board_world_with_player((0, 0)) } #[test] fn enter_board_switches_board() { let mut game = GameState::from_world(two_board_world()); game.enter_board("b2", "from_b1"); assert_eq!(game.current_board_name(), "b2"); } #[test] fn enter_board_places_player_at_arrival_portal() { let mut game = GameState::from_world(two_board_world()); game.enter_board("b2", "from_b1"); // Arrival portal "from_b1" is at (1, 1) on b2. assert_eq!(game.board().player_pos(), Point { x: 1, y: 1 }); } #[test] fn entering_a_board_leaves_exactly_one_player_on_it() { // b2 is authored with a player at (0,0); arriving through the portal must // *relocate* that player rather than stamping a second one, or the board ends // up with two Tile::Player cells and player_pos() reports whichever comes // first in row-major order. let mut game = GameState::from_world(two_board_world()); game.enter_board("b2", "from_b1"); let b = game.board(); let players = (0..b.height) .flat_map(|y| (0..b.width).map(move |x| (x, y))) .filter(|&(x, y)| b.get((x, y)).as_ref().is_some_and(|t| t.player())) .count(); assert_eq!(players, 1, "arriving on a board must not duplicate the player"); } #[test] fn enter_board_sets_board_transition() { let mut game = GameState::from_world(two_board_world()); game.enter_board("b2", "from_b1"); assert_eq!(game.board_transition, Some(1.0)); } #[test] fn enter_board_unknown_board_logs_error() { let mut game = GameState::from_world(two_board_world()); game.enter_board("nope", "entry"); assert_eq!(game.current_board_name(), "b1", "board should not change"); assert!(!game.log.is_empty(), "an error should be logged"); } #[test] fn enter_board_unknown_entry_logs_error() { let mut game = GameState::from_world(two_board_world()); game.enter_board("b2", "nope"); assert_eq!(game.current_board_name(), "b1", "board should not change"); assert!(!game.log.is_empty(), "an error should be logged"); } #[test] fn try_move_onto_portal_switches_board() { // Start the player one cell west of b1's portal at (2, 0). let mut game = GameState::from_world(two_board_world_with_player((1, 0))); game.try_move(Direction::East); assert_eq!(game.current_board_name(), "b2"); }