Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/bin/rsp/arena/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ fn build_comparison(args: &CompareArgs) -> Result<ArenaComparison, CompareError>
}

/// Convert a PermutationResult into a GameResult for the TUI.
fn perm_to_game_result(perm: PermutationResult) -> GameResult {
fn perm_to_game_result(perm: PermutationResult, big_blind: f32) -> GameResult {
let num_players = perm.agent_names.len();
let ending_round = ending_round_from_stats(&perm.stats, num_players);
let profits: Vec<f32> = (0..num_players)
Expand All @@ -116,6 +116,7 @@ fn perm_to_game_result(perm: PermutationResult) -> GameResult {
profits,
ending_round,
seat_stats,
big_blind,
}
}

Expand All @@ -125,6 +126,7 @@ fn run_comparison_background(
tx: std::sync::mpsc::SyncSender<SimMessage<GameResult>>,
hand_store: HandStore,
ohh_path: Option<PathBuf>,
big_blind: f32,
) {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut prev_file_size: u64 = 0;
Expand All @@ -138,7 +140,7 @@ fn run_comparison_background(
prev_file_size = current_size;
}

let game_result = perm_to_game_result(perm);
let game_result = perm_to_game_result(perm, big_blind);
// If send fails, the TUI has quit — exit cleanly
let _ = tx.send(SimMessage::GameResult(game_result));
})
Expand All @@ -158,7 +160,10 @@ fn run_comparison_background(
}

