1use colored::Colorize;
2use crate::{
3 state::gamestate::GameState,
4 ids::{PlayerId, CardId},
5 engine::cards::CardRegistry,
6};
7
8#[derive(Debug, Clone)]
10pub struct LogEntry {
11 pub turn: u32,
12 pub phase: String,
13 pub step: String,
14 pub message: String,
15}
16
17pub 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 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 output.push_str(&self.render_header(state, viewer));
49 output.push('\n');
50
51 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 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 output.push_str(&self.render_stack(state));
76 output.push('\n');
77
78 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 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 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 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 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 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}