chironaut 0.3.5

A poker game library for Texas Hold'em and other poker variants
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
use chironaut::Game;
use chironaut::rules::GameRules;
use chironaut::action::Action;
use chironaut::game::GameStage;

fn main() {
    println!("=== POKER ENGINE ERROR HANDLING TEST ===\n");
    
    println!("Running error handling tests...\n");
    
    // Run each test in sequence, collecting failures
    let mut failed_tests = Vec::new();
    
    // Betting validation tests
    run_test("Invalid bet below minimum", test_bet_below_minimum, &mut failed_tests);
    run_test("Invalid bet above maximum", test_bet_above_maximum, &mut failed_tests);
    run_test("Check when facing bet", test_check_facing_bet, &mut failed_tests);
    run_test("Fold when already folded", test_fold_when_folded, &mut failed_tests);
    run_test("Bet with insufficient chips", test_bet_insufficient_chips, &mut failed_tests);
    run_test("Act when all-in", test_act_when_all_in, &mut failed_tests);
    
    // Straddle validation tests
    run_test("Invalid straddle amount", test_invalid_straddle_amount, &mut failed_tests);
    run_test("Straddle from wrong position", test_straddle_wrong_position, &mut failed_tests);
    run_test("Straddle with insufficient chips", test_straddle_insufficient_chips, &mut failed_tests);
    run_test("Straddle in game without straddles", test_straddle_when_not_allowed, &mut failed_tests);
    
    // Game state tests
    run_test("Action after showdown", test_action_after_showdown, &mut failed_tests);
    run_test("Start hand with no players", test_start_hand_no_players, &mut failed_tests);
    run_test("Start hand with one player", test_start_hand_one_player, &mut failed_tests);
    
    // Results
    if failed_tests.is_empty() {
        println!("\n✅ All error handling tests passed successfully!");
    } else {
        println!("\n{} tests failed:", failed_tests.len());
        for test in &failed_tests {
            println!("  - {}", test);
        }
    }
}

// Test runner helper function
fn run_test<F>(test_name: &str, test_fn: F, failed_tests: &mut Vec<String>)
where
    F: FnOnce() -> Result<(), String>,
{
    println!("\n--- Running test: {} ---", test_name);
    match test_fn() {
        Ok(_) => println!("✅ Test passed: {}", test_name),
        Err(e) => {
            println!("❌ Test failed: {}", test_name);
            println!("   Error: {}", e);
            failed_tests.push(test_name.to_string());
        }
    }
}

// Helper function to set up a standard 4-player game
fn setup_standard_game() -> Game {
    let rules = GameRules::nlhe(10, 20);
    let mut game = Game::new(rules);
    
    for i in 0..4 {
        game.add_player(format!("Player {}", i+1), 1000).unwrap();
    }
    
    game.start_hand().unwrap();
    game
}

// Helper function to set up a game with straddles enabled
fn setup_straddle_game() -> Game {
    let rules = GameRules::nlhe(10, 20).with_straddles();
    let mut game = Game::new(rules);
    
    for i in 0..4 {
        game.add_player(format!("Player {}", i+1), 1000).unwrap();
    }
    
    game.start_hand().unwrap();
    game
}

// TESTS

// Test betting less than the minimum (big blind)
fn test_bet_below_minimum() -> Result<(), String> {
    let mut game = setup_standard_game();
    
    // Current position should be UTG
    let result = game.handle_action(Action::Bet(5));
    
    // Verify the error
    assert!(result.is_err(), "Should reject a bet below minimum");
    if let Err(e) = result {
        assert!(e.contains("Bet must be at least the big blind"), 
                "Expected error about minimum bet, got: {}", e);
    } else {
        return Err("Expected error for bet below minimum".to_string());
    }
    
    Ok(())
}

// Test betting more than available chips
fn test_bet_above_maximum() -> Result<(), String> {
    let mut game = setup_standard_game();
    
    // Current position should be UTG
    let current_pos = game.current_position;
    let player_chips = game.players[current_pos].chips;
    
    // Try to bet more chips than player has
    let result = game.handle_action(Action::Bet(player_chips + 100));
    
    // Should fail - silently capped or error
    if result.is_ok() {
        // If action was accepted, verify it was capped to available chips
        assert_eq!(game.players[current_pos].chips, 0, "Player should have no chips left");
        assert!(game.players[current_pos].is_all_in, "Player should be all-in");
    } else {
        // Or the API rejects the action outright
        if let Err(e) = result {
            assert!(e.contains("insufficient") || e.contains("enough chips"), 
                    "Expected error about insufficient chips, got: {}", e);
        }
    }
    
    Ok(())
}

