2026-07-25 12:48:11 -05:00
|
|
|
|
//! 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.
|
|
|
|
|
|
|
2026-06-13 16:24:29 -05:00
|
|
|
|
use crate::board::Board;
|
2026-07-25 12:48:11 -05:00
|
|
|
|
use crate::board::tests::open_board;
|
2026-06-13 16:24:29 -05:00
|
|
|
|
use crate::game::GameState;
|
2026-07-25 12:48:11 -05:00
|
|
|
|
use crate::portal::Portal;
|
2026-08-11 01:02:38 -05:00
|
|
|
|
use crate::utils::{Direction, Point};
|
2026-06-13 16:24:29 -05:00
|
|
|
|
use crate::world::World;
|
2026-06-15 23:35:18 -05:00
|
|
|
|
use std::cell::RefCell;
|
2026-07-25 12:48:11 -05:00
|
|
|
|
use std::collections::HashMap;
|
2026-06-15 23:35:18 -05:00
|
|
|
|
use std::rc::Rc;
|
2026-06-13 16:24:29 -05:00
|
|
|
|
|
2026-07-25 12:48:11 -05:00
|
|
|
|
/// Builds a 3×3 board with the player at `player` and the given portals.
|
|
|
|
|
|
fn make_board(player: (usize, usize), portals: Vec<Portal>) -> 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 {
|
|
|
|
|
|
x,
|
|
|
|
|
|
y,
|
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
|
target_board: target_board.to_string(),
|
|
|
|
|
|
target_name: target_name.to_string(),
|
|
|
|
|
|
glyph: None,
|
2026-06-13 16:24:29 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 12:48:11 -05:00
|
|
|
|
/// Two-board world used by all tests in this module, with b1's player at `b1_player`.
|
2026-06-13 16:24:29 -05:00
|
|
|
|
///
|
2026-07-25 12:48:11 -05:00
|
|
|
|
/// - `"b1"`: portal `"to_b2"` at `(2, 0)` → `"b2"` / `"from_b1"`
|
2026-06-13 16:24:29 -05:00
|
|
|
|
/// - `"b2"`: player at `(0, 0)`, portal `"from_b1"` at `(1, 1)` → `"b1"` / `"to_b2"`
|
2026-07-25 12:48:11 -05:00
|
|
|
|
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")]);
|
2026-06-13 16:24:29 -05:00
|
|
|
|
World {
|
|
|
|
|
|
name: "test".into(),
|
|
|
|
|
|
start: "b1".into(),
|
2026-07-11 14:47:08 -05:00
|
|
|
|
torch: 10,
|
2026-06-13 16:24:29 -05:00
|
|
|
|
scripts: HashMap::new(),
|
|
|
|
|
|
boards: HashMap::from([
|
|
|
|
|
|
("b1".to_string(), Rc::new(RefCell::new(b1))),
|
|
|
|
|
|
("b2".to_string(), Rc::new(RefCell::new(b2))),
|
|
|
|
|
|
]),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 12:48:11 -05:00
|
|
|
|
/// The two-board world with b1's player at the origin.
|
|
|
|
|
|
fn two_board_world() -> World {
|
|
|
|
|
|
two_board_world_with_player((0, 0))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 16:24:29 -05:00
|
|
|
|
#[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.
|
2026-08-11 01:02:38 -05:00
|
|
|
|
assert_eq!(game.board().player_pos(), Point { x: 1, y: 1 });
|
2026-07-25 12:48:11 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[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)))
|
2026-08-11 01:02:38 -05:00
|
|
|
|
.filter(|&(x, y)| b.get((x, y)).as_ref().is_some_and(|t| t.player()))
|
2026-07-25 12:48:11 -05:00
|
|
|
|
.count();
|
|
|
|
|
|
assert_eq!(players, 1, "arriving on a board must not duplicate the player");
|
2026-06-13 16:24:29 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[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() {
|
2026-07-25 12:48:11 -05:00
|
|
|
|
// 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)));
|
2026-06-13 16:24:29 -05:00
|
|
|
|
game.try_move(Direction::East);
|
|
|
|
|
|
assert_eq!(game.current_board_name(), "b2");
|
|
|
|
|
|
}
|