/// Run comparison with the TUI dashboard.
fn run_comparison_with_tui(comparison: ArenaComparison) -> Result<(), CompareError> {
fn run_comparison_with_tui(
comparison: ArenaComparison,
big_blind: f32,
) -> Result<(), CompareError> {
let total_games = comparison.total_games();

// Extract OHH path before moving comparison into background thread
Expand All @@ -182,7 +187,7 @@ fn run_comparison_with_tui(comparison: ArenaComparison) -> Result<(), CompareErr
std::thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(move || {
run_comparison_background(comparison, tx, bg_hand_store, ohh_path);
run_comparison_background(comparison, tx, bg_hand_store, ohh_path, big_blind);
})
.expect("failed to spawn comparison thread");

Expand Down Expand Up @@ -211,7 +216,7 @@ pub fn run(mut args: CompareArgs, tui_flags: &TuiFlags) -> Result<(), CompareErr

if tui_flags.should_use_tui() {
comparison.print_configuration_summary();
run_comparison_with_tui(comparison)
run_comparison_with_tui(comparison, args.big_blind)
} else {
// Print configuration summary
comparison.print_configuration_summary();
Expand Down
1 change: 1 addition & 0 deletions src/bin/rsp/arena/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ fn run_generation_inner(
profits,
ending_round,
seat_stats,
big_blind: args.big_blind,
};

// If send fails, the TUI has quit - exit cleanly
Expand Down
1 change: 1 addition & 0 deletions src/bin/rsp/ohh/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ pub fn build_state_from_hands(hands: &[HandHistory]) -> TuiState {
profits,
ending_round,
seat_stats,
big_blind: hand.big_blind_amount,
});
}

Expand Down
118 changes: 111 additions & 7 deletions src/bin/rsp/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ impl App {
self.state.active_panel = self.state.active_panel.next();
self.focus_effect = Some(effects::border_chase());
}
KeyCode::BackTab => {
self.state.active_panel = self.state.active_panel.prev();
self.focus_effect = Some(effects::border_chase());
}
KeyCode::Char('s') => {
self.state.sort_col = self.state.sort_col.next();
self.state.invalidate_display_cache();
Expand Down Expand Up @@ -358,6 +362,10 @@ impl App {
self.state.filter.toggle_winner(name);
self.reset_log_selection();
}
FilterItem::Loser(name) => {
self.state.filter.toggle_loser(name);
self.reset_log_selection();
}
FilterItem::Participant(name) => {
self.state.filter.toggle_participant(name);
self.reset_log_selection();
Expand All @@ -366,6 +374,14 @@ impl App {
self.state.filter.toggle_street(*round);
self.reset_log_selection();
}
FilterItem::WinSize(bucket) => {
self.state.filter.toggle_win_size(*bucket);
self.reset_log_selection();
}
FilterItem::LossSize(bucket) => {
self.state.filter.toggle_loss_size(*bucket);
self.reset_log_selection();
}
FilterItem::PlayerCount(count) => {
self.state.filter.toggle_player_count(*count);
self.reset_log_selection();
Expand Down Expand Up @@ -473,12 +489,13 @@ impl App {
pub fn handle_sim_message(&mut self, msg: SimMessage<GameResult>) {
match msg {
SimMessage::GameResult(result) => {
let entry = GameLogEntry {
game_number: self.state.games_completed + 1,
agent_names: result.agent_names.clone(),
profits: result.profits.clone(),
ending_round: result.ending_round,
};
let entry = GameLogEntry::new(
self.state.games_completed + 1,
result.agent_names.clone(),
result.profits.clone(),
result.ending_round,
result.big_blind,
);
self.state.update(result);
self.filtered_log.on_new_game(&entry, &self.state.filter);
}
Expand Down Expand Up @@ -658,6 +675,18 @@ mod tests {
assert_eq!(app.state.active_panel, Panel::Table);
}

#[test]
fn test_shift_tab_switches_panel_reverse() {
let mut app = App::new(Some(10));
assert_eq!(app.state.active_panel, Panel::Table);
app.handle_key(key(KeyCode::BackTab));
assert_eq!(app.state.active_panel, Panel::Filter);
app.handle_key(key(KeyCode::BackTab));
assert_eq!(app.state.active_panel, Panel::GameLog);
app.handle_key(key(KeyCode::BackTab));
assert_eq!(app.state.active_panel, Panel::Table);
}

#[test]
fn test_s_cycles_sort_column() {
let mut app = App::new(Some(10));
Expand Down Expand Up @@ -686,6 +715,7 @@ mod tests {
profits: vec![10.0],
ending_round: RoundLabel::Preflop,
seat_stats: vec![SeatStats::default()],
big_blind: 10.0,
};
app.handle_sim_message(SimMessage::GameResult(result));
assert_eq!(app.state.games_completed, 1);
Expand All @@ -712,6 +742,7 @@ mod tests {
profits: profits.to_vec(),
ending_round: round,
seat_stats,
big_blind: 10.0,
}));
}

Expand Down Expand Up @@ -764,7 +795,7 @@ mod tests {
}

#[test]
fn test_filter_panel_enter_toggles_item() {
fn test_filter_panel_enter_toggles_winner() {
let mut app = App::new(Some(10));
add_game(
&mut app,
Expand Down Expand Up @@ -800,6 +831,79 @@ mod tests {
assert!(app.state.filter.winners.contains("Alice"));
}

#[test]
fn test_filter_panel_enter_toggles_loser() {
let mut app = App::new(Some(10));
add_game(
&mut app,
&["Alice", "Bob"],
&[10.0, -10.0],
RoundLabel::River,
);

app.state.active_panel = Panel::Filter;
// Header "Winner" + 2 winners + Header "Loser" = index 3 is header
// index 4 is Loser("Alice")
app.state.filter.selected = 4;
app.handle_key(key(KeyCode::Enter));
assert!(app.state.filter.losers.contains("Alice"));

app.handle_key(key(KeyCode::Enter));
assert!(!app.state.filter.losers.contains("Alice"));
}

#[test]
fn test_filter_panel_enter_toggles_win_size() {
let mut app = App::new(Some(10));
add_game(
&mut app,
&["Alice", "Bob"],
&[10.0, -10.0],
RoundLabel::River,
);

app.state.active_panel = Panel::Filter;
// Winner(2) + Loser(2) + Participant(2) + headers(3) = 9
// + Header "Street" = 10, + 5 streets = 15
// + Header "Win Size" = 16, first WinSize = 17
// With 2 agents: indices are:
// 0: Header Winner, 1-2: winners, 3: Header Loser, 4-5: losers,
// 6: Header Participant, 7-8: participants,
// 9: Header Street, 10-14: streets,
// 15: Header Win Size, 16: WinSize(Small)
app.state.filter.selected = 16;
app.handle_key(key(KeyCode::Enter));
assert!(
app.state
.filter
.win_sizes
.contains(&crate::tui::state::ProfitBucket::Small)
);
}

#[test]
fn test_filter_panel_enter_toggles_loss_size() {
let mut app = App::new(Some(10));
add_game(
&mut app,
&["Alice", "Bob"],
&[10.0, -10.0],
RoundLabel::River,
);

app.state.active_panel = Panel::Filter;
// After Win Size section: 16-19 are WinSize buckets
// 20: Header Loss Size, 21: LossSize(Small)
app.state.filter.selected = 21;
app.handle_key(key(KeyCode::Enter));
assert!(
app.state
.filter
.loss_sizes
.contains(&crate::tui::state::ProfitBucket::Small)
);
}

#[test]
fn test_filter_panel_header_is_noop() {
let mut app = App::new(Some(10));
Expand Down
45 changes: 16 additions & 29 deletions src/bin/rsp/tui/filtered_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,13 @@ impl FilteredGameLog {
};
match hand_store.fetch_entry(game_number) {
Ok(Some(entry)) => self.window_cache.push(entry),
_ => self.window_cache.push(GameLogEntry {
_ => self.window_cache.push(GameLogEntry::new(
game_number,
agent_names: vec![],
profits: vec![],
ending_round: crate::tui::state::RoundLabel::Preflop,
}),
vec![],
vec![],
crate::tui::state::RoundLabel::Preflop,
0.0,
)),
}
}
}
Expand Down Expand Up @@ -213,12 +214,7 @@ mod tests {
fn test_on_new_game_no_filter() {
let mut log = FilteredGameLog::new();
let filter = FilterState::default();
let entry = GameLogEntry {
game_number: 1,
agent_names: vec!["A".into()],
profits: vec![1.0],
ending_round: RoundLabel::Preflop,
};
let entry = GameLogEntry::new(1, vec!["A".into()], vec![1.0], RoundLabel::Preflop, 10.0);
log.on_new_game(&entry, &filter);
assert_eq!(log.total(), 1);
}
Expand All @@ -231,12 +227,7 @@ mod tests {
let mut filter = FilterState::default();
filter.toggle_street(RoundLabel::River);

let entry = GameLogEntry {
game_number: 1,
agent_names: vec!["A".into()],
profits: vec![1.0],
ending_round: RoundLabel::River,
};
let entry = GameLogEntry::new(1, vec!["A".into()], vec![1.0], RoundLabel::River, 10.0);
log.on_new_game(&entry, &filter);
assert_eq!(log.total(), 1);
assert_eq!(log.game_number_at(0), Some(1));
Expand All @@ -250,12 +241,7 @@ mod tests {
let mut filter = FilterState::default();
filter.toggle_street(RoundLabel::River);

let entry = GameLogEntry {
game_number: 1,
agent_names: vec!["A".into()],
profits: vec![1.0],
ending_round: RoundLabel::Flop,
};
let entry = GameLogEntry::new(1, vec!["A".into()], vec![1.0], RoundLabel::Flop, 10.0);
log.on_new_game(&entry, &filter);
assert_eq!(log.total(), 0);
}
Expand Down Expand Up @@ -300,12 +286,13 @@ mod tests {
#[test]
fn test_invalidate_clears_cache() {
let mut log = FilteredGameLog::new();
log.window_cache.push(GameLogEntry {
game_number: 1,
agent_names: vec!["A".into()],
profits: vec![1.0],
ending_round: RoundLabel::Preflop,
});
log.window_cache.push(GameLogEntry::new(
1,
vec!["A".into()],
vec![1.0],
RoundLabel::Preflop,
10.0,
));
assert_eq!(log.window_cache.len(), 1);
log.invalidate();
assert!(log.window_cache.is_empty());
Expand Down
8 changes: 8 additions & 0 deletions src/bin/rsp/tui/screens/overview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use ratatui::{

use crate::tui::{
state::{AgentDisplayData, GameLogEntry, Panel, TuiState},
theme,
widgets::{
filter_panel::render_filter_panel, game_log::render_game_log,
profit_chart::render_profit_chart, progress_bar::render_progress,
Expand Down Expand Up @@ -101,13 +102,20 @@ pub fn render_overview(
Layout::horizontal([Constraint::Percentage(70), Constraint::Percentage(30)])
.split(main_chunks[1]);

let agent_colors: std::collections::HashMap<&str, ratatui::style::Color> = agents
.iter()
.enumerate()
.map(|(i, a)| (a.name.as_str(), theme::agent_color(i)))
.collect();

render_game_log(
frame,
bottom_chunks[0],
game_log_entries,
0,
log_selected,
log_focused,
&agent_colors,
);

let player_counts: Vec<usize> = state.distinct_player_counts.iter().copied().collect();
Expand Down
Loading
Loading