// Test checking when facing a bet
fn test_check_facing_bet() -> Result<(), String> {
    let mut game = setup_standard_game();
    
    // First player bets
    game.handle_action(Action::Bet(20)).unwrap();
    
    // Second player tries to check
    let result = game.handle_action(Action::Bet(0));
    
    // Verify the error
    assert!(result.is_err(), "Should reject checking when facing a bet");
    if let Err(e) = result {
        assert!(e.contains("Cannot check when there is a bet"), 
                "Expected error about checking when facing bet, got: {}", e);
    } else {
        return Err("Expected error for checking when facing bet".to_string());
    }
    
    Ok(())
}

// Test folding after already folding
fn test_fold_when_folded() -> Result<(), String> {
    let mut game = setup_standard_game();
    
    // First player folds
    game.handle_action(Action::Fold).unwrap();
    
    // Current position has now moved, so we can't test double-folding directly
    // Instead, we'll simulate folding then trying to act again
    
    // Get the position of the player who folded
    let folded_player_idx = 0; // Assuming UTG was position 0
    
    // Ensure they're actually folded
    assert!(game.players[folded_player_idx].is_folded, "Player should be folded");
    
    // API prevents direct access to act for a specific position
    // In a real implementation this would need to be tested at a lower level
    // or with a modified API that allows acting as any player
    
    // For now, we'll confirm that folded players are skipped in position
    assert_ne!(game.current_position, folded_player_idx, 
               "Current position should skip folded player");
    
    Ok(())
}

// Test betting more chips than player has
fn test_bet_insufficient_chips() -> Result<(), String> {
    // Create game with a short-stacked player
    let rules = GameRules::nlhe(10, 20);
    let mut game = Game::new(rules);
    
    game.add_player("Player 1".to_string(), 1000).unwrap();
    game.add_player("Player 2".to_string(), 1000).unwrap();
    game.add_player("Player 3".to_string(), 30).unwrap(); // Only has 30 chips
    game.add_player("Player 4".to_string(), 1000).unwrap();
    
    game.start_hand().unwrap();
    
    // Find short stack position - Player 3
    let short_stack_pos = 2; // Based on order added
    
    // Play until short stack position
    while game.current_position != short_stack_pos {
        game.handle_action(Action::Bet(20)).unwrap(); // Call
    }
    
    // Try to bet more than available
    let result = game.handle_action(Action::Bet(50)); // More than 30 chips
    
    // Either the bet is capped to available chips or rejected
    if result.is_ok() {
        // If accepted, player should be all-in with 0 chips left
        assert_eq!(game.players[short_stack_pos].chips, 0, "Player should have no chips left");
        assert!(game.players[short_stack_pos].is_all_in, "Player should be all-in");
    } else {
        // Or it's rejected with an error
        if let Err(e) = result {
            assert!(e.contains("insufficient") || e.contains("enough chips"), 
                    "Expected error about insufficient chips, got: {}", e);
        }
    }
    
    Ok(())
}

// Test trying to act when all-in
fn test_act_when_all_in() -> Result<(), String> {
    let mut game = setup_standard_game();
    
    // Current position
    let pos = game.current_position;
    
    // Player goes all-in
    game.handle_action(Action::Bet(game.players[pos].chips)).unwrap();
    
    // Verify player is all-in
    assert!(game.players[pos].is_all_in, "Player should be all-in");
    
    // Cannot directly test acting when all-in with the public API
    // In a real scenario, these tests would be at a lower level
    
    // Instead, verify the player is skipped in subsequent rounds
    // First, complete the preflop betting
    while game.stage == GameStage::Preflop {
        let current_pos = game.current_position;
        // Everyone else calls
        if game.current_bet > game.players[current_pos].bet_amount {
            game.handle_action(Action::Bet(game.current_bet - game.players[current_pos].bet_amount)).unwrap();
        } else {
            game.handle_action(Action::Bet(0)).unwrap();
        }
    }
    
    // At the flop, ensure the all-in player isn't the current position
    assert_ne!(game.current_position, pos, "All-in player should be skipped in position");
    
    Ok(())
}

// Test posting a straddle with invalid amount
fn test_invalid_straddle_amount() -> Result<(), String> {
    let mut game = setup_straddle_game();
    
    // Find UTG position
    let utg_pos = (game.get_big_blind_position() + 1) % game.players.len();
    
    // Try to post a straddle smaller than 2x BB
    let result = game.post_straddle(utg_pos, 30); // Less than 2x BB (40)
    
    // Verify the error
    assert!(result.is_err(), "Should reject straddle smaller than 2x BB");
    if let Err(e) = result {
        assert!(e.contains("must be at least 2x"), 
                "Expected error about straddle size, got: {}", e);
    } else {
        return Err("Expected error for invalid straddle amount".to_string());
    }
    
    Ok(())
}

