Skip to main content

cardinal_kernel/
display.rs

1use colored::Colorize;
2use crate::{
3    state::gamestate::GameState,
4    ids::{PlayerId, CardId},
5    engine::cards::CardRegistry,
6};
7
8/// Game log entry for tracking what happened
9#[derive(Debug, Clone)]
10pub struct LogEntry {
11    pub turn: u32,
12    pub phase: String,
13    pub step: String,
14    pub message: String,
15}
16
17/// Complete game display with formatting
18pub struct GameDisplay {
19    pub game_log: Vec<LogEntry>,
20}
21
22impl GameDisplay {
23    pub fn new() -> Self {
24        Self {
25            game_log: Vec::new(),
26        }
27    }
28
29    pub fn log(&mut self, turn: u32, phase: &str, step: &str, message: String) {
30        self.game_log.push(LogEntry {
31            turn,
32            phase: phase.to_string(),
33            step: step.to_string(),
34            message,
35        });
36    }
37
38    /// Render the complete game state
39    pub fn render_game(
40        &self,
41        state: &GameState,
42        cards: &CardRegistry,
43        viewer: PlayerId,
44    ) -> String {
45        let mut output = String::new();
46
47        // Header
48        output.push_str(&self.render_header(state, viewer));
49        output.push('\n');
50
51        // Your field
52        output.push_str(&self.render_zone(
53            state,
54            cards,
55            viewer,
56            "field",
57            "Your Field",
58            false,
59        ));
60        output.push('\n');
61
62        // Opponent field
63        let opponent = if viewer.0 == 0 { PlayerId(1) } else { PlayerId(0) };
64        output.push_str(&self.render_zone(
65            state,
66            cards,
67            opponent,
68            "field",
69            "Opponent Field",
70            true,
71        ));
72        output.push('\n');
73
74        // Stack
75        output.push_str(&self.render_stack(state));
76        output.push('\n');
77
78        // Hand
79        output.push_str(&self.render_hand(state, cards, viewer));
80
81        output
82    }
83
84    fn render_header(&self, state: &GameState, viewer: PlayerId) -> String {
85        let mut output = String::new();
86
87        // Turn info
88        let turn_info = format!(
89            "Turn {} | Phase: {} | Step: {} | Priority: {:?}",
90            state.turn.number, state.turn.phase.0, state.turn.step.0, state.turn.priority_player
91        );
92        output.push_str(&turn_info.bold().to_string());
93        output.push('\n');
94
95        // Life totals
96        let your_player = viewer;
97        let your_life = state.players.iter()
98            .find(|p| p.id == your_player)
99            .map(|p| p.life)
100            .unwrap_or(0);
101
102        let opponent = if viewer.0 == 0 { PlayerId(1) } else { PlayerId(0) };
103        let opponent_life = state.players.iter()
104            .find(|p| p.id == opponent)
105            .map(|p| p.life)
106            .unwrap_or(0);
107
108        let your_life_str = if your_life > 10 {
109            format!("{}", your_life).green()
110        } else if your_life > 5 {
111            format!("{}", your_life).yellow()
112        } else {
113            format!("{}", your_life).red()
114        };
115
116        let opponent_life_str = if opponent_life > 10 {
117            format!("{}", opponent_life).green()
118        } else if opponent_life > 5 {
119            format!("{}", opponent_life).yellow()
120        } else {
121            format!("{}", opponent_life).red()
122        };
123
124        output.push_str(&format!("Your Life: {} ♥  |  Opponent Life: {} ♥", your_life_str, opponent_life_str));
125        output.push('\n');
126
127        output
128    }
129
130    fn render_zone(
131        &self,
132        state: &GameState,
133        cards: &CardRegistry,
134        player: PlayerId,
135        zone_name: &str,
136        display_name: &str,
137        hide_cards: bool,
138    ) -> String {
139        let zone_id_str = format!("{}@{}", zone_name, player.0);
140        let zone = state.zones.iter().find(|z| z.id.0 == zone_id_str);
141
142        let mut output = String::new();
143        output.push_str(&format!("{}\n", display_name.bold().cyan()));
144
145        if let Some(zone) = zone {
146            if zone.cards.is_empty() {
147                output.push_str("  (empty)\n");
148            } else {
149                for (idx, card_id) in zone.cards.iter().enumerate() {
150                    if hide_cards {
151                        output.push_str(&format!("  [{}] Mystery Card\n", idx + 1));
152                    } else if let Some(card_def) = cards.get(&card_id.0) {
153                        let card_str = format!("[{}] {} ({})", idx + 1, card_def.name, card_def.card_type);
154                        output.push_str(&format!("  {}\n", card_str.yellow()));
155                    } else {
156                        output.push_str(&format!("  [{}] Card #{}\n", idx + 1, card_id.0));
157                    }
158                }
159            }
160        } else {
161            output.push_str("  (zone not found)\n");
162        }
163
164        output
165    }
166
167    fn render_hand(
168        &self,
169        state: &GameState,
170        cards: &CardRegistry,
171        player: PlayerId,
172    ) -> String {
173        let hand_id_str = format!("hand@{}", player.0);
174        let hand_zone = state.zones.iter().find(|z| z.id.0 == hand_id_str);
175
176        let mut output = String::new();
177        output.push_str(&format!("{}\n", "Your Hand".bold().cyan()));
178
179        if let Some(hand) = hand_zone {
180            if hand.cards.is_empty() {
181                output.push_str("  (empty)\n");
182            } else {
183                for (idx, card_id) in hand.cards.iter().enumerate() {
184                    if let Some(card_def) = cards.get(&card_id.0) {
185                        let cost_str = card_def.cost.as_deref().unwrap_or("—");
186                        let card_str = format!(
187                            "[{}] {} ({}) [{}]",
188                            idx + 1, card_def.name, card_def.card_type, cost_str
189                        );
190                        output.push_str(&format!("  {}\n", card_str.yellow()));
191                    } else {
192                        output.push_str(&format!("  [{}] Card #{}\n", idx + 1, card_id.0));
193                    }
194                }
195            }
196        }
197
198        output
199    }
200
201    fn render_stack(&self, state: &GameState) -> String {
202        let mut output = String::new();
203        output.push_str(&format!("{}\n", "Stack".bold().cyan()));
204
205        if state.stack.is_empty() {
206            output.push_str("  (empty)\n");
207        } else {
208            for (idx, item) in state.stack.iter().enumerate() {
209                let source_str = item.source
210                    .map(|id| format!("Card #{}", id.0))
211                    .unwrap_or_else(|| "Unknown".to_string());
212                
213                let effect_str = match &item.effect {
214                    crate::model::command::EffectRef::Builtin(name) => name.to_string(),
215                    crate::model::command::EffectRef::Scripted(name) => name.clone(),
216                };
217                
218                output.push_str(&format!(
219                    "  [{}] {} (source: {}, controller: {:?})\n",
220                    idx + 1, effect_str, source_str, item.controller
221                ));
222            }
223        }
224
225        output
226    }
227
228    /// Render game log (last N entries)
229    pub fn render_log(&self, limit: usize) -> String {
230        let mut output = String::new();
231        output.push_str(&format!("{}\n", "Game Log".bold().cyan()));
232
233        let start = if self.game_log.len() > limit {
234            self.game_log.len() - limit
235        } else {
236            0
237        };
238
239        for entry in &self.game_log[start..] {
240            let header = format!(
241                "[Turn {}, {} - {}]",
242                entry.turn, entry.phase, entry.step
243            ).dimmed();
244            output.push_str(&format!("{} {}\n", header, entry.message));
245        }
246
247        output
248    }
249
250    /// Show the main menu
251    pub fn render_menu(&self, is_active_player: bool, is_priority_player: bool) -> String {
252        let mut output = String::new();
253        output.push_str(&format!("{}\n", "Available Actions".bold().green()));
254
255        if is_active_player && is_priority_player {
256            output.push_str("  [1] Play card from hand\n");
257            output.push_str("  [2] View hand (detailed)\n");
258            output.push_str("  [3] View your field\n");
259            output.push_str("  [4] View opponent's field\n");
260            output.push_str("  [5] View game log\n");
261            output.push_str("  [6] Pass priority\n");
262            output.push_str("  [7] Concede\n");
263        } else if is_priority_player {
264            output.push_str("  [2] View hand (detailed)\n");
265            output.push_str("  [3] View your field\n");
266            output.push_str("  [4] View opponent's field\n");
267            output.push_str("  [5] View game log\n");
268            output.push_str("  [6] Pass priority\n");
269            output.push_str("  [7] Concede\n");
270        } else {
271            output.push_str("  [2] View hand (detailed)\n");
272            output.push_str("  [3] View your field\n");
273            output.push_str("  [4] View opponent's field\n");
274            output.push_str("  [5] View game log\n");
275            output.push_str("  [7] Concede\n");
276            output.push_str("\n  Waiting for opponent's priority...\n");
277        }
278
279        output
280    }
281
282    /// Render a single card in detail
283    pub fn render_card_detail(&self, cards: &CardRegistry, card_id: CardId) -> String {
284        let mut output = String::new();
285
286        if let Some(card_def) = cards.get(&card_id.0) {
287            output.push_str(&format!("{}", "┌─────────────────────────┐\n".bright_black()));
288            output.push_str(&format!(
289                "{} {} [{}] {}\n",
290                "│".bright_black(),
291                card_def.name.bold().yellow(),
292                card_def.cost.as_deref().unwrap_or("—"),
293                "│".bright_black()
294            ));
295            output.push_str(&format!("{}", "├─────────────────────────┤\n".bright_black()));
296            output.push_str(&format!(
297                "{} {} {}\n",
298                "│".bright_black(),
299                card_def.card_type.cyan(),
300                "│".bright_black()
301            ));
302            output.push_str(&format!("{}", "│                         │\n".bright_black()));
303
304            if let Some(desc) = &card_def.description {
305                for line in desc.lines() {
306                    output.push_str(&format!(
307                        "{} {:<23} {}\n",
308                        "│".bright_black(),
309                        line,
310                        "│".bright_black()
311                    ));
312                }
313                output.push_str(&format!("{}", "│                         │\n".bright_black()));
314            }
315
316            for ability in &card_def.abilities {
317                let ability_text = format!(
318                    "{} ({})",
319                    ability.trigger.green(), ability.effect.magenta()
320                );
321                output.push_str(&format!(
322                    "{} {:<23} {}\n",
323                    "│".bright_black(),
324                    &ability_text.to_string()[..std::cmp::min(23, ability_text.len())],
325                    "│".bright_black()
326                ));
327            }
328
329            output.push_str(&format!("{}", "└─────────────────────────┘\n".bright_black()));
330        } else {
331            output.push_str(&format!("Card #{} not found\n", card_id.0).red().to_string());
332        }
333
334        output
335    }
336}