summaryrefslogtreecommitdiffstats
path: root/tests/client_core.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/client_core.rs')
-rw-r--r--tests/client_core.rs151
1 files changed, 107 insertions, 44 deletions
diff --git a/tests/client_core.rs b/tests/client_core.rs
index 2f43d47..3a2fb27 100644
--- a/tests/client_core.rs
+++ b/tests/client_core.rs
@@ -7,17 +7,17 @@
7use std::time::Duration; 7use std::time::Duration;
8use zagan_bet::blackjack::BlackjackState; 8use zagan_bet::blackjack::BlackjackState;
9use zagan_bet::client::core::{ClientCore, Effect}; 9use zagan_bet::client::core::{ClientCore, Effect};
10use zagan_bet::client::input::{game_input, Editor, GameInput, Input, Key};
11use zagan_bet::client::shell::{self, Shell};
12use zagan_bet::client::ui::help_lines;
13use zagan_bet::client::games::response::{ 10use zagan_bet::client::games::response::{
14 parse_blackjack_response, parse_crash_response, parse_spin_response, 11 parse_blackjack_response, parse_crash_response, parse_spin_response,
15}; 12};
16use zagan_bet::client::games::roulette::{Cursor, Direction}; 13use zagan_bet::client::games::roulette::{Cursor, Direction};
14use zagan_bet::client::input::{Editor, GameInput, Input, Key, game_input};
15use zagan_bet::client::shell::{self, Shell};
17use zagan_bet::client::ui::command_names; 16use zagan_bet::client::ui::command_names;
17use zagan_bet::client::ui::help_lines;
18use zagan_bet::client::view::{Command, RoulettePhase, SlotsPhase, View}; 18use zagan_bet::client::view::{Command, RoulettePhase, SlotsPhase, View};
19use zagan_bet::roulette::BetTarget;
20use zagan_bet::crash::{CrashState, multiplier_at}; 19use zagan_bet::crash::{CrashState, multiplier_at};
20use zagan_bet::roulette::BetTarget;
21 21
22const WIN3: &str = "CCCCLLCLODGSDGS"; 22const WIN3: &str = "CCCCLLCLODGSDGS";
23const LOSS: &str = "CLOOGDSLCDCOGLS"; 23const LOSS: &str = "CLOOGDSLCDCOGLS";
@@ -54,7 +54,7 @@ impl Screen {
54 self.region_start = None; 54 self.region_start = None;
55 self.region_len = 0; 55 self.region_len = 0;
56 } 56 }
57 Effect::SendLine(line) => sent.push(line.clone()), 57 Effect::SendRequest { payload, .. } => sent.push(payload.clone()),
58 Effect::Quit | Effect::Disconnect => {} 58 Effect::Quit | Effect::Disconnect => {}
59 } 59 }
60 } 60 }
@@ -601,7 +601,7 @@ fn ping_prints_pong() {
601 h.line("ping"); 601 h.line("ping");
602 assert_eq!(h.sent.last().map(String::as_str), Some("PING")); 602 assert_eq!(h.sent.last().map(String::as_str), Some("PING"));
603 603
604 h.reply("PONG"); 604 h.reply("PONG 1700000000000");
605 605
606 assert!(h.screen.lines.iter().any(|line| line == "Pong")); 606 assert!(h.screen.lines.iter().any(|line| line == "Pong"));
607} 607}
@@ -968,6 +968,13 @@ fn crash_state(
968 crash_point, 968 crash_point,
969 payout, 969 payout,
970 result: phase.to_string(), 970 result: phase.to_string(),
971 started_at: 1_700_000_000_000,
972 commit: Some("00".repeat(32)),
973 seed: if phase == "running" {
974 None
975 } else {
976 Some("00".repeat(32))
977 },
971 } 978 }
972} 979}
973 980
@@ -1221,11 +1228,12 @@ fn history_prints_recent_plays() {
1221 let json = r#"[{"game":"roulette","bet":10,"winnings":20,"balance_after":1010,"at":"2026-10-30 01:20:11","details":{"number":"17","color":"black"}}]"#; 1228 let json = r#"[{"game":"roulette","bet":10,"winnings":20,"balance_after":1010,"at":"2026-10-30 01:20:11","details":{"number":"17","color":"black"}}]"#;
1222 h.reply(&format!("HISTORY {json}")); 1229 h.reply(&format!("HISTORY {json}"));
1223 1230
1224 assert!(h 1231 assert!(
1225 .screen 1232 h.screen
1226 .lines 1233 .lines
1227 .iter() 1234 .iter()
1228 .any(|line| line.starts_with("Recent plays (1)"))); 1235 .any(|line| line.starts_with("Recent plays (1)"))
1236 );
1229 let row = h 1237 let row = h
1230 .screen 1238 .screen
1231 .lines 1239 .lines
@@ -1242,11 +1250,12 @@ fn history_without_plays_says_so() {
1242 let mut h = Harness::new(); 1250 let mut h = Harness::new();
1243 h.line("history"); 1251 h.line("history");
1244 h.reply("HISTORY []"); 1252 h.reply("HISTORY []");
1245 assert!(h 1253 assert!(
1246 .screen 1254 h.screen
1247 .lines 1255 .lines
1248 .iter() 1256 .iter()
1249 .any(|line| line == "No plays recorded yet.")); 1257 .any(|line| line == "No plays recorded yet.")
1258 );
1250} 1259}
1251 1260
1252#[test] 1261#[test]
@@ -1254,11 +1263,12 @@ fn history_rejects_a_bad_count_without_sending() {
1254 let mut h = Harness::new(); 1263 let mut h = Harness::new();
1255 h.line("history 0"); 1264 h.line("history 0");
1256 assert!(!h.sent.iter().any(|line| line == "HISTORY 0")); 1265 assert!(!h.sent.iter().any(|line| line == "HISTORY 0"));
1257 assert!(h 1266 assert!(
1258 .screen 1267 h.screen
1259 .lines 1268 .lines
1260 .iter() 1269 .iter()
1261 .any(|line| line.contains("Usage: history [1-50]"))); 1270 .any(|line| line.contains("Usage: history [1-50]"))
1271 );
1262} 1272}
1263 1273
1264#[test] 1274#[test]
@@ -1291,10 +1301,7 @@ fn the_roulette_view_reflects_the_cursor_and_bets() {
1291 Some(BetTarget::Straight { number: 2 }) 1301 Some(BetTarget::Straight { number: 2 })
1292 ); 1302 );
1293 assert_eq!(view.bets.total(), 10); 1303 assert_eq!(view.bets.total(), 10);
1294 assert_eq!( 1304 assert_eq!(view.bets.amount_on(&BetTarget::Straight { number: 2 }), 10);
1295 view.bets.amount_on(&BetTarget::Straight { number: 2 }),
1296 10
1297 );
1298} 1305}
1299 1306
1300#[test] 1307#[test]
@@ -1302,7 +1309,9 @@ fn clicking_a_bet_target_does_not_move_the_cursor() {
1302 let mut h = Harness::new(); 1309 let mut h = Harness::new();
1303 h.enter_roulette(); 1310 h.enter_roulette();
1304 1311
1305 let effects = h.core.command(Command::PlaceBetAt(BetTarget::Split { a: 1, b: 4 })); 1312 let effects = h
1313 .core
1314 .command(Command::PlaceBetAt(BetTarget::Split { a: 1, b: 4 }));
1306 h.apply(effects); 1315 h.apply(effects);
1307 1316
1308 let Some(View::Roulette(view)) = h.core.view() else { 1317 let Some(View::Roulette(view)) = h.core.view() else {
@@ -1387,9 +1396,7 @@ fn the_slots_view_carries_the_result() {
1387 let mut h = Harness::new(); 1396 let mut h = Harness::new();
1388 h.enter_slots("10"); 1397 h.enter_slots("10");
1389 h.key(Key::Char(' ')); 1398 h.key(Key::Char(' '));
1390 h.reply(&format!( 1399 h.reply(&format!("GAME_RESULT s1 20 10 1010 {WIN3} win 1 0,3,6"));
1391 "GAME_RESULT s1 20 10 1010 {WIN3} win 1 0,3,6"
1392 ));
1393 1400
1394 let Some(View::Slots(view)) = h.core.view() else { 1401 let Some(View::Slots(view)) = h.core.view() else {
1395 panic!("expected a slots view"); 1402 panic!("expected a slots view");
@@ -1518,11 +1525,12 @@ fn history_is_a_screen_and_keeps_its_records() {
1518 assert_eq!(view.records[0].summary(), "cashed 1.13x"); 1525 assert_eq!(view.records[0].summary(), "cashed 1.13x");
1519 1526
1520 // The terminal adapter still prints the table. 1527 // The terminal adapter still prints the table.
1521 assert!(h 1528 assert!(
1522 .screen 1529 h.screen
1523 .lines 1530 .lines
1524 .iter() 1531 .iter()
1525 .any(|line| line.starts_with("Recent plays (1)"))); 1532 .any(|line| line.starts_with("Recent plays (1)"))
1533 );
1526} 1534}
1527 1535
1528#[test] 1536#[test]
@@ -1563,11 +1571,12 @@ fn a_login_command_sends_auth_and_sets_the_user() {
1563 }; 1571 };
1564 assert_eq!(lobby.user.as_deref(), Some("alice")); 1572 assert_eq!(lobby.user.as_deref(), Some("alice"));
1565 assert!(lobby.login_error.is_none()); 1573 assert!(lobby.login_error.is_none());
1566 assert!(h 1574 assert!(
1567 .screen 1575 h.screen
1568 .lines 1576 .lines
1569 .iter() 1577 .iter()
1570 .any(|line| line == "Logged in as alice")); 1578 .any(|line| line == "Logged in as alice")
1579 );
1571} 1580}
1572 1581
1573#[test] 1582#[test]
@@ -1600,11 +1609,12 @@ fn a_register_command_sends_register_and_reports_the_result() {
1600 assert_eq!(h.sent.last().map(String::as_str), Some("REGISTER bob hash")); 1609 assert_eq!(h.sent.last().map(String::as_str), Some("REGISTER bob hash"));
1601 1610
1602 h.reply("REGISTERED bob"); 1611 h.reply("REGISTERED bob");
1603 assert!(h 1612 assert!(
1604 .screen 1613 h.screen
1605 .lines 1614 .lines
1606 .iter() 1615 .iter()
1607 .any(|line| line == "Account created. Log in to play.")); 1616 .any(|line| line == "Account created. Log in to play.")
1617 );
1608} 1618}
1609 1619
1610#[test] 1620#[test]
@@ -1646,3 +1656,56 @@ fn the_crash_multiplier_is_not_rounded_for_the_chart() {
1646 "a two-decimal store would flatten the curve into steps" 1656 "a two-decimal store would flatten the curve into steps"
1647 ); 1657 );
1648} 1658}
1659
1660#[test]
1661fn a_reply_is_routed_by_request_id_and_late_replies_are_ignored() {
1662 let mut h = Harness::new();
1663 let first = h.core.command(Command::Ping);
1664 let first_id = first
1665 .iter()
1666 .find_map(|effect| match effect {
1667 Effect::SendRequest { id, .. } => Some(*id),
1668 _ => None,
1669 })
1670 .unwrap();
1671
1672 let second = h.core.command(Command::Info);
1673 let second_id = second
1674 .iter()
1675 .find_map(|effect| match effect {
1676 Effect::SendRequest { id, .. } => Some(*id),
1677 _ => None,
1678 })
1679 .unwrap();
1680 assert_ne!(first_id, second_id, "ids must be unique on the connection");
1681
1682 // The ping reply arrives after INFO was sent. It must not complete INFO.
1683 assert!(
1684 h.core
1685 .handle_reply_id(first_id, "PONG 1700000000000")
1686 .is_empty()
1687 );
1688 let effects = h.core.handle_reply_id(second_id, "INFO alice 1 1000");
1689 assert!(
1690 effects.iter().any(|effect| matches!(effect, Effect::Print(lines) if lines.iter().any(|line| line.contains("Balance: 1000")))),
1691 "the matching reply is applied"
1692 );
1693}
1694
1695#[test]
1696fn a_crash_tick_updates_the_board_for_its_own_session() {
1697 let mut h = Harness::new();
1698 h.enter_crash();
1699 h.key(Key::Char(' '));
1700 let running = crash_state("running", 1.0, None, 0, 990);
1701 h.reply(&crash_line("CRASH_START", &running));
1702 assert!(h.board()[2].contains("Multiplier: 1.00x"));
1703
1704 h.push("crash.tick session=cr1 t=1700000000100 m=1.5");
1705 assert!(h.board()[2].contains("Multiplier: 1.50x"));
1706 assert!(h.board()[3].contains("Potential: 15 Kr"));
1707
1708 // A tick for another round cannot move this one.
1709 h.push("crash.tick session=other t=1700000000200 m=9.0");
1710 assert!(h.board()[2].contains("Multiplier: 1.50x"));
1711}