// Test posting a straddle from the wrong position
fn test_straddle_wrong_position() -> Result<(), String> {
    let mut game = setup_straddle_game();
    
    // Find positions
    let utg_pos = (game.get_big_blind_position() + 1) % game.players.len();
    let wrong_pos = (utg_pos + 1) % game.players.len(); // Not UTG
    
    // Try to post a straddle from the wrong position
    let result = game.post_straddle(wrong_pos, 40);
    
    // Verify the error
    assert!(result.is_err(), "Should reject straddle from wrong position");
    if let Err(e) = result {
        assert!(e.contains("must be from the player after the big blind"), 
                "Expected error about straddle position, got: {}", e);
    } else {
        return Err("Expected error for straddle from wrong position".to_string());
    }
    
    Ok(())
}

// Test posting a straddle with insufficient chips
fn test_straddle_insufficient_chips() -> Result<(), String> {
    // Create game with straddles enabled and a short-stacked UTG player
    let rules = GameRules::nlhe(10, 20).with_straddles();
    let mut game = Game::new(rules);
    
    // Add players and ensure UTG has low chips
    for i in 0..4 {
        let chips = if i == 2 { 30 } else { 1000 }; // Player 3 has few chips
        game.add_player(format!("Player {}", i+1), chips).unwrap();
    }
    
    game.start_hand().unwrap();
    
    // Find UTG position
    let utg_pos = (game.get_big_blind_position() + 1) % game.players.len();
    
    // Try to post a straddle larger than available chips
    let result = game.post_straddle(utg_pos, 50); // More than 30 chips
    
    // Verify the error or capping behavior
    if result.is_ok() {
        // If allowed, should cap to available chips and be all-in
        assert_eq!(game.players[utg_pos].chips, 0, "Player should have no chips left");
        assert!(game.players[utg_pos].is_all_in, "Player should be all-in");
    } else {
        // Or error about insufficient chips
        if let Err(e) = result {
            assert!(e.contains("doesn't have enough chips"), 
                    "Expected error about insufficient chips, got: {}", e);
        }
    }
    
    Ok(())
}

// Test posting a straddle in a game where straddles aren't allowed
fn test_straddle_when_not_allowed() -> Result<(), String> {
    // Create standard game WITHOUT straddles enabled
    let mut game = setup_standard_game();
    
    // Find UTG position
    let utg_pos = (game.get_big_blind_position() + 1) % game.players.len();
    
    // Try to post a straddle
    let result = game.post_straddle(utg_pos, 40);
    
    // Verify the error
    assert!(result.is_err(), "Should reject straddle when not allowed");
    if let Err(e) = result {
        assert!(e.contains("not allowed"), 
                "Expected error about straddles not allowed, got: {}", e);
    } else {
        return Err("Expected error for straddle when not allowed".to_string());
    }
    
    Ok(())
}

// Test acting after the hand is at showdown
fn test_action_after_showdown() -> Result<(), String> {
    let mut game = setup_standard_game();
    
    // Play through to showdown with everyone checking
    while game.stage != GameStage::Showdown {
        let curr_pos = game.current_position;
        
        if game.current_bet > game.players[curr_pos].bet_amount {
            game.handle_action(Action::Bet(game.current_bet - game.players[curr_pos].bet_amount)).unwrap();
        } else {
            game.handle_action(Action::Bet(0)).unwrap();
        }
    }
    
    // Verify we're at showdown
    assert_eq!(game.stage, GameStage::Showdown, "Game should be at showdown");
    
    // Try to act after showdown
    let result = game.handle_action(Action::Bet(0));
    
    // Verify the error
    assert!(result.is_err(), "Should reject action after showdown");
    if let Err(e) = result {
        assert!(e.contains("already at showdown"), 
                "Expected error about hand being at showdown, got: {}", e);
    } else {
        return Err("Expected error for action after showdown".to_string());
    }
    
    Ok(())
}

// Test starting a hand with no players
fn test_start_hand_no_players() -> Result<(), String> {
    let rules = GameRules::nlhe(10, 20);
    let mut game = Game::new(rules);
    
    // Try to start hand with no players
    let result = game.start_hand();
    
    // Verify the error
    assert!(result.is_err(), "Should reject starting hand with no players");
    if let Err(e) = result {
        assert!(e.contains("enough players"), 
                "Expected error about not enough players, got: {}", e);
    } else {
        return Err("Expected error for starting hand with no players".to_string());
    }
    
    Ok(())
}

// Test starting a hand with only one player
fn test_start_hand_one_player() -> Result<(), String> {
    let rules = GameRules::nlhe(10, 20);
    let mut game = Game::new(rules);
    
    // Add a single player
    game.add_player("Player 1".to_string(), 1000).unwrap();
    
    // Try to start hand with only one player
    let result = game.start_hand();
    
    // Verify the error
    assert!(result.is_err(), "Should reject starting hand with only one player");
    if let Err(e) = result {
        assert!(e.contains("enough players"), 
                "Expected error about not enough players, got: {}", e);
    } else {
        return Err("Expected error for starting hand with only one player".to_string());
    }
    
    Ok(())
}