use super::ui::{
ASIDE_FONT, ASIDE_GAP, BADGE_FONT, BY_W, CARD_NAME_FONT, CARD_SUB_FONT, LABEL_GAP, LABEL_W,
MAP_CARD_W, NAME_INSET, NAME_W, NOTE_FONT, OPEN_FONT, PILL_FONT, ROW_FONT, SEAT_HINT_FONT,
SEATS_CARD_W, STATUS_FONT, STATUS_W, THUMB_W, VALUE_W, card_name, card_note, moved_note,
tile_words,
};
use super::ui::{HEADING_FONT, TILE_W, slot_note, slot_words};
use super::*;
use crate::app::gamepad::LastDevice;
use crate::app::i18n::metrics::{display_px, text_px};
use crate::app::settings::GameSettings;
fn beach(name: &str, castles: &str) -> Beach {
let text = format!(
"name: {name}\nposts: 1\nkind: arena\ncrab: 0,1 R R common\nmap:\n\
+-+-+-+\n|{castles}|\n+ + + +\n|. . .|\n+ + + +\n|. . .|\n+-+-+-+\n"
);
Beach::new(crate::sim::Level::parse(&text).expect("a level"))
}
fn setup(config: MatchConfig, shelf: CustomBeaches, step: Step) -> App {
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.insert_resource(State::new(Screen::MatchSetup));
app.add_message::<bevy::input::keyboard::KeyboardInput>();
app.init_resource::<ButtonInput<KeyCode>>();
app.insert_resource(MatchMenu {
step,
seed: 0xC0FFEE,
..MatchMenu::default()
});
app.insert_resource(config);
app.insert_resource(shelf);
app.init_resource::<crate::app::bot_seats::BotSeats>();
app.init_resource::<crate::app::tournament::Tournament>();
app.init_resource::<crate::app::gamepad::PadSeats>();
app.init_resource::<LastDevice>();
app.init_resource::<crate::app::gamepad::LastPad>();
app.init_resource::<crate::app::keycaps::KeyCaps>();
app.insert_resource(GameSettings::default());
app.add_systems(Update, match_setup_input);
app.update();
app
}
fn tap(app: &mut App, key: KeyCode) {
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.press(key);
app.update();
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
keys.release(key);
keys.clear();
}
fn menu(app: &App) -> &MatchMenu {
app.world().resource::<MatchMenu>()
}
fn config(app: &App) -> &MatchConfig {
app.world().resource::<MatchConfig>()
}
#[test]
fn going_back_and_forth_keeps_every_choice() {
let mut app = setup(MatchConfig::default(), CustomBeaches::default(), Step::Game);
tap(&mut app, KeyCode::KeyD);
tap(&mut app, KeyCode::KeyD);
tap(&mut app, KeyCode::KeyS);
tap(&mut app, KeyCode::KeyS);
tap(&mut app, KeyCode::KeyD);
assert_eq!(config(&app).seats, 4);
assert_eq!(config(&app).round, RoundLength::Long);
tap(&mut app, KeyCode::Enter);
assert_eq!(menu(&app).step, Step::Map);
let picked = menu(&app).slot;
tap(&mut app, KeyCode::KeyD);
tap(&mut app, KeyCode::KeyS);
let card = menu(&app).slot;
assert_eq!(card, picked + 1 + CARD_COLUMNS);
let map = config(&app).map;
assert_eq!(
Slot::Card(Card::Map(map)),
map_slots(&CustomBeaches::default())[card].1
);
tap(&mut app, KeyCode::Escape);
assert_eq!(menu(&app).step, Step::Game);
assert_eq!(menu(&app).game_row, 2, "step 1's cursor where it was left");
assert_eq!((config(&app).seats, config(&app).map), (4, map));
tap(&mut app, KeyCode::Enter);
assert_eq!(menu(&app).slot, card, "step 2 opens on the pick");
tap(&mut app, KeyCode::Enter);
assert_eq!(menu(&app).step, Step::Players);
tap(&mut app, KeyCode::KeyS);
tap(&mut app, KeyCode::KeyS);
tap(&mut app, KeyCode::KeyD);
assert_eq!(
config(&app).controller(2),
SeatController::Ai(BotLevel::Easy)
);
tap(&mut app, KeyCode::Escape);
assert_eq!(menu(&app).step, Step::Map);
tap(&mut app, KeyCode::Enter);
assert_eq!(menu(&app).seat, 2, "step 3's cursor where it was left");
assert_eq!(
config(&app).controller(2),
SeatController::Ai(BotLevel::Easy)
);
assert_eq!(config(&app).round, RoundLength::Long);
assert!(!config(&app).armed, "nothing started on the way");
tap(&mut app, KeyCode::Escape);
tap(&mut app, KeyCode::Escape);
tap(&mut app, KeyCode::Escape);
assert!(matches!(
app.world().resource::<NextState<Screen>>(),
NextState::Pending(Screen::Menu)
));
}
#[test]
fn step_one_turns_its_dials() {
let shelf = CustomBeaches::default();
let mut settings = GameSettings::default();
let mut config = MatchConfig::with_ai(3, 2);
config.controllers[2] = SeatController::Bot;
for _ in 0..10 {
turn_game_row(
GameRow::Players,
Turn::Right,
&mut config,
&mut settings,
&shelf,
);
}
assert_eq!(config.seats, *SEATS_RANGE.end());
assert!(
holds(config.map, config.seats),
"reseated onto a wide beach"
);
for _ in 0..10 {
turn_game_row(
GameRow::Players,
Turn::Left,
&mut config,
&mut settings,
&shelf,
);
}
assert_eq!(config.seats, *SEATS_RANGE.start());
turn_game_row(
GameRow::AiLevel,
Turn::Right,
&mut config,
&mut settings,
&shelf,
);
assert_eq!(config.ai_level, BotLevel::Hard);
assert_eq!(config.controller(1), SeatController::Ai(BotLevel::Hard));
turn_game_row(
GameRow::Posts,
Turn::Right,
&mut config,
&mut settings,
&shelf,
);
assert_eq!(config.posts, MAX_SIGNPOSTS_PER_PLAYER as u8 + 1);
turn_game_row(
GameRow::Series,
Turn::Right,
&mut config,
&mut settings,
&shelf,
);
assert!(config.series.is_series());
turn_game_row(
GameRow::Scoring,
Turn::Right,
&mut config,
&mut settings,
&shelf,
);
assert_eq!(settings.team_mode, crate::app::teams::TeamMode::Pairs);
turn_game_row(
GameRow::Gulls,
Turn::Left,
&mut config,
&mut settings,
&shelf,
);
assert_eq!(config.gulls, GullPressure::Calm);
}
#[test]
fn the_scoring_row_plays_teams_only_where_they_fit() {
use crate::app::i18n::EN;
use crate::app::teams::TeamMode;
let settings = GameSettings {
team_mode: TeamMode::Pairs,
..GameSettings::default()
};
let row = |seats| {
let config = MatchConfig {
seats,
..MatchConfig::default()
};
game_row_text(&EN, GameRow::Scoring, &config, &settings).1
};
assert!(row(4).contains(EN.team_modes[TeamMode::Pairs.index()]));
let odd = row(5);
assert!(
odd.contains(EN.team_mode_names[TeamMode::Pairs.index()]),
"{odd}"
);
assert!(odd.contains('6'), "free-for-all until six: {odd}");
assert_eq!(TeamMode::Pairs.played_by(5), TeamMode::Solo);
}
#[test]
fn step_one_rows_fit_their_cells_in_every_language() {
use crate::app::teams::TeamMode;
for lang in crate::app::i18n::ALL_LANGS {
let tr = lang.tr();
for seats in SEATS_RANGE {
for (mode, series) in TeamMode::ALL.into_iter().flat_map(|mode| {
crate::app::tournament::SeriesLength::ALL.map(move |series| (mode, series))
}) {
for (level, (gulls, round)) in
BOT_LEVELS
.into_iter()
.zip(GullPressure::ALL.into_iter().zip([
RoundLength::Short,
RoundLength::Long,
RoundLength::Quick,
]))
{
let settings = GameSettings {
team_mode: mode,
..GameSettings::default()
};
let config = MatchConfig {
seats,
ai_level: level,
gulls,
round,
series,
..MatchConfig::default()
};
for row in GameRow::ALL {
let (label, value, aside) = game_row_text(tr, row, &config, &settings);
let label_w = text_px(label, ROW_FONT) + LABEL_GAP;
assert!(label_w <= LABEL_W, "{lang:?} {row:?}: {label} {label_w}");
let wide = text_px(&value, ROW_FONT)
+ match aside.is_empty() {
true => 0.0,
false => ASIDE_GAP + text_px(aside, ASIDE_FONT),
};
assert!(
wide <= VALUE_W,
"{lang:?} {row:?}: {value} {aside} is {wide}px of {VALUE_W}"
);
}
}
}
}
}
}
#[test]
fn every_line_fits_its_place_in_every_language() {
let shelf = CustomBeaches(vec![
beach("Driftwood Arena", "0 . 1"),
beach("Gap", "0 . 2"),
beach("Driftwood Cove Number 00 Long Name", "0 . 1"),
beach("一人あたりの矢印一人あたりの矢印一人あたりの矢印", "0 1 2"),
beach(&"W".repeat(60), "0 . 1"),
]);
let note_room = MAP_CARD_W - 2.0 * crate::app::menu_ui::CARD_PAD - 20.0;
let seats_room = SEATS_CARD_W - 2.0 * crate::app::menu_ui::CARD_PAD - 20.0;
let over = std::cell::RefCell::new(Vec::new());
for lang in crate::app::i18n::ALL_LANGS {
let tr = lang.tr();
let long = |line: &str, px: f32, room: f32| {
let wide = text_px(line, px);
if wide > room {
over.borrow_mut()
.push(format!("{lang:?}: {line:?} is {wide:.0}px of {room}"));
}
};
for row in GameRow::ALL {
long(
&game_row_note(tr, row),
NOTE_FONT,
1.8 * (LABEL_W + VALUE_W),
);
}
let names: f32 = tr
.setup_steps
.iter()
.map(|name| 22.0 + 8.0 + display_px(name, 17.0))
.sum();
assert!(names + 2.0 * 40.0 <= LABEL_W + VALUE_W, "{lang:?}: {names}");
let heading = super::ui::host_map_title(tr, 6);
let wide = display_px(&heading, super::ui::HOST_MAP_TITLE_FONT);
assert!(wide <= MAP_CARD_W / 2.0, "{lang:?}: {heading}");
let far = fill(tr.host_map_too_big, &[("m", &card_name(&"W".repeat(60)))]);
long(&far, NOTE_FONT, note_room);
for heading in tr.setup_sections {
let wide = display_px(heading, HEADING_FONT);
assert!(wide <= 2.0 * TILE_W, "{lang:?}: {heading} is {wide:.0}px");
}
for seats in SEATS_RANGE {
let config = MatchConfig {
seats,
..MatchConfig::default()
};
for (_, slot) in map_slots(&shelf)
.into_iter()
.chain([(MapSection::Yours, Slot::MakeOne)])
{
for online in [false, true] {
let words = slot_words(tr, slot, seats, &shelf, online);
long(&words.sub, CARD_SUB_FONT, THUMB_W);
long(words.badge, BADGE_FONT, THUMB_W - 40.0);
long(&words.name, CARD_NAME_FONT, THUMB_W - NAME_INSET);
if let Some(pill) = &words.pill {
let wide = display_px(pill, PILL_FONT) + 24.0;
assert!(wide <= THUMB_W, "{lang:?}: {pill}");
}
long(
&slot_note(tr, slot, &config, &shelf, online),
NOTE_FONT,
note_room,
);
}
let Slot::Card(card) = slot else {
continue;
};
let mut moved = MatchConfig {
seats: 2,
..MatchConfig::default()
};
pick_card(&mut moved, card, &shelf);
reseat(&mut moved, seats, &shelf);
if let Some(note) = moved_note(tr, &moved, &shelf) {
long(¬e, NOTE_FONT, note_room);
long(¬e, SEAT_HINT_FONT, seats_room);
}
}
}
let by_room = BY_W - 40.0;
for by in [
PlayedBy::Keys(0),
PlayedBy::Keys(1),
PlayedBy::Pad {
number: 6,
joined: false,
},
PlayedBy::Pad {
number: 6,
joined: true,
},
PlayedBy::Ai(BotLevel::Normal),
PlayedBy::Ai(BotLevel::Hard),
PlayedBy::Ai(BotLevel::Easy),
PlayedBy::Bot,
] {
long(
&format!("< {} >", played_by_text(tr, by)),
ROW_FONT,
by_room,
);
long(status_text(tr, by), STATUS_FONT, STATUS_W);
}
for keys in [false, true] {
long(
&played_by_text(tr, PlayedBy::Open { keys }),
OPEN_FONT,
by_room,
);
long(
status_text(tr, PlayedBy::Open { keys }),
STATUS_FONT,
STATUS_W,
);
}
for device in [LastDevice::Keyboard, LastDevice::Pad] {
long(&unnamed(tr, device), STATUS_FONT, NAME_W);
long(&seat_hint(tr, device, 1), SEAT_HINT_FONT, seats_room);
}
for name in ["W".repeat(12), "あいうえおかきくけこさし".to_string()] {
for mode in crate::app::teams::TeamMode::ALL {
for (seats, ai) in [(4, 0), (5, 0), (6, 0)] {
let config = MatchConfig::with_ai(seats, ai);
let settings = GameSettings {
team_mode: mode,
names: std::array::from_fn(|_| name.clone()),
..GameSettings::default()
};
for seat in 0..seats {
for device in [LastDevice::Keyboard, LastDevice::Pad] {
for pin in [
None,
Some(P2Pin::Keys),
Some(P2Pin::Pad(3)),
Some(P2Pin::KeptForKeys),
Some(P2Pin::KeptForPad(3)),
] {
let said =
partner_line(tr, device, (&config, &settings), seat, pin);
long(said.as_deref().unwrap_or(""), SEAT_HINT_FONT, seats_room);
}
}
}
}
}
}
let all_open: Vec<u8> = (1..MAX_PLAYERS as u8).collect();
let (line, warns) = seats_line(tr, &all_open, BotLevel::Normal, true);
assert!(warns);
long(&line, ROW_FONT - 2.0, seats_room);
long(
&seats_line(tr, &[], BotLevel::Normal, true).0,
ROW_FONT - 2.0,
seats_room,
);
}
let over = over.into_inner();
assert!(over.is_empty(), "too long:\n{}", over.join("\n"));
}
#[test]
fn a_long_beach_name_is_cut_to_its_card() {
let room = THUMB_W - NAME_INSET;
for name in [
"Driftwood Cove Number 00 Long Name",
"一人あたりの矢印一人あたりの矢印一人",
"Plage du Phare à l'Écume Très Longue",
"Пляж с очень длинным названием тут",
&"M".repeat(80),
"ABC 一人あたり DEF 矢印 GHIJKLMNOPQRS",
] {
let shown = card_name(name);
let wide = text_px(&shown, CARD_NAME_FONT);
assert!(wide <= room, "{shown:?} is {wide:.0}px of {room}");
assert!(shown.ends_with('…'), "{shown:?}");
let chars: Vec<char> = name.chars().collect();
let fits = (0..=chars.len())
.rev()
.map(|n| chars[..n].iter().collect::<String>())
.find(|start| text_px(&format!("{start}…"), CARD_NAME_FONT) <= room)
.expect("an empty start fits");
assert_eq!(shown, format!("{}…", fits.trim_end()), "{name}");
}
for name in ["Driftwood Arena", "一人あたりの矢印", "Gap"] {
assert_eq!(card_name(name), name, "fits, whole");
}
}
#[test]
fn map_cards_grey_what_cannot_seat_the_table() {
use crate::app::i18n::EN;
let shelf = CustomBeaches(vec![beach("Pair", "0 . 1"), beach("Gap", "0 . 2")]);
let all = cards(&shelf);
assert_eq!(
all.len(),
MapChoice::ALL.len() - 1 + 2 + 1,
"every built-in map, the shelf, Random"
);
assert_eq!(all.last(), Some(&Card::Random));
for seats in SEATS_RANGE {
for card in &all {
let fits = card_fits(*card, seats, &shelf);
let want = match card {
Card::Map(map) => holds(*map, seats),
Card::Shelf(0) => seats <= 2,
Card::Shelf(_) => false,
Card::Random => true,
};
assert_eq!(fits, want, "{card:?} at {seats}");
assert_eq!(
tile_words(&EN, *card, seats, &shelf, false).pill.is_some(),
!fits,
"{card:?} at {seats}: greyed, and says so"
);
}
}
let words = tile_words(&EN, Card::Map(MapChoice::Classic), 5, &shelf, false);
assert_eq!(words.pill.as_deref(), Some("seats 4, not 5"));
let words = tile_words(&EN, Card::Map(MapChoice::RoundTable), 3, &shelf, false);
assert_eq!(words.pill.as_deref(), Some("seats 2/4/6, not 3"));
assert!(words.sub.contains("2/4/6"), "{}", words.sub);
let three = MatchConfig {
seats: 3,
..MatchConfig::default()
};
let note = card_note(&EN, Card::Map(MapChoice::Duel), &three, &shelf, false);
assert!(
note.contains("Duel") && note.contains("for 2 players"),
"{note}"
);
assert!(!card_fits(Card::Map(MapChoice::Duel), 4, &shelf));
assert!(card_fits(Card::Map(MapChoice::Surprise), 5, &shelf));
let mut config = MatchConfig {
seats: 5,
map: MapChoice::GenLarge,
..MatchConfig::default()
};
pick_card(&mut config, Card::Map(MapChoice::Classic), &shelf);
assert_eq!(
config.map,
MapChoice::GenLarge,
"a greyed card is not picked"
);
config.seats = 2;
pick_card(&mut config, Card::Shelf(0), &shelf);
assert_eq!((config.map, config.custom), (MapChoice::Custom, 0));
assert_eq!(picked_card(&config, &shelf), Card::Shelf(0));
pick_card(&mut config, Card::Random, &shelf);
assert_eq!(picked_card(&config, &shelf), Card::Random);
}
#[test]
fn a_shelf_beachs_seats_line_labels_and_draws_its_card() {
use crate::app::i18n::EN;
let listed = |name: &str, seats: &str| {
let text = format!(
"name: {name}\nposts: 1\nkind: arena\nseats: {seats}\ncrab: 0,1 R R common\nmap:\n\
+-+-+-+-+\n|0 . . 1|\n+ + + + +\n|. . . .|\n+ + + + +\n|2 . . 3|\n+-+-+-+-+\n"
);
Beach::new(crate::sim::Level::parse(&text).expect("a level"))
};
let shelf = CustomBeaches(vec![listed("Four", "4"), listed("Even", "2 4")]);
let four = Card::Shelf(0);
let even = Card::Shelf(1);
assert_eq!(card_seats(four, &shelf), Some((4, 4)));
assert_eq!(card_tables(even, &shelf), vec![2, 4]);
for seats in SEATS_RANGE {
assert_eq!(card_fits(four, seats, &shelf), seats == 4, "{seats}");
assert_eq!(card_fits(even, seats, &shelf), seats == 2 || seats == 4);
}
let words = tile_words(&EN, four, 2, &shelf, false);
assert_eq!(words.pill.as_deref(), Some("seats 4, not 2"));
assert!(words.sub.ends_with("· 4 players"), "{}", words.sub);
let words = tile_words(&EN, even, 3, &shelf, false);
assert_eq!(words.pill.as_deref(), Some("seats 2/4, not 3"));
assert!(words.sub.ends_with("· 2/4 players"), "{}", words.sub);
let two = MatchConfig::default();
let note = card_note(&EN, four, &two, &shelf, false);
assert!(note.contains("built for 4 players"), "{note}");
let shown = card_config(&two, four, &shelf).expect("a picture");
assert_eq!((shown.seats, shown.map), (4, MapChoice::Custom));
assert_eq!(
shelf
.fitting(4)
.into_iter()
.nth(shown.custom)
.map(|b| &b.level.name),
Some(&"Four".to_string())
);
}
#[test]
fn every_map_says_what_it_is() {
for lang in crate::app::i18n::ALL_LANGS {
let tr = lang.tr();
for map in MapChoice::ALL {
assert!(!map_about(tr, map).is_empty(), "{lang:?}: {map:?}");
}
assert_eq!(map_about(tr, MapChoice::Custom), tr.setup_custom_about);
}
}
#[test]
fn step_three_says_the_map_moved_and_what_a_d_does() {
use crate::app::i18n::EN;
let shelf = CustomBeaches::default();
let mut moved = MatchConfig {
seats: 4,
..MatchConfig::default()
};
pick_card(&mut moved, Card::Map(MapChoice::GenSmall), &shelf);
reseat(&mut moved, 5, &shelf);
let note = moved_note(&EN, &moved, &shelf).expect("the map moved");
let mut app = App::new();
app.insert_resource(MatchMenu {
step: Step::Players,
seat: 1,
..MatchMenu::default()
});
app.insert_resource(moved);
app.insert_resource(shelf);
app.insert_resource(GameSettings::default());
app.init_resource::<LastDevice>();
app.insert_resource(crate::app::art::Art::blank());
app.init_resource::<crate::app::gamepad::PadSeats>();
app.add_systems(Update, (update_match_ui, paint_players_step).chain());
app.update();
app.update();
let said = |app: &mut App| -> Vec<String> {
let world = app.world_mut();
let mut lines: Vec<String> = world
.query_filtered::<(&Text, &Node), With<ui::SeatMoved>>()
.iter(world)
.filter(|(_, node)| node.display != Display::None)
.map(|(text, _)| text.0.clone())
.collect();
lines.extend(
world
.query_filtered::<&Text, With<ui::SeatHint>>()
.iter(world)
.map(|text| text.0.clone()),
);
lines
};
assert_eq!(
said(&mut app),
vec![note, seat_hint(&EN, LastDevice::Keyboard, 1)]
);
reseat(
&mut app.world_mut().resource_mut::<MatchConfig>(),
4,
&CustomBeaches::default(),
);
app.update();
assert_eq!(
said(&mut app),
vec![seat_hint(&EN, LastDevice::Keyboard, 1)]
);
}
#[test]
fn enter_does_not_go_on_from_a_greyed_card() {
let mut app = setup(
MatchConfig {
seats: 5,
map: MapChoice::GullRoad,
..MatchConfig::default()
},
CustomBeaches::default(),
Step::Map,
);
let gull_road = map_slots(&CustomBeaches::default())
.iter()
.position(|(_, slot)| *slot == Slot::Card(Card::Map(MapChoice::GullRoad)))
.expect("Gull Road's card");
app.world_mut().resource_mut::<MatchMenu>().slot = gull_road;
for _ in 0..gull_road {
tap(&mut app, KeyCode::KeyA);
}
assert_eq!(menu(&app).slot, 0, "resting on the classic arena");
assert_eq!(config(&app).map, MapChoice::GullRoad, "still the pick");
tap(&mut app, KeyCode::Enter);
assert_eq!(menu(&app).step, Step::Map, "Enter did not go on");
let note = card_note(
&crate::app::i18n::EN,
Card::Map(MapChoice::Classic),
config(&app),
&CustomBeaches::default(),
false,
);
assert!(note.contains('4') && note.contains("Game"), "{note}");
for _ in 0..gull_road {
tap(&mut app, KeyCode::KeyD);
}
tap(&mut app, KeyCode::Enter);
assert_eq!(menu(&app).step, Step::Players, "on from Gull Road");
}
#[test]
fn random_draws_every_round_among_the_maps_that_seat_the_table() {
let shelf = CustomBeaches(vec![beach("Pair", "0 . 1"), beach("Trio", "0 1 2")]);
for seats in SEATS_RANGE {
let maps = maps_that_seat(seats, &shelf);
let mut drawn = std::collections::BTreeSet::new();
let mut config = MatchConfig {
seats,
random: true,
..MatchConfig::default()
};
for roll in 0..64u64 {
map_for_next_round(&mut config, &shelf, roll);
assert!(config.random, "still random for the round after");
let played = local_board(&config, &shelf, roll);
match config.map {
MapChoice::Custom => {
let beach = &shelf.fitting(seats)[config.custom];
assert_eq!(played.width(), beach.level.board().width());
}
MapChoice::GenXl => panic!("Random drew the 20x13 at {seats}"),
MapChoice::Surprise => panic!("Random drew Surprise at {seats}"),
map @ (MapChoice::Classic
| MapChoice::GenSmall
| MapChoice::GenClassic
| MapChoice::GenLarge
| MapChoice::GenOcean
| MapChoice::TheGates
| MapChoice::TheMoat
| MapChoice::LogJam
| MapChoice::ThreeLanes
| MapChoice::RoundTable
| MapChoice::RoundTheWorld
| MapChoice::Duel
| MapChoice::TheFountain
| MapChoice::FourDoors
| MapChoice::GullRoad
| MapChoice::BackToBack) => {
assert!(holds(map, seats), "{map:?} at {seats}");
assert_eq!(played.width(), versus_board(map, roll, seats).width());
}
}
drawn.insert(format!("{:?}{}", config.map, config.custom));
}
assert_eq!(drawn.len(), maps.len(), "every map that seats {seats}");
}
assert!(maps_that_seat(2, &shelf).contains(&(MapChoice::Custom, 1)));
assert!(maps_that_seat(3, &shelf).contains(&(MapChoice::Custom, 0)));
assert!(
!maps_that_seat(5, &shelf)
.iter()
.any(|(map, _)| *map == MapChoice::Custom)
);
}
#[test]
fn start_match_draws_random_and_keeps_the_previewed_seed() {
let shelf = CustomBeaches::default();
let mut random = MatchConfig {
seats: 3,
random: true,
map: MapChoice::GenXl,
..MatchConfig::default()
};
seat_the_match(&mut random, &[], 77, &shelf, 1);
assert_eq!(random.seed, None, "a fresh seed for a drawn map");
assert_eq!(
(random.map, random.custom),
maps_that_seat(3, &shelf)[1],
"the draw the roll asked for"
);
let mut picked = MatchConfig {
seats: 3,
map: MapChoice::GenLarge,
..MatchConfig::default()
};
seat_the_match(&mut picked, &[2], 77, &shelf, 1);
assert_eq!(picked.seed, Some(77));
assert_eq!(picked.controller(2), SeatController::Ai(picked.ai_level));
let mut app = setup(
MatchConfig {
map: MapChoice::GenLarge,
..MatchConfig::default()
},
shelf,
Step::Players,
);
tap(&mut app, KeyCode::Enter);
let launched = config(&app);
assert!(launched.armed);
assert_eq!(launched.seed, Some(menu(&app).seed));
}
#[test]
fn an_open_seat_gets_the_ai_for_that_match_only() {
use bevy::ecs::system::RunSystemOnce;
let mut app = setup(
MatchConfig::default(),
CustomBeaches::default(),
Step::Players,
);
tap(&mut app, KeyCode::Enter);
assert!(config(&app).armed);
assert_eq!(
config(&app).controller(1),
SeatController::Ai(BotLevel::Normal)
);
app.world_mut()
.run_system_once(enter_match_setup)
.expect("runs");
assert_eq!(
config(&app).controller(1),
SeatController::Local,
"open again"
);
tap(&mut app, KeyCode::KeyI);
assert!(
app.world()
.resource::<crate::app::gamepad::PadSeats>()
.p2_at_keys()
);
let settings = GameSettings::default();
assert_eq!(
played_by(config(&app), 1, &[], true, &settings),
PlayedBy::Keys(1),
"P2's keys take it"
);
let mut set = MatchConfig::with_ai(3, 1);
seat_the_match(&mut set, &[1], 5, &CustomBeaches::default(), 0);
settle(&mut set, &CustomBeaches::default());
assert_eq!(set.controller(1), SeatController::Local);
assert_eq!(set.controller(2), SeatController::Ai(BotLevel::Normal));
}
#[test]
fn a_doorway_card_backed_out_of_leaves_the_table_as_it_was() {
let mut table = MatchConfig {
seats: 3,
map: MapChoice::Duel,
random: true,
..MatchConfig::default()
};
table.controllers[2] = SeatController::Bot;
let mut app = setup(table, CustomBeaches::default(), Step::Players);
app.init_resource::<Clipboard>();
app.init_resource::<crate::app::keycaps::KeyCaps>();
app.init_resource::<crate::app::cursor::LiveSeatKeys>();
app.add_systems(
Update,
crate::app::bot_seats::door_input.after(match_setup_input),
);
let as_set = |app: &App| {
let config = config(app);
(
config.controllers[..3].to_vec(),
config.filled_at_start,
config.map,
config.seed,
config.armed,
)
};
let before = as_set(&app);
assert!(
!maps_that_seat(3, &CustomBeaches::default())
.iter()
.any(|(map, _)| *map == MapChoice::Duel)
);
tap(&mut app, KeyCode::Enter);
let bots = app.world().resource::<crate::app::bot_seats::BotSeats>();
assert!(bots.door_for.card_up(), "waiting for the bot");
assert_eq!(as_set(&app), before, "nothing set while the card waits");
tap(&mut app, KeyCode::Escape);
let bots = app.world().resource::<crate::app::bot_seats::BotSeats>();
assert!(!bots.door_for.card_up(), "backed out");
assert_eq!(as_set(&app), before, "and the table as step 3 showed it");
}
#[test]
fn the_seats_filled_at_start_open_again_whatever_the_level() {
let shelf = CustomBeaches::default();
let mut set = MatchConfig {
seats: 4,
..MatchConfig::default()
};
seat_the_match(&mut set, &[1, 2, 3], 5, &shelf, 0);
assert_eq!(set.controller(1), SeatController::Ai(BotLevel::Normal));
set.ai_level = BotLevel::Hard;
set.controllers[3] = SeatController::Bot;
settle(&mut set, &shelf);
assert_eq!(set.controller(1), SeatController::Local);
assert_eq!(set.controller(2), SeatController::Local);
assert_eq!(set.controller(3), SeatController::Bot);
}
#[test]
fn the_preview_is_the_board_played() {
let shelf = || CustomBeaches(vec![beach("Pair", "0 . 1")]);
for (index, (_, slot)) in map_slots(&shelf()).into_iter().enumerate() {
let Slot::Card(card) = slot else {
continue;
};
if card == Card::Random || card == Card::Map(MapChoice::Surprise) {
continue;
}
let seats = match card {
Card::Map(map) => map.tables().map_or(2, |tables| tables[0]),
Card::Shelf(_) | Card::Random => 2,
};
let mut app = setup(
MatchConfig {
seats,
..MatchConfig::default()
},
shelf(),
Step::Map,
);
app.world_mut().resource_mut::<MatchMenu>().slot = index;
let shown = card_config(config(&app), card, &shelf()).expect("a beach to show");
let pictured = preview_board(&shown, &shelf(), menu(&app).seed);
tap(&mut app, KeyCode::Enter);
assert_eq!(menu(&app).step, Step::Players, "{card:?}");
tap(&mut app, KeyCode::Enter);
let launched = config(&app);
assert!(launched.armed, "{card:?}");
let seed = launched.seed.expect("the previewed seed");
assert_eq!(seed, menu(&app).seed);
let mut played = local_board(launched, &shelf(), seed);
for _ in 0..PREVIEW_TICKS {
played.tick_idle();
}
assert_eq!(played.state_hash(), pictured.state_hash(), "{card:?}");
}
}
#[test]
fn step_three_turns_a_seat_but_never_player_one() {
let mut config = MatchConfig {
seats: 3,
..MatchConfig::default()
};
let mut seen = Vec::new();
for _ in 0..SEAT_KINDS.len() + 1 {
seen.push(config.controller(2));
turn_seat(&mut config, 2, Turn::Right);
}
assert_eq!(seen[..SEAT_KINDS.len()], SEAT_KINDS);
assert_eq!(
seen[SEAT_KINDS.len()],
SeatController::Bot,
"held at the end"
);
turn_seat(&mut config, 0, Turn::Right);
assert_eq!(config.controller(0), SeatController::Local);
let en = &crate::app::i18n::EN;
assert!(seat_hint(en, LastDevice::Keyboard, 0).is_empty());
assert!(seat_hint(en, LastDevice::Keyboard, 2).contains("A/D"));
assert_eq!(
config.controller(1),
SeatController::Local,
"one seat at a time"
);
assert_eq!(bot_seat_list(&config), vec![2]);
}
#[test]
fn typing_renames_a_seat_without_starting_the_match() {
use bevy::input::ButtonState;
use bevy::input::keyboard::{Key, KeyboardInput};
let mut app = setup(
MatchConfig::with_ai(2, 1),
CustomBeaches::default(),
Step::Players,
);
let type_char = |app: &mut App, ch: &str| {
app.world_mut().write_message(KeyboardInput {
key_code: KeyCode::KeyB,
logical_key: Key::Character(ch.into()),
state: ButtonState::Pressed,
text: Some(ch.into()),
repeat: false,
window: Entity::PLACEHOLDER,
});
app.update();
};
tap(&mut app, KeyCode::KeyS);
tap(&mut app, KeyCode::Tab);
assert_eq!(menu(&app).naming, Some(1), "the seat under the cursor");
type_char(&mut app, "B");
type_char(&mut app, "o");
assert_eq!(app.world().resource::<GameSettings>().names[1], "Bo");
tap(&mut app, KeyCode::Enter);
assert_eq!(menu(&app).naming, None);
assert!(!config(&app).armed, "the Enter that kept the name");
tap(&mut app, KeyCode::Enter);
assert!(config(&app).armed, "and the next one starts the match");
}
#[test]
fn the_map_follows_the_pick_and_says_when_it_cannot() {
use crate::app::i18n::EN;
let shelf = CustomBeaches::default();
let mut app = setup(
MatchConfig {
map: MapChoice::GenSmall,
..MatchConfig::default()
},
shelf,
Step::Game,
);
let note = |app: &App| {
card_note(
&EN,
picked_card(config(app), &CustomBeaches::default()),
config(app),
&CustomBeaches::default(),
false,
)
};
tap(&mut app, KeyCode::KeyD);
assert_eq!(
(config(&app).seats, config(&app).map),
(3, MapChoice::GenSmall)
);
assert!(!note(&app).contains("does not seat"), "{}", note(&app));
for _ in 0..2 {
tap(&mut app, KeyCode::KeyD);
}
assert_eq!(config(&app).seats, 5);
assert_eq!(
config(&app).map,
MapChoice::GenLarge,
"the closest that seats five"
);
assert_eq!(config(&app).pick, Some(Card::Map(MapChoice::GenSmall)));
let said = note(&app);
assert!(
said.contains(EN.setup_map_names[MapChoice::GenSmall.index()])
&& said.contains(EN.setup_map_names[MapChoice::GenLarge.index()])
&& said.contains('5'),
"{said}"
);
tap(&mut app, KeyCode::KeyA);
assert_eq!(config(&app).map, MapChoice::GenSmall, "the pick is back");
assert_eq!(config(&app).pick, None);
assert_eq!(
moved_note(&EN, config(&app), &CustomBeaches::default()),
None
);
let mut large = MatchConfig {
seats: 5,
map: MapChoice::GenLarge,
..MatchConfig::default()
};
reseat(&mut large, 2, &CustomBeaches::default());
assert_eq!((large.map, large.pick), (MapChoice::GenLarge, None));
let pair = CustomBeaches(vec![beach("Pair", "0 . 1")]);
let mut config = MatchConfig::default();
pick_card(&mut config, Card::Shelf(0), &pair);
reseat(&mut config, 3, &pair);
assert_ne!(config.map, MapChoice::Custom);
let said = moved_note(&EN, &config, &pair).expect("moved");
assert!(said.contains("Pair") && said.contains('3'), "{said}");
reseat(&mut config, 2, &pair);
assert_eq!(
(config.map, config.custom, config.pick),
(MapChoice::Custom, 0, None)
);
}
#[test]
fn each_step_has_its_own_keys() {
use crate::app::gamepad::Face;
use crate::app::gamepad::scheme::legend;
use crate::app::i18n::EN;
let [game, map, players] = Step::ALL.map(|step| {
legend(
&EN,
&Face::MatchSetup {
step,
join: true,
partners: false,
},
)
});
assert!(
game.contains("A: next") && game.contains("B: back"),
"{game}"
);
assert!(map.contains("A: next") && !map.contains("adjust"), "{map}");
for part in ["X: rename", "A: start the match", "Start: join", "B: back"] {
assert!(players.contains(part), "{players}");
}
let seated = legend(
&EN,
&Face::MatchSetup {
step: Step::Players,
join: false,
partners: false,
},
);
assert!(
seated.contains("Start/A: start the match") && !seated.contains("join"),
"{seated}"
);
assert_eq!(
start_button_label(&EN, LastDevice::Pad, false),
"Start match (Start)"
);
assert_eq!(
start_button_label(&EN, LastDevice::Pad, true),
"Start match (A)"
);
assert_eq!(
start_button_label(&EN, LastDevice::Keyboard, true),
"Start match (Enter)"
);
assert!(EN.prompt_setup_players.contains("Tab") && EN.prompt_setup_game.contains("A/D"));
}
#[test]
fn the_start_button_names_its_key_inside_it() {
use super::ui::{BUTTON_FONT, START_BUTTON_W};
for lang in crate::app::i18n::ALL_LANGS {
let tr = lang.tr();
for device in [LastDevice::Keyboard, LastDevice::Pad] {
for join in [false, true] {
let label = start_button_label(tr, device, join);
let wide = display_px(&label, BUTTON_FONT);
assert!(
wide + 24.0 <= START_BUTTON_W,
"{lang:?}: {label} is {wide:.0}px"
);
}
}
}
}
fn preview_app(config: MatchConfig, shelf: CustomBeaches) -> App {
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.insert_resource(State::new(Screen::MatchSetup));
app.insert_resource(Assets::<Image>::default());
app.insert_resource(crate::app::art::Art::blank());
app.init_resource::<MapPreviews>();
app.insert_resource(MatchMenu {
step: Step::Map,
seed: 9,
..MatchMenu::default()
});
app.insert_resource(config);
app.insert_resource(shelf);
app.add_systems(Update, (retire_previews, render_previews).chain());
app
}
fn preview_count(app: &mut App) -> (usize, usize, usize) {
use bevy::camera::visibility::RenderLayers;
let world = app.world_mut();
let cameras = world
.query_filtered::<Entity, With<PreviewCamera>>()
.iter(world)
.count();
let parts = world
.query_filtered::<Entity, With<preview::PreviewPart>>()
.iter(world)
.count();
let unlayered = world
.query_filtered::<Entity, (With<preview::PreviewPart>, Without<RenderLayers>)>()
.iter(world)
.count();
(cameras, parts, unlayered)
}
fn settle_previews(app: &mut App, limit: usize) -> usize {
for frame in 0..limit {
app.update();
if app.world().resource::<MapPreviews>().drawing() == 0 {
return frame + 1;
}
}
limit
}
#[test]
fn previews_are_drawn_once_and_released_on_leaving() {
let mut app = preview_app(
MatchConfig::default(),
CustomBeaches(vec![beach("Pair", "0 . 1")]),
);
app.update();
let cards = cards(app.world().resource::<CustomBeaches>()).len();
let (cameras, parts, unlayered) = preview_count(&mut app);
assert_eq!(cameras, preview::PREVIEWS_PER_FRAME, "a few a frame");
assert!(parts > 50, "the boards are laid out: {parts}");
assert_eq!(unlayered, 0, "nothing of a preview on the game's own layer");
settle_previews(&mut app, 400);
let pictures = cards - 2 + preview::SURPRISE_SAMPLES.len();
assert_eq!(app.world().resource::<MapPreviews>().held(), pictures);
assert_eq!(app.world().resource::<MapPreviews>().drawn(), pictures);
assert_eq!(
preview_count(&mut app),
(0, 0, 0),
"drawn, and the drawing put away"
);
let images: Vec<Handle<Image>> = cards_with_pictures(&app);
assert!(
images
.iter()
.all(|image| app.world().resource::<Assets<Image>>().contains(image))
);
app.update();
assert_eq!(preview_count(&mut app).0, 0);
app.world_mut().resource_mut::<MatchConfig>().seats = 3;
app.update();
assert!(preview_count(&mut app).0 > 0);
settle_previews(&mut app, 200);
app.add_systems(Last, release_previews);
app.update();
assert_eq!(app.world().resource::<MapPreviews>().held(), 0);
assert_eq!(preview_count(&mut app), (0, 0, 0));
for image in images {
assert!(!app.world().resource::<Assets<Image>>().contains(&image));
}
}
#[test]
fn a_greyed_card_is_not_redrawn_for_another_count() {
let shelf = CustomBeaches(vec![beach("Pair", "0 . 1")]);
let mut app = preview_app(
MatchConfig {
seats: 5,
map: MapChoice::GenLarge,
..MatchConfig::default()
},
shelf,
);
settle_previews(&mut app, 200);
let shelf = app.world().resource::<CustomBeaches>();
let config = app.world().resource::<MatchConfig>().clone();
let greyed: Vec<Card> = cards(shelf)
.into_iter()
.filter(|card| {
*card != Card::Random && !card_fits(*card, 5, shelf) && !card_fits(*card, 6, shelf)
})
.collect();
assert_eq!(
greyed.len(),
11,
"classic, small, 12x9, the six handmade maps for two or four, Four Doors, and the pair"
);
for card in &greyed {
let own = match card {
Card::Map(map) => map.tables().map_or(2, |tables| tables[0]),
Card::Shelf(_) | Card::Random => 2,
};
assert_eq!(
card_config(&config, *card, shelf).map(|c| c.seats),
Some(own)
);
}
app.world_mut().resource_mut::<MatchConfig>().seats = 6;
let mut redrawn = std::collections::BTreeSet::new();
for _ in 0..200 {
app.update();
let previews = app.world().resource::<MapPreviews>();
for card in cards(app.world().resource::<CustomBeaches>()) {
if previews.is_drawing(card) {
redrawn.insert(format!("{card:?}"));
}
}
if previews.drawing() == 0 {
break;
}
}
for card in greyed {
assert!(!redrawn.contains(&format!("{card:?}")), "{card:?} redrawn");
}
assert!(!redrawn.is_empty(), "the large beaches are drawn for six");
}
#[test]
fn the_cards_on_show_are_drawn_first() {
let shelf = CustomBeaches(
(0..24)
.map(|n| beach(&format!("Beach {n}"), "0 . 1"))
.collect(),
);
let slots = map_slots(&shelf);
let page = MapPage::of(&slots);
let last = slots.len() - 1;
let scroll = page.scrolled(0.0, last);
let mut app = preview_app(MatchConfig::default(), shelf);
{
let mut menu = app.world_mut().resource_mut::<MatchMenu>();
menu.slot = last;
menu.scroll = scroll;
}
let mut first: Vec<Pic> = slots
.iter()
.enumerate()
.filter_map(|(index, (_, slot))| match slot {
Slot::Card(card @ (Card::Map(_) | Card::Shelf(_)))
if page.slot_in_view(scroll, index) && *card != Card::Map(MapChoice::Surprise) =>
{
Some(Pic::Card(*card))
}
Slot::Card(_) | Slot::MakeOne => None,
})
.collect();
assert_eq!(first.len(), CARD_COLUMNS, "the shelf's last row");
first.extend(MOSAIC.map(|map| Pic::Card(Card::Map(map))));
first.extend((0..preview::SURPRISE_SAMPLES.len()).map(Pic::Sample));
let frames = first.len().div_ceil(preview::PREVIEWS_PER_FRAME);
for _ in 0..frames {
app.update();
}
let previews = app.world().resource::<MapPreviews>();
for pic in &first {
assert!(previews.image(*pic).is_some(), "{pic:?} not drawn first");
}
assert_eq!(previews.held(), first.len(), "and nothing else yet");
}
#[test]
fn a_preview_waits_for_the_renderer_and_tries_again() {
let mut app = preview_app(MatchConfig::default(), CustomBeaches::default());
let pipelines = PreviewPipelines::default();
app.insert_resource(pipelines.clone());
app.update();
let drawing = |app: &App| app.world().resource::<MapPreviews>().drawing();
for _ in 0..3 * preview::PREVIEW_FRAMES {
app.update();
}
assert!(drawing(&app) > 0, "still compiling: the cameras stay");
for _ in 0..preview::PREVIEW_FRAMES {
pipelines.count(true);
}
let started = app.world().resource::<MapPreviews>().held();
app.update();
let previews = app.world().resource::<MapPreviews>();
assert_eq!(previews.drawn(), started, "settled: drawn");
let mut app = preview_app(MatchConfig::default(), CustomBeaches::default());
app.insert_resource(PreviewPipelines::default());
app.update();
let card = Card::Map(MapChoice::Classic);
let first = app.world().resource::<MapPreviews>().image(Pic::Card(card));
for _ in 0..preview::PREVIEW_PATIENCE {
app.update();
}
let previews = app.world().resource::<MapPreviews>();
assert_eq!(previews.drawn(), 0, "nothing claimed drawn");
assert!(previews.is_drawing(card), "drawn again");
assert_eq!(
previews.image(Pic::Card(card)),
first,
"into the same picture"
);
}
fn cards_with_pictures(app: &App) -> Vec<Handle<Image>> {
let previews = app.world().resource::<MapPreviews>();
MapChoice::ALL
.into_iter()
.filter(|&map| map != MapChoice::Custom)
.filter_map(|map| previews.image(Pic::Card(Card::Map(map))))
.collect()
}
#[test]
fn the_map_bar_follows_the_window() {
use crate::app::menu_ui::scroll_bar::{self, reading};
use bevy::ecs::system::RunSystemOnce;
let bar_app = |shelf: CustomBeaches| {
let mut app = setup(MatchConfig::default(), shelf, Step::Map);
app.world_mut()
.run_system_once(|mut commands: Commands| {
commands.spawn(Node::default()).with_children(|band| {
scroll_bar::spawn(band, MapBar);
});
})
.expect("the bar is built");
app.add_systems(Update, follow_map_bar.after(match_setup_input));
app.update();
app
};
let bar = |app: &mut App| reading::<MapBar>(app.world_mut());
let shelf = CustomBeaches(
(0..5)
.map(|n| beach(&format!("Beach {n}"), "0 . 1"))
.collect(),
);
let page = MapPage::of(&map_slots(&shelf));
let shown = ui::VIEW_H / page.height;
let travel = 1.0 - shown;
let at = |app: &App| (menu(app).scroll / (page.height - ui::VIEW_H)).min(1.0);
let mut app = bar_app(shelf);
assert_eq!(bar(&mut app), Some([0.0, shown, travel]), "at the top");
for _ in 1..page.rows.len() {
tap(&mut app, KeyCode::KeyS);
let [above, thumb, below] = bar(&mut app).expect("a bar");
assert!((thumb - shown).abs() < 1e-5, "{thumb}");
assert!((above - travel * at(&app)).abs() < 1e-4, "{above}");
assert!((below - travel * (1.0 - at(&app))).abs() < 1e-4, "{below}");
}
assert_eq!(page.row_of(menu(&app).slot), page.rows.len() - 1);
assert_eq!(bar(&mut app), Some([travel, shown, 0.0]), "at the bottom");
for _ in 1..page.rows.len() {
tap(&mut app, KeyCode::KeyW);
}
assert_eq!(bar(&mut app), Some([0.0, shown, travel]), "back at the top");
}
#[test]
fn only_a_start_pressed_says_joined() {
let tr = crate::app::i18n::Lang::En.tr();
let dealt = PlayedBy::Pad {
number: 1,
joined: false,
};
let pressed = PlayedBy::Pad {
number: 1,
joined: true,
};
assert_eq!(status_text(tr, dealt), tr.setup_status[0]);
assert_eq!(status_text(tr, PlayedBy::Keys(0)), tr.setup_status[0]);
assert_eq!(status_text(tr, pressed), tr.setup_status[1]);
}
#[test]
fn the_map_step_has_three_sections_in_the_dials_order() {
let built_in: Vec<Slot> = [
MapChoice::Classic,
MapChoice::TheGates,
MapChoice::TheMoat,
MapChoice::LogJam,
MapChoice::ThreeLanes,
MapChoice::RoundTable,
MapChoice::RoundTheWorld,
MapChoice::Duel,
MapChoice::TheFountain,
MapChoice::FourDoors,
MapChoice::GullRoad,
MapChoice::BackToBack,
MapChoice::GenSmall,
MapChoice::GenClassic,
MapChoice::GenLarge,
MapChoice::GenXl,
MapChoice::GenOcean,
]
.map(|map| Slot::Card(Card::Map(map)))
.to_vec();
let chance = [
Slot::Card(Card::Map(MapChoice::Surprise)),
Slot::Card(Card::Random),
];
let in_section = |shelf: &CustomBeaches, section: MapSection| -> Vec<Slot> {
map_slots(shelf)
.into_iter()
.filter(|(of, _)| *of == section)
.map(|(_, slot)| slot)
.collect()
};
let empty = CustomBeaches::default();
assert_eq!(in_section(&empty, MapSection::BuiltIn), built_in);
assert_eq!(in_section(&empty, MapSection::Yours), vec![Slot::MakeOne]);
assert_eq!(in_section(&empty, MapSection::Chance), chance);
let sections: Vec<MapSection> = map_slots(&empty).into_iter().map(|(s, _)| s).collect();
assert!(sections.is_sorted_by_key(|section| section.index()));
assert!(!cards(&empty).is_empty() && cards(&empty).len() == map_slots(&empty).len() - 1);
let shelf = CustomBeaches(vec![beach("Pair", "0 . 1"), beach("Trio", "0 1 2")]);
assert_eq!(
in_section(&shelf, MapSection::Yours),
vec![Slot::Card(Card::Shelf(0)), Slot::Card(Card::Shelf(1))]
);
assert_eq!(in_section(&shelf, MapSection::BuiltIn), built_in);
assert_eq!(in_section(&shelf, MapSection::Chance), chance);
let dial: Vec<usize> = MapChoice::DIAL
.iter()
.map(|&map| MapSection::of(map).index())
.collect();
assert!(dial.is_sorted(), "{dial:?}");
let mut config = MatchConfig {
map: MapChoice::GenOcean,
..MatchConfig::default()
};
cycle_map(&mut config, Turn::Right, &shelf);
assert_eq!((config.map, config.custom), (MapChoice::Custom, 0));
cycle_map(&mut config, Turn::Right, &shelf);
cycle_map(&mut config, Turn::Right, &shelf);
assert_eq!(config.map, MapChoice::Surprise, "the shelf, then Surprise");
cycle_map(&mut config, Turn::Right, &empty);
assert_eq!(config.map, MapChoice::Classic, "and round to the start");
}
#[test]
fn the_cursor_walks_across_the_sections() {
let mut app = setup(MatchConfig::default(), CustomBeaches::default(), Step::Map);
let slots = map_slots(&CustomBeaches::default());
let at = |app: &App| slots[menu(app).slot].1;
let map = |map| Slot::Card(Card::Map(map));
assert_eq!(at(&app), map(MapChoice::Classic));
tap(&mut app, KeyCode::KeyD);
tap(&mut app, KeyCode::KeyD);
tap(&mut app, KeyCode::KeyD);
assert_eq!(
at(&app),
map(MapChoice::LogJam),
"the first row's last card"
);
let down = [
map(MapChoice::Duel),
map(MapChoice::BackToBack),
map(MapChoice::GenXl),
map(MapChoice::GenOcean),
Slot::MakeOne,
map(MapChoice::Surprise),
map(MapChoice::Surprise),
];
for want in down {
tap(&mut app, KeyCode::KeyS);
assert_eq!(at(&app), want);
}
tap(&mut app, KeyCode::KeyD);
assert_eq!(at(&app), Slot::Card(Card::Random));
tap(&mut app, KeyCode::KeyD);
assert_eq!(at(&app), Slot::Card(Card::Random), "the last card");
for want in [
Slot::MakeOne,
map(MapChoice::GenOcean),
map(MapChoice::GenSmall),
map(MapChoice::TheFountain),
map(MapChoice::ThreeLanes),
map(MapChoice::Classic),
map(MapChoice::Classic),
] {
tap(&mut app, KeyCode::KeyW);
assert_eq!(at(&app), want);
}
let shelf = || {
CustomBeaches(
(0..5)
.map(|n| beach(&format!("Beach {n}"), "0 . 1"))
.collect(),
)
};
let mut app = setup(MatchConfig::default(), shelf(), Step::Map);
let slots = map_slots(&shelf());
let at = |app: &App| slots[menu(app).slot].1;
let page = MapPage::of(&slots);
let ocean = slots
.iter()
.position(|(_, slot)| *slot == map(MapChoice::GenOcean))
.expect("the open ocean");
app.world_mut().resource_mut::<MatchMenu>().slot = ocean;
tap(&mut app, KeyCode::KeyS);
assert_eq!(at(&app), Slot::Card(Card::Shelf(0)));
tap(&mut app, KeyCode::KeyD);
tap(&mut app, KeyCode::KeyS);
assert_eq!(at(&app), Slot::Card(Card::Shelf(4)), "the short row's card");
tap(&mut app, KeyCode::KeyS);
assert_eq!(at(&app), map(MapChoice::Surprise));
tap(&mut app, KeyCode::KeyD);
tap(&mut app, KeyCode::KeyW);
assert_eq!(at(&app), Slot::Card(Card::Shelf(4)), "clamped to the row");
assert_eq!(page.rows.len(), 5 + 2 + 1);
}
#[test]
fn the_make_one_card_explains_and_picks_nothing() {
use crate::app::i18n::EN;
let empty = CustomBeaches::default();
let make_one = map_slots(&empty)
.iter()
.position(|(_, slot)| *slot == Slot::MakeOne)
.expect("the card saying how to make a map");
let mut app = setup(
MatchConfig {
map: MapChoice::GenOcean,
..MatchConfig::default()
},
CustomBeaches::default(),
Step::Map,
);
app.world_mut().resource_mut::<MatchMenu>().slot = make_one - 1;
tap(&mut app, KeyCode::KeyD);
assert_eq!(menu(&app).slot, make_one);
assert_eq!(config(&app).map, MapChoice::GenOcean, "nothing picked");
assert!(!config(&app).random);
tap(&mut app, KeyCode::Enter);
assert_eq!(menu(&app).step, Step::Map, "Enter did not go on");
assert_eq!(config(&app).map, MapChoice::GenOcean);
let note = slot_note(&EN, Slot::MakeOne, config(&app), &empty, false);
assert!(
note.contains("Level Editor") && note.contains(EN.ed_levels_title),
"{note}"
);
let words = slot_words(&EN, Slot::MakeOne, 2, &empty, false);
assert_eq!(words.pill, None, "not greyed: nothing is wrong with it");
tap(&mut app, KeyCode::Escape);
tap(&mut app, KeyCode::Enter);
assert_eq!(
map_slots(&empty)[menu(&app).slot].1,
Slot::Card(Card::Map(MapChoice::GenOcean))
);
}
#[test]
fn the_window_keeps_the_cursor_in_view() {
for shelf in [
CustomBeaches::default(),
CustomBeaches(vec![beach("Pair", "0 . 1"), beach("Trio", "0 1 2")]),
CustomBeaches((0..9).map(|n| beach(&format!("B{n}"), "0 . 1")).collect()),
] {
let slots = map_slots(&shelf);
let page = MapPage::of(&slots);
let check = |app: &App| {
let (slot, scroll) = (menu(app).slot, menu(app).scroll);
assert!(page.slot_in_view(scroll, slot), "{slot} at {scroll}");
let row = &page.rows[page.row_of(slot)];
if row.first {
assert!(page.heading_in_view(scroll, row.section), "{slot}");
}
let lines: Vec<f32> = page
.headings
.iter()
.copied()
.chain(page.rows.iter().filter(|row| !row.first).map(|row| row.top))
.collect();
assert!(lines.contains(&scroll), "{scroll} in {lines:?}");
};
let mut app = setup(MatchConfig::default(), shelf, Step::Map);
for key in [KeyCode::KeyD, KeyCode::KeyA] {
for _ in 0..slots.len() {
let before = menu(&app).scroll;
tap(&mut app, key);
check(&app);
let slot = menu(&app).slot;
if page.scrolled(before, slot) == before {
assert_eq!(menu(&app).scroll, before);
}
}
}
for key in [KeyCode::KeyS, KeyCode::KeyW] {
for _ in 0..page.rows.len() {
tap(&mut app, key);
check(&app);
}
}
assert_eq!(menu(&app).scroll, 0.0, "back at the top");
}
}
#[test]
fn step_two_shows_what_the_window_holds() {
use crate::app::i18n::EN;
let empty = CustomBeaches::default();
let slots = map_slots(&empty);
let page = MapPage::of(&slots);
let make_one = slots
.iter()
.position(|(_, slot)| *slot == Slot::MakeOne)
.expect("the card saying how to make a map");
let scroll = page.scrolled(0.0, make_one);
assert!(scroll > 0.0, "the page scrolled to it");
let mut app = App::new();
app.insert_resource(MatchMenu {
step: Step::Map,
slot: make_one,
scroll,
..MatchMenu::default()
});
app.insert_resource(MatchConfig::default());
app.insert_resource(empty);
app.insert_resource(GameSettings::default());
app.init_resource::<LastDevice>();
app.init_resource::<MapPreviews>();
app.insert_resource(crate::app::art::Art::blank());
app.add_systems(Update, (update_match_ui, paint_map_step).chain());
app.update();
app.update();
let world = app.world_mut();
let mut shown: Vec<usize> = world
.query::<(&ui::TileUi, &Visibility)>()
.iter(world)
.filter(|(_, visibility)| **visibility != Visibility::Hidden)
.map(|(tile, _)| tile.index())
.collect();
shown.sort_unstable();
let want: Vec<usize> = (0..slots.len())
.filter(|&slot| page.slot_in_view(scroll, slot))
.collect();
assert_eq!(shown, want);
assert!(shown.contains(&make_one));
let headings: Vec<MapSection> = world
.query::<(&ui::SectionHeading, &Visibility)>()
.iter(world)
.filter(|(_, visibility)| **visibility != Visibility::Hidden)
.map(|(heading, _)| heading.section())
.collect();
assert_eq!(headings, vec![MapSection::Yours]);
let moved = world
.query_filtered::<&Node, With<ui::MapPageUi>>()
.single(world)
.expect("the page")
.margin
.top;
assert_eq!(moved, Val::Px(-scroll));
let note = world
.query_filtered::<&Text, With<ui::StepNote>>()
.single(world)
.expect("the note")
.0
.clone();
assert_eq!(
note,
slot_note(
&EN,
Slot::MakeOne,
&MatchConfig::default(),
&CustomBeaches::default(),
false
)
);
}
#[test]
fn the_host_map_heading_follows_the_table() {
let mut app = App::new();
app.insert_resource(MatchMenu {
step: Step::Map,
online: true,
..MatchMenu::default()
});
app.insert_resource(MatchConfig {
seats: 4,
..MatchConfig::default()
});
app.insert_resource(CustomBeaches::default());
app.insert_resource(GameSettings::default());
app.init_resource::<LastDevice>();
app.insert_resource(crate::app::art::Art::blank());
app.add_systems(Update, update_host_map_ui);
app.update();
app.update();
let heading = |app: &mut App| -> Vec<String> {
let world = app.world_mut();
world
.query_filtered::<&Text, With<ui::HostMapTitle>>()
.iter(world)
.map(|text| text.0.clone())
.collect()
};
assert_eq!(
heading(&mut app),
[ui::host_map_title(&crate::app::i18n::EN, 4)]
);
app.world_mut().resource_mut::<MatchConfig>().seats = 3;
app.update();
assert_eq!(
heading(&mut app),
[ui::host_map_title(&crate::app::i18n::EN, 3)]
);
}