summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-28 22:28:20 +0100
committersalmonik <salmonik@tuta.io>2026-10-28 22:28:20 +0100
commit86d5f832c8452aad2313489b82eb8fd514215b95 (patch)
tree596a9be39c3d91ebc20cc8b877549e3d10895901 /tests
parentv0.2.2: split client/server logic into the library (diff)
downloadzagan-bet-86d5f832c8452aad2313489b82eb8fd514215b95.tar.gz
zagan-bet-86d5f832c8452aad2313489b82eb8fd514215b95.zip
v0.2.3: add ClientCore and fix the game board rendering
Diffstat (limited to '')
-rw-r--r--tests/client_core.rs216
1 files changed, 216 insertions, 0 deletions
diff --git a/tests/client_core.rs b/tests/client_core.rs
new file mode 100644
index 0000000..3dbbfe5
--- /dev/null
+++ b/tests/client_core.rs
@@ -0,0 +1,216 @@
1//! Behaviour tests for the client core.
2//!
3//! These tests never touch a terminal or a socket. `Screen` applies the
4//! logical `Effect`s that `ClientCore` emits, so assertions are about what a
5//! player would see, not about ANSI sequences.
6
7use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key};
8
9#[derive(Default)]
10struct Screen {
11 lines: Vec<String>,
12 region_start: Option<usize>,
13 region_len: usize,
14 prompt: String,
15}
16
17impl Screen {
18 fn apply(
19 &mut self,
20 effects: &[Effect],
21 sent: &mut Vec<String>,
22 interactive: &mut Vec<InteractiveCommand>,
23 ) {
24 for effect in effects {
25 match effect {
26 Effect::Print(lines) => self.lines.extend(lines.iter().cloned()),
27 Effect::ReplaceRegion(lines) => {
28 if let Some(start) = self.region_start {
29 self.lines.truncate(start);
30 } else {
31 self.region_start = Some(self.lines.len());
32 }
33 self.lines.extend(lines.iter().cloned());
34 self.region_len = lines.len();
35 }
36 Effect::ClearRegion => {
37 if let Some(start) = self.region_start {
38 self.lines.truncate(start);
39 }
40 self.region_start = None;
41 self.region_len = 0;
42 }
43 Effect::SetPrompt { text, .. } => self.prompt = text.clone(),
44 Effect::SendLine(line) => sent.push(line.clone()),
45 Effect::Interactive(command) => interactive.push(command.clone()),
46 Effect::Quit
47 | Effect::Disconnect
48 | Effect::ListHosts
49 | Effect::ForgetHost(_) => {}
50 }
51 }
52 }
53
54 fn board(&self) -> &[String] {
55 match self.region_start {
56 Some(start) => &self.lines[start..start + self.region_len],
57 None => &[],
58 }
59 }
60}
61
62struct Harness {
63 core: ClientCore,
64 screen: Screen,
65 sent: Vec<String>,
66 interactive: Vec<InteractiveCommand>,
67}
68
69impl Harness {
70 fn new() -> Self {
71 let mut core = ClientCore::new();
72 core.set_connected(true);
73 core.set_logged_in(Some("alice".to_string()));
74 Self {
75 core,
76 screen: Screen::default(),
77 sent: Vec::new(),
78 interactive: Vec::new(),
79 }
80 }
81
82 fn apply(&mut self, effects: Vec<Effect>) {
83 self.screen
84 .apply(&effects, &mut self.sent, &mut self.interactive);
85 }
86
87 fn key(&mut self, key: Key) {
88 let effects = self.core.handle_key(key);
89 self.apply(effects);
90 if let Some(prompt) = self.core.prompt_effect() {
91 self.apply(vec![prompt]);
92 }
93 }
94
95 fn line(&mut self, line: &str) {
96 for c in line.chars() {
97 self.key(Key::Char(c));
98 }
99 self.key(Key::Enter);
100 }
101
102 fn reply(&mut self, reply: &str) {
103 let effects = self.core.handle_reply(reply);
104 self.apply(effects);
105 if let Some(prompt) = self.core.prompt_effect() {
106 self.apply(vec![prompt]);
107 }
108 }
109
110 fn board(&self) -> &[String] {
111 self.screen.board()
112 }
113
114 fn enter_slots(&mut self, bet: &str) {
115 self.line(&format!("play slots {bet}"));
116 self.reply("INFO alice 1 1000");
117 }
118}
119
120#[test]
121fn play_shows_a_seven_line_board_and_fetches_the_balance() {
122 let mut h = Harness::new();
123 h.line("play slots 50");
124
125 assert_eq!(h.board().len(), 7);
126 assert!(h.board()[1].contains("Bet: 50 PLN"));
127 assert!(h.sent.contains(&"INFO".to_string()));
128
129 h.reply("INFO alice 1 1000");
130 assert_eq!(h.board().len(), 7);
131 assert!(h.board()[1].contains("Balance: 1000 PLN"));
132}
133
134#[test]
135fn spin_replaces_the_board_and_keeps_the_controls_line() {
136 let mut h = Harness::new();
137 h.enter_slots("50");
138 let lines_before_spin = h.screen.lines.len();
139
140 h.key(Key::Char(' '));
141 assert_eq!(h.board().len(), 7);
142 assert!(h.board()[4].contains("Spinning..."));
143 assert!(h.board()[6].contains("[Q] Quit"));
144 assert!(h.sent.iter().any(|line| line == "PLAY_START slots 50"));
145 assert_eq!(h.screen.lines.len(), lines_before_spin);
146
147 h.reply("GAME_RESULT session-1 60 50 1010 OSS two");
148 assert_eq!(h.board().len(), 7);
149 assert!(h.board()[2].contains("[ O ] [ S ] [ S ]"));
150 assert!(h.board()[4].contains("Two of a kind! Winnings: 60 PLN"));
151 assert!(h.board()[1].contains("Balance: 1010 PLN"));
152 assert!(h.board()[6].contains("[Q] Quit"));
153 assert_eq!(h.screen.lines.len(), lines_before_spin);
154}
155
156#[test]
157fn changing_the_bet_after_a_spin_does_not_leave_stale_lines() {
158 let mut h = Harness::new();
159 h.enter_slots("50");
160 h.key(Key::Char(' '));
161 h.reply("GAME_RESULT session-1 60 50 1010 OSS two");
162 let lines_before_bet = h.screen.lines.len();
163
164 h.key(Key::Char('+'));
165
166 assert_eq!(h.board().len(), 7);
167 assert!(h.board()[1].contains("Bet: 60 PLN"));
168 assert!(h.board()[2].contains("[ O ] [ S ] [ S ]"));
169 assert_eq!(h.screen.lines.len(), lines_before_bet);
170 assert_eq!(
171 h.screen
172 .lines
173 .iter()
174 .filter(|line| line.contains("=== SLOTS ==="))
175 .count(),
176 1
177 );
178}
179
180#[test]
181fn quitting_clears_the_board_and_returns_the_prompt() {
182 let mut h = Harness::new();
183 h.enter_slots("50");
184
185 h.key(Key::Char('q'));
186
187 assert!(h.board().is_empty());
188 assert!(h.screen.lines.iter().any(|line| line == "Game exited."));
189 assert_eq!(h.screen.prompt, "");
190}
191
192#[test]
193fn a_server_error_replaces_the_status_line() {
194 let mut h = Harness::new();
195 h.enter_slots("50");
196 h.key(Key::Char(' '));
197
198 h.reply("ERROR Insufficient balance");
199
200 assert_eq!(h.board().len(), 7);
201 assert!(h.board()[4].contains("ERROR: Insufficient balance"));
202 assert!(h.board()[2].contains("[ ? ]"));
203 assert!(!h.board().iter().any(|line| line.contains("Spinning")));
204}
205
206#[test]
207fn an_empty_reply_is_an_error_not_a_silent_spin() {
208 let mut h = Harness::new();
209 h.enter_slots("50");
210 h.key(Key::Char(' '));
211
212 h.reply("");
213
214 assert_eq!(h.board().len(), 7);
215 assert!(h.board()[4].contains("No response from server"));
216}