Manual Team Selection + CPU Auto-Fill (TUI + Quick Mode)
This commit is contained in:
parent
dae34cce41
commit
e3242583b6
5 changed files with 374 additions and 35 deletions
17
DEVLOG.md
17
DEVLOG.md
|
|
@ -37,3 +37,20 @@
|
||||||
- No external data files are required at runtime.
|
- No external data files are required at runtime.
|
||||||
- Team data is fully embedded in the binary.
|
- Team data is fully embedded in the binary.
|
||||||
- CSV export format is mode-specific and generated with std I/O.
|
- CSV export format is mode-specific and generated with std I/O.
|
||||||
|
|
||||||
|
## 2026-02-10 - Team selection controls (manual + CPU auto-fill)
|
||||||
|
|
||||||
|
### Scope completed
|
||||||
|
- Added interactive create modal in TUI for team slot configuration.
|
||||||
|
- Added per-slot selection mode:
|
||||||
|
- `Manual` team selection
|
||||||
|
- `CPU auto-fill`
|
||||||
|
- Kept deterministic behavior by resolving CPU team picks with the instance seed.
|
||||||
|
- Added keyboard controls for modal flow:
|
||||||
|
- `m` set manual, `p` set CPU
|
||||||
|
- `[` / `]` or left/right to cycle manual teams
|
||||||
|
- up/down to switch slot
|
||||||
|
- Enter to create, Esc to cancel
|
||||||
|
- Updated `quick` command to support optional `--home` / `--away`:
|
||||||
|
- If missing, CPU auto-fills from remaining teams.
|
||||||
|
- Same seed yields the same auto-filled teams and outcomes.
|
||||||
|
|
|
||||||
262
src/app.rs
262
src/app.rs
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
@ -52,10 +53,50 @@ pub struct App {
|
||||||
pub instances: Vec<SimulationInstance>,
|
pub instances: Vec<SimulationInstance>,
|
||||||
pub selected: usize,
|
pub selected: usize,
|
||||||
pub show_detail: bool,
|
pub show_detail: bool,
|
||||||
|
pub create_draft: Option<CreateDraft>,
|
||||||
pub status_line: String,
|
pub status_line: String,
|
||||||
next_id: usize,
|
next_id: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TeamSlotDraft {
|
||||||
|
pub is_cpu: bool,
|
||||||
|
pub team_idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CreateDraft {
|
||||||
|
pub mode: SimulationType,
|
||||||
|
pub slots: Vec<TeamSlotDraft>,
|
||||||
|
pub selected_slot: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateDraft {
|
||||||
|
pub fn new(mode: SimulationType) -> Self {
|
||||||
|
let count = slot_count_for_mode(mode);
|
||||||
|
let mut slots = Vec::with_capacity(count);
|
||||||
|
for i in 0..count {
|
||||||
|
slots.push(TeamSlotDraft {
|
||||||
|
is_cpu: true,
|
||||||
|
team_idx: i % TEAMS.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
mode,
|
||||||
|
slots,
|
||||||
|
selected_slot: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mode_label(&self) -> &'static str {
|
||||||
|
match self.mode {
|
||||||
|
SimulationType::Single => "Single Match",
|
||||||
|
SimulationType::League4 => "4-Team League",
|
||||||
|
SimulationType::Knockout4 => "4-Team Knockout",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(base_seed: u64, speed: Speed) -> Self {
|
pub fn new(base_seed: u64, speed: Speed) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -64,24 +105,113 @@ impl App {
|
||||||
instances: Vec::with_capacity(MAX_INSTANCES),
|
instances: Vec::with_capacity(MAX_INSTANCES),
|
||||||
selected: 0,
|
selected: 0,
|
||||||
show_detail: false,
|
show_detail: false,
|
||||||
|
create_draft: None,
|
||||||
status_line: format!("Ready. Seed={base_seed}, Speed={}", speed.label()),
|
status_line: format!("Ready. Seed={base_seed}, Speed={}", speed.label()),
|
||||||
next_id: 0,
|
next_id: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_instance(&mut self, sim_type: SimulationType) {
|
pub fn open_create_draft(&mut self, sim_type: SimulationType) {
|
||||||
|
self.create_draft = Some(CreateDraft::new(sim_type));
|
||||||
|
self.status_line = format!("Create modal: {}", sim_type.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel_create_draft(&mut self) {
|
||||||
|
self.create_draft = None;
|
||||||
|
self.status_line = "Create canceled".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_instance_seed_preview(&self) -> u64 {
|
||||||
|
derive_seed(self.base_seed, self.next_id as u64 + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draft_select_prev_slot(&mut self) {
|
||||||
|
let Some(draft) = self.create_draft.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if draft.selected_slot == 0 {
|
||||||
|
draft.selected_slot = draft.slots.len().saturating_sub(1);
|
||||||
|
} else {
|
||||||
|
draft.selected_slot -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draft_select_next_slot(&mut self) {
|
||||||
|
let Some(draft) = self.create_draft.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if draft.slots.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
draft.selected_slot = (draft.selected_slot + 1) % draft.slots.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draft_set_selected_manual(&mut self) {
|
||||||
|
let Some(draft) = self.create_draft.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(slot) = draft.slots.get_mut(draft.selected_slot) {
|
||||||
|
slot.is_cpu = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draft_set_selected_cpu(&mut self) {
|
||||||
|
let Some(draft) = self.create_draft.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(slot) = draft.slots.get_mut(draft.selected_slot) {
|
||||||
|
slot.is_cpu = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draft_cycle_selected_team(&mut self, delta: i32) {
|
||||||
|
let Some(draft) = self.create_draft.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(slot) = draft.slots.get_mut(draft.selected_slot) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if slot.is_cpu {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = TEAMS.len() as i32;
|
||||||
|
let mut idx = slot.team_idx as i32 + delta;
|
||||||
|
if idx < 0 {
|
||||||
|
idx = len - 1;
|
||||||
|
} else if idx >= len {
|
||||||
|
idx = 0;
|
||||||
|
}
|
||||||
|
slot.team_idx = idx as usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn confirm_create_draft(&mut self) {
|
||||||
if self.instances.len() >= MAX_INSTANCES {
|
if self.instances.len() >= MAX_INSTANCES {
|
||||||
self.status_line = format!("Instance limit reached ({MAX_INSTANCES})");
|
self.status_line = format!("Instance limit reached ({MAX_INSTANCES})");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(draft) = self.create_draft.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let id = self.next_id;
|
let id = self.next_id;
|
||||||
let seed = derive_seed(self.base_seed, id as u64 + 1);
|
let seed = derive_seed(self.base_seed, id as u64 + 1);
|
||||||
let teams = self.random_teams_for_mode(sim_type, seed);
|
|
||||||
let instance = SimulationInstance::new(id, sim_type, teams, seed);
|
let teams = match resolve_teams_from_slots(&draft.slots, seed) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
self.status_line = e;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let instance = SimulationInstance::new(id, draft.mode, teams, seed);
|
||||||
self.instances.push(instance);
|
self.instances.push(instance);
|
||||||
self.selected = self.instances.len().saturating_sub(1);
|
self.selected = self.instances.len().saturating_sub(1);
|
||||||
self.next_id += 1;
|
self.next_id += 1;
|
||||||
self.status_line = format!("Created sim-{id}");
|
self.status_line = format!("Created sim-{id}");
|
||||||
|
self.create_draft = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_selected(&mut self) {
|
pub fn start_selected(&mut self) {
|
||||||
|
|
@ -146,14 +276,6 @@ impl App {
|
||||||
self.status_line = format!("Speed set to {}", self.speed.label());
|
self.status_line = format!("Speed set to {}", self.speed.label());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn random_teams_for_mode(&self, sim_type: SimulationType, seed: u64) -> Vec<String> {
|
|
||||||
let count = match sim_type {
|
|
||||||
SimulationType::Single => 2,
|
|
||||||
SimulationType::League4 | SimulationType::Knockout4 => 4,
|
|
||||||
};
|
|
||||||
unique_random_teams(count, seed)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(&mut self) {
|
pub fn tick(&mut self) {
|
||||||
let frames = self.speed.frames_per_tick();
|
let frames = self.speed.frames_per_tick();
|
||||||
for inst in &mut self.instances {
|
for inst in &mut self.instances {
|
||||||
|
|
@ -205,13 +327,34 @@ pub fn run_tui(mut app: App) -> io::Result<()> {
|
||||||
if event::poll(timeout)? {
|
if event::poll(timeout)? {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.kind == KeyEventKind::Press {
|
if key.kind == KeyEventKind::Press {
|
||||||
|
if app.create_draft.is_some() {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Esc => app.cancel_create_draft(),
|
||||||
|
KeyCode::Enter => app.confirm_create_draft(),
|
||||||
|
KeyCode::Up | KeyCode::Char('k') | KeyCode::Tab => {
|
||||||
|
app.draft_select_prev_slot()
|
||||||
|
}
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => app.draft_select_next_slot(),
|
||||||
|
KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('[') => {
|
||||||
|
app.draft_cycle_selected_team(-1)
|
||||||
|
}
|
||||||
|
KeyCode::Right | KeyCode::Char('l') | KeyCode::Char(']') => {
|
||||||
|
app.draft_cycle_selected_team(1)
|
||||||
|
}
|
||||||
|
KeyCode::Char('m') => app.draft_set_selected_manual(),
|
||||||
|
KeyCode::Char('p') => app.draft_set_selected_cpu(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => running = false,
|
KeyCode::Char('q') => running = false,
|
||||||
KeyCode::Up | KeyCode::Char('k') => app.select_prev(),
|
KeyCode::Up | KeyCode::Char('k') => app.select_prev(),
|
||||||
KeyCode::Down | KeyCode::Char('j') => app.select_next(),
|
KeyCode::Down | KeyCode::Char('j') => app.select_next(),
|
||||||
KeyCode::Char('n') => app.create_instance(SimulationType::Single),
|
KeyCode::Char('n') => app.open_create_draft(SimulationType::Single),
|
||||||
KeyCode::Char('l') => app.create_instance(SimulationType::League4),
|
KeyCode::Char('l') => app.open_create_draft(SimulationType::League4),
|
||||||
KeyCode::Char('o') => app.create_instance(SimulationType::Knockout4),
|
KeyCode::Char('o') => app.open_create_draft(SimulationType::Knockout4),
|
||||||
KeyCode::Char('s') => app.start_selected(),
|
KeyCode::Char('s') => app.start_selected(),
|
||||||
KeyCode::Char('c') => app.clone_selected(),
|
KeyCode::Char('c') => app.clone_selected(),
|
||||||
KeyCode::Char('d') => app.delete_selected(),
|
KeyCode::Char('d') => app.delete_selected(),
|
||||||
|
|
@ -239,13 +382,88 @@ pub fn run_tui(mut app: App) -> io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unique_random_teams(count: usize, seed: u64) -> Vec<String> {
|
fn slot_count_for_mode(sim_type: SimulationType) -> usize {
|
||||||
let mut rng = Rng::new(seed);
|
match sim_type {
|
||||||
let mut pool: Vec<&str> = TEAMS.to_vec();
|
SimulationType::Single => 2,
|
||||||
let mut picked = Vec::with_capacity(count);
|
SimulationType::League4 | SimulationType::Knockout4 => 4,
|
||||||
while picked.len() < count && !pool.is_empty() {
|
|
||||||
let i = rng.range_usize(pool.len());
|
|
||||||
picked.push(pool.remove(i).to_string());
|
|
||||||
}
|
}
|
||||||
picked
|
}
|
||||||
|
|
||||||
|
fn resolve_teams_from_slots(slots: &[TeamSlotDraft], seed: u64) -> Result<Vec<String>, String> {
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
let mut cpu_count = 0usize;
|
||||||
|
|
||||||
|
for slot in slots {
|
||||||
|
if slot.is_cpu {
|
||||||
|
cpu_count += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let team = TEAMS
|
||||||
|
.get(slot.team_idx)
|
||||||
|
.ok_or_else(|| "Manual team selection is out of range".to_string())?;
|
||||||
|
if !seen.insert(*team) {
|
||||||
|
return Err(format!("Duplicate manual team: {team}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut remaining: Vec<&str> = TEAMS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|team| !seen.contains(team))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if remaining.len() < cpu_count {
|
||||||
|
return Err("Not enough teams left for CPU auto-fill".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut rng = Rng::new(seed);
|
||||||
|
let mut output: Vec<String> = Vec::with_capacity(slots.len());
|
||||||
|
|
||||||
|
for slot in slots {
|
||||||
|
if slot.is_cpu {
|
||||||
|
let i = rng.range_usize(remaining.len());
|
||||||
|
output.push(remaining.remove(i).to_string());
|
||||||
|
} else {
|
||||||
|
let team = TEAMS
|
||||||
|
.get(slot.team_idx)
|
||||||
|
.ok_or_else(|| "Manual team selection is out of range".to_string())?;
|
||||||
|
output.push((*team).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_quick_single_teams(
|
||||||
|
home: Option<&str>,
|
||||||
|
away: Option<&str>,
|
||||||
|
selection_seed: u64,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
let mut slots = vec![
|
||||||
|
TeamSlotDraft {
|
||||||
|
is_cpu: home.is_none(),
|
||||||
|
team_idx: 0,
|
||||||
|
},
|
||||||
|
TeamSlotDraft {
|
||||||
|
is_cpu: away.is_none(),
|
||||||
|
team_idx: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(home_team) = home {
|
||||||
|
let idx = TEAMS
|
||||||
|
.iter()
|
||||||
|
.position(|t| *t == home_team)
|
||||||
|
.ok_or_else(|| format!("Unknown home team: {home_team}"))?;
|
||||||
|
slots[0].team_idx = idx;
|
||||||
|
}
|
||||||
|
if let Some(away_team) = away {
|
||||||
|
let idx = TEAMS
|
||||||
|
.iter()
|
||||||
|
.position(|t| *t == away_team)
|
||||||
|
.ok_or_else(|| format!("Unknown away team: {away_team}"))?;
|
||||||
|
slots[1].team_idx = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_teams_from_slots(&slots, selection_seed)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
src/main.rs
20
src/main.rs
|
|
@ -11,7 +11,7 @@ use std::io::{self, Write};
|
||||||
|
|
||||||
use clap::{Parser, Subcommand, ValueEnum};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
|
|
||||||
use app::{run_tui, App, Speed};
|
use app::{resolve_quick_single_teams, run_tui, App, Speed};
|
||||||
use data::{display_name, TEAMS};
|
use data::{display_name, TEAMS};
|
||||||
use export::simulation_to_csv_bytes;
|
use export::simulation_to_csv_bytes;
|
||||||
use sim::{run_simulation, SimulationType};
|
use sim::{run_simulation, SimulationType};
|
||||||
|
|
@ -35,9 +35,9 @@ struct Cli {
|
||||||
enum Commands {
|
enum Commands {
|
||||||
Quick {
|
Quick {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
home: String,
|
home: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
away: String,
|
away: Option<String>,
|
||||||
},
|
},
|
||||||
List,
|
List,
|
||||||
Export {
|
Export {
|
||||||
|
|
@ -76,10 +76,7 @@ fn main() -> io::Result<()> {
|
||||||
let app = App::new(base_seed, cli.speed);
|
let app = App::new(base_seed, cli.speed);
|
||||||
run_tui(app)
|
run_tui(app)
|
||||||
}
|
}
|
||||||
Some(Commands::Quick { home, away }) => {
|
Some(Commands::Quick { home, away }) => quick_mode(home, away, base_seed),
|
||||||
quick_mode(&home, &away, base_seed);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Some(Commands::List) => {
|
Some(Commands::List) => {
|
||||||
for team in TEAMS {
|
for team in TEAMS {
|
||||||
println!("{}", display_name(team));
|
println!("{}", display_name(team));
|
||||||
|
|
@ -90,8 +87,11 @@ fn main() -> io::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quick_mode(home: &str, away: &str, base_seed: u64) {
|
fn quick_mode(home: Option<String>, away: Option<String>, base_seed: u64) -> io::Result<()> {
|
||||||
let teams = vec![home.to_string(), away.to_string()];
|
let teams =
|
||||||
|
resolve_quick_single_teams(home.as_deref(), away.as_deref(), derive_seed(base_seed, 1))
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
|
||||||
|
|
||||||
let seed = derive_seed(base_seed, 1);
|
let seed = derive_seed(base_seed, 1);
|
||||||
let mut rng = Rng::new(seed);
|
let mut rng = Rng::new(seed);
|
||||||
let prepared = run_simulation(SimulationType::Single, &teams, &mut rng);
|
let prepared = run_simulation(SimulationType::Single, &teams, &mut rng);
|
||||||
|
|
@ -108,6 +108,8 @@ fn quick_mode(home: &str, away: &str, base_seed: u64) {
|
||||||
println!("{}", line);
|
println!("{}", line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn export_mode(mode: ModeArg, out: String, teams: Vec<String>, base_seed: u64) -> io::Result<()> {
|
fn export_mode(mode: ModeArg, out: String, teams: Vec<String>, base_seed: u64) -> io::Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ pub fn draw(f: &mut Frame<'_>, app: &App) {
|
||||||
])
|
])
|
||||||
.split(f.area());
|
.split(f.area());
|
||||||
|
|
||||||
let header = Paragraph::new("MentalNet SoccerCloud | n=single l=league4 o=knockout4 s=start c=clone d=delete e=export v=detail q=quit")
|
let header = Paragraph::new("MentalNet SoccerCloud | n/l/o create | s start | c clone | d delete | e export | v detail | q quit")
|
||||||
.block(Block::default().title("Dashboard").borders(Borders::ALL))
|
.block(Block::default().title("Dashboard").borders(Borders::ALL))
|
||||||
.style(Style::default().fg(Color::Cyan));
|
.style(Style::default().fg(Color::Cyan));
|
||||||
f.render_widget(header, areas[0]);
|
f.render_widget(header, areas[0]);
|
||||||
|
|
@ -31,11 +31,15 @@ pub fn draw(f: &mut Frame<'_>, app: &App) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let footer = Paragraph::new(format!(
|
let footer = Paragraph::new(format!(
|
||||||
"{} | speed={} (1/2/4/0)",
|
"{} | speed={} (1/2/4/0) | modal: m=manual p=cpu [ ] team Enter=create Esc=cancel",
|
||||||
app.status_line,
|
app.status_line,
|
||||||
app.speed.label()
|
app.speed.label()
|
||||||
))
|
))
|
||||||
.block(Block::default().borders(Borders::ALL).title("Status"))
|
.block(Block::default().borders(Borders::ALL).title("Status"))
|
||||||
.style(Style::default().fg(Color::Green));
|
.style(Style::default().fg(Color::Green));
|
||||||
f.render_widget(footer, areas[2]);
|
f.render_widget(footer, areas[2]);
|
||||||
|
|
||||||
|
if let Some(draft) = &app.create_draft {
|
||||||
|
modal::render(f, f.area(), app, draft);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
102
src/ui/modal.rs
102
src/ui/modal.rs
|
|
@ -1,2 +1,100 @@
|
||||||
// Placeholder module for future interactive creation modal.
|
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||||
// Current MVP uses keyboard shortcuts for fast instance creation.
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph};
|
||||||
|
|
||||||
|
use crate::app::{App, CreateDraft};
|
||||||
|
use crate::data::TEAMS;
|
||||||
|
|
||||||
|
pub fn render(f: &mut Frame<'_>, area: Rect, app: &App, draft: &CreateDraft) {
|
||||||
|
let popup = centered_rect(70, 70, area);
|
||||||
|
f.render_widget(Clear, popup);
|
||||||
|
|
||||||
|
let frame = Block::default()
|
||||||
|
.title("Create Simulation")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default().fg(Color::White).bg(Color::Black));
|
||||||
|
f.render_widget(frame, popup);
|
||||||
|
|
||||||
|
let inner = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.margin(1)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(2),
|
||||||
|
Constraint::Min(4),
|
||||||
|
Constraint::Length(2),
|
||||||
|
])
|
||||||
|
.split(popup);
|
||||||
|
|
||||||
|
let top = Paragraph::new(format!(
|
||||||
|
"Mode: {} | next-seed={} | select slot with up/down",
|
||||||
|
draft.mode_label(),
|
||||||
|
app.next_instance_seed_preview()
|
||||||
|
));
|
||||||
|
f.render_widget(top, inner[0]);
|
||||||
|
|
||||||
|
let mut rows = Vec::with_capacity(draft.slots.len());
|
||||||
|
for (i, slot) in draft.slots.iter().enumerate() {
|
||||||
|
let slot_name = slot_label(draft.slots.len(), i);
|
||||||
|
let content = if slot.is_cpu {
|
||||||
|
format!("{}: CPU auto-fill", slot_name)
|
||||||
|
} else {
|
||||||
|
format!("{}: MANUAL -> {}", slot_name, TEAMS[slot.team_idx])
|
||||||
|
};
|
||||||
|
let mut item = ListItem::new(content);
|
||||||
|
if i == draft.selected_slot {
|
||||||
|
item = item.style(
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Yellow)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rows.push(item);
|
||||||
|
}
|
||||||
|
let list = List::new(rows).block(Block::default().title("Team Slots").borders(Borders::ALL));
|
||||||
|
f.render_widget(list, inner[1]);
|
||||||
|
|
||||||
|
let help = Paragraph::new(
|
||||||
|
"m=manual, p=cpu, [ / ] or left/right change manual team, Enter=create, Esc=cancel",
|
||||||
|
);
|
||||||
|
f.render_widget(help, inner[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
|
||||||
|
let vertical = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Percentage((100 - percent_y) / 2),
|
||||||
|
Constraint::Percentage(percent_y),
|
||||||
|
Constraint::Percentage((100 - percent_y) / 2),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let horizontal = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Percentage((100 - percent_x) / 2),
|
||||||
|
Constraint::Percentage(percent_x),
|
||||||
|
Constraint::Percentage((100 - percent_x) / 2),
|
||||||
|
])
|
||||||
|
.split(vertical[1]);
|
||||||
|
|
||||||
|
horizontal[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slot_label(total: usize, i: usize) -> &'static str {
|
||||||
|
if total == 2 {
|
||||||
|
match i {
|
||||||
|
0 => "Home",
|
||||||
|
1 => "Away",
|
||||||
|
_ => "Slot",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match i {
|
||||||
|
0 => "Team A",
|
||||||
|
1 => "Team B",
|
||||||
|
2 => "Team C",
|
||||||
|
3 => "Team D",
|
||||||
|
_ => "Slot",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue