use card_stuffs::{self, CardPosition}; use core::panic; use std::io; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; use ratatui::{ layout::{Constraint, Flex, Layout, Rect}, style::{Color, Style, Stylize}, text::Line, widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap}, DefaultTerminal, Frame, }; #[derive(Debug)] pub struct App { cards: card_stuffs::Klondike, // There aren't pretty... not sure what else I can do about that though... highlighted_card: card_stuffs::CardPosition, // I should think about making this a Vec so I can highlight a whole stack which is about to move selected_card: Option, exit: bool, show_help: bool, show_exit: bool, } impl Default for App { fn default() -> Self { Self { cards: card_stuffs::Klondike::default(), highlighted_card: card_stuffs::CardPosition::TopWaste, selected_card: None, exit: false, show_exit: false, show_help: false, } } } const CARD_HEIGHT: u16 = 11; const CARD_WIDTH: u16 = 15; fn draw_waste( cards_in_waste: &Vec, area: Rect, frame: &mut Frame, highlight: bool, selected: bool, ) { let horizontal = Layout::horizontal([ Constraint::Length(3), Constraint::Length(3), Constraint::Length(CARD_WIDTH), ]); let [w1, w2, top_waste] = horizontal.areas(area); // There must be a better way to do all of this if cards_in_waste.is_empty() { frame.render_widget(empty_pile(highlight, selected), top_waste); } if cards_in_waste.len() >= 3 { frame.render_widget( partially_covered_card(&cards_in_waste[cards_in_waste.len() - 3]), w1, ); frame.render_widget( partially_covered_card(&cards_in_waste[cards_in_waste.len() - 2]), w2, ); frame.render_widget( card_widget( &cards_in_waste[cards_in_waste.len() - 1], true, highlight, false, ), top_waste, ); } else if cards_in_waste.len() == 2 { frame.render_widget( partially_covered_card(&cards_in_waste[cards_in_waste.len() - 2]), w2, ); frame.render_widget( card_widget( &cards_in_waste[cards_in_waste.len() - 1], true, highlight, false, ), top_waste, ); } else if cards_in_waste.len() == 1 { frame.render_widget( card_widget( &cards_in_waste[cards_in_waste.len() - 1], true, highlight, false, ), top_waste, ); } } impl App { /// runs the application's main loop until the user quits pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { while !self.exit { terminal.draw(|frame| self.draw(frame))?; self.handle_events()?; } Ok(()) } fn draw(&self, frame: &mut Frame) { let area = frame.area(); let vertical = Layout::vertical([ Constraint::Length(1), Constraint::Min(0), Constraint::Length(1), ]); let [title_bar, main_area, status_bar] = vertical.areas(area); frame.render_widget( Block::new() .borders(Borders::TOP) .border_type(BorderType::Thick) .title(Line::from("Legends of Soltar").bold()), title_bar, ); let status_bar_info = format!( "Cards Per-Draw: {} ---- Times Through Deck: {} ---- Press 'h' for Help ", self.cards.num_cards_turned, self.cards.current_num_passes_through_deck ); frame.render_widget( Block::new().borders(Borders::TOP).title(status_bar_info), status_bar, ); let vertical = Layout::vertical([Constraint::Length(CARD_HEIGHT), Constraint::Min(0)]); let [dwf_area, piles_area] = vertical.areas(main_area); let horizontal = Layout::horizontal([ Constraint::Length(CARD_WIDTH), Constraint::Length(3 + 3 + CARD_WIDTH), // for 2 cards shown underneath Constraint::Length(CARD_WIDTH), Constraint::Length(CARD_WIDTH), Constraint::Length(CARD_WIDTH), Constraint::Length(CARD_WIDTH), ]) .flex(Flex::SpaceAround); let [deck_area, waste_area, fa, fb, fc, fd] = horizontal.areas(dwf_area); let foundation_areas = [fa, fb, fc, fd]; frame.render_widget(deck_widget(&self.cards.deck), deck_area); { let highlight = self.highlighted_card == CardPosition::TopWaste; let selected = { match self.selected_card { None => false, Some(pos) => pos == CardPosition::TopWaste, } }; draw_waste(&self.cards.waste, waste_area, frame, highlight, selected); } for (i, fa) in foundation_areas.iter().enumerate() { let highlight = self.highlighted_card == CardPosition::Foundation(i); let selected = { match self.selected_card { None => false, Some(pos) => pos == CardPosition::Foundation(i), } }; frame.render_widget(empty_pile(highlight, selected), *fa); } let horizontal = Layout::horizontal([ Constraint::Length(CARD_WIDTH), Constraint::Length(CARD_WIDTH), Constraint::Length(CARD_WIDTH), Constraint::Length(CARD_WIDTH), Constraint::Length(CARD_WIDTH), Constraint::Length(CARD_WIDTH), Constraint::Length(CARD_WIDTH), ]) .flex(Flex::SpaceAround); let pileses: [Rect; 7] = horizontal.areas(piles_area); for pile in 0..card_stuffs::NUM_PILES_KLONDIKE { let mut constraints = Vec::new(); for card in 0..(card_stuffs::NUM_PILES_KLONDIKE + 13) { match self.cards.piles[pile].get(card) { Some(_) => { if card == self.cards.piles[pile].len() - 1 { constraints.push(Constraint::Length(CARD_HEIGHT)); } else { constraints.push(Constraint::Length(2)); } } None => { constraints.push(Constraint::Length(0)); } } } let vertical = Layout::vertical(constraints); let card_display: [Rect; card_stuffs::NUM_PILES_KLONDIKE + 13] = vertical.areas(pileses[pile]); for (i, card) in card_display.iter().enumerate() { match self.cards.piles[pile].get(i) { None => break, Some(c) => { let is_top_card = i == self.cards.piles[pile].len() - 1; let highlight = self.highlighted_card == CardPosition::Pile(pile, i); let selected = { match self.selected_card { None => false, Some(pos) => pos == CardPosition::Pile(pile, i), } }; let a_card = match c.visible { true => card_widget(c, is_top_card, highlight, selected), false => card_widget(c, is_top_card, highlight, selected), }; frame.render_widget(&a_card, *card) } } } } if self.show_help { show_help(frame, &area); } else if self.show_exit { show_exit(frame, &area); } } fn handle_events(&mut self) -> io::Result<()> { match event::read()? { // it's important to check that the event is a key press event as // crossterm also emits key release and repeat events on Windows. Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { self.handle_key_event(key_event) } _ => {} }; Ok(()) } fn handle_key_event(&mut self, key_event: KeyEvent) { match key_event.code { KeyCode::Char('q') => { if self.show_exit { self.exit() } self.show_exit = true; } KeyCode::Char('b') => self.show_exit = false, KeyCode::Char('d') => self.cards.deck_to_waste(), KeyCode::Char('w') => self.cards.waste_to_deck(), KeyCode::Char('1') => self.cards.num_cards_turned = 1, KeyCode::Char('3') => self.cards.num_cards_turned = 3, KeyCode::Char('h') => self.show_help = !self.show_help, // toggle KeyCode::Char(' ') => self.select_card(), KeyCode::Left => { self.highlighted_card = handle_move_highlighted(&self.highlighted_card, Direction::Left, &self.cards) } KeyCode::Right => { self.highlighted_card = handle_move_highlighted(&self.highlighted_card, Direction::Right, &self.cards) } KeyCode::Up => { self.highlighted_card = handle_move_highlighted(&self.highlighted_card, Direction::Up, &self.cards) } KeyCode::Down => { self.highlighted_card = handle_move_highlighted(&self.highlighted_card, Direction::Down, &self.cards) } _ => {} } } fn exit(&mut self) { self.exit = true; } fn select_card(&mut self) { self.selected_card = Some(self.highlighted_card); // FIXME - actually moving the selected card to next position // FIXME - don't allow selection of empty pile/foundation } } fn main() -> io::Result<()> { let mut terminal = ratatui::init(); let app_result = App::default().run(&mut terminal); ratatui::restore(); app_result } enum Direction { Up, Down, Left, Right, } fn handle_move_highlighted( current_position: &CardPosition, direction: Direction, cards: &card_stuffs::Klondike, ) -> CardPosition { match current_position { CardPosition::TopWaste => match direction { Direction::Up | Direction::Left => CardPosition::TopWaste, Direction::Right => CardPosition::Foundation(0), Direction::Down => { let lowest_shown_card = cards.clone().lowest_visible_card_in_pile_from_index(1, 0); CardPosition::Pile(1, lowest_shown_card) } }, CardPosition::Pile(p, i) => { match direction { Direction::Down => { if *i == cards.piles[*p].len() - 1 { CardPosition::Pile(*p, *i) } else { CardPosition::Pile(*p, *i + 1) } } Direction::Up => { let lowest_shown_card = cards.clone().lowest_visible_card_in_pile_from_index(*p, 0); if *i == lowest_shown_card { match *p { 0 | 1 => CardPosition::TopWaste, 2 | 3 => CardPosition::Foundation(0), 4 => CardPosition::Foundation(1), 5 => CardPosition::Foundation(2), 6 => CardPosition::Foundation(3), _ => panic!("Should be on Pile over 6"), } } else { CardPosition::Pile(*p, *i - 1) } } Direction::Left => { if *p == 0 { CardPosition::Pile(*p, *i) } else { let lowest_shown_card = cards .clone() .lowest_visible_card_in_pile_from_index(*p - 1, 0); if lowest_shown_card <= *i && cards.piles[*p].len() <= *i { CardPosition::Pile(*p - 1, *i) // CHECK } else { CardPosition::Pile(*p - 1, lowest_shown_card) } } } Direction::Right => { if *p == 6 { CardPosition::Pile(*p, *i) } else { let lowest_shown_card = cards .clone() .lowest_visible_card_in_pile_from_index(*p + 1, 0); if lowest_shown_card <= *i && cards.piles[*p].len() <= *i { CardPosition::Pile(*p + 1, *i) // CHECK } else { CardPosition::Pile(*p + 1, lowest_shown_card) } } } } } CardPosition::Foundation(f) => match direction { Direction::Up => CardPosition::Foundation(*f), Direction::Left => { if *f == 0 { CardPosition::TopWaste } else { CardPosition::Foundation(f - 1) } } Direction::Right => { if *f >= 3 { CardPosition::Foundation(3) } else { CardPosition::Foundation(*f + 1) } } Direction::Down => match f { 0 => { let i = cards.clone().lowest_visible_card_in_pile_from_index(3, 0); CardPosition::Pile(3, i) } 1 => { let i = cards.clone().lowest_visible_card_in_pile_from_index(4, 0); CardPosition::Pile(4, i) } 2 => { let i = cards.clone().lowest_visible_card_in_pile_from_index(5, 0); CardPosition::Pile(5, i) } 3 => { let i = cards.clone().lowest_visible_card_in_pile_from_index(6, 0); CardPosition::Pile(6, i) } _ => panic!("Can't be on a foundation this high"), }, }, } } fn show_help(frame: &mut Frame, area: &Rect) { let block = Block::bordered().title("Help"); let text = "You are playing \"Legends of Soltar\" - a Klondike thingy Press 'q' to Quit Press '1' or '3' to change the number of cards you draw from the deck Press 'd' to draw from your deck Press 'w' to put the waste pile back into the deck (you can only do this when the waste is empty)"; let p = Paragraph::new(text).wrap(Wrap { trim: true }); let vertical = Layout::vertical([Constraint::Max(10)]).flex(Flex::Center); let horizontal = Layout::horizontal([Constraint::Percentage(70)]).flex(Flex::Center); let [area] = vertical.areas(*area); let [area] = horizontal.areas(area); frame.render_widget(Clear, area); frame.render_widget(p.block(block), area); } fn show_exit(frame: &mut Frame, area: &Rect) { let block = Block::bordered().title("Exit?"); let text = "Really want to exit Legend of Soltar? Press 'q' to Quit or 'b' to go back"; let p = Paragraph::new(text).wrap(Wrap { trim: true }); let vertical = Layout::vertical([Constraint::Max(10)]).flex(Flex::Center); let horizontal = Layout::horizontal([Constraint::Percentage(70)]).flex(Flex::Center); let [area] = vertical.areas(*area); let [area] = horizontal.areas(area); frame.render_widget(Clear, area); frame.render_widget(p.block(block), area); } fn card_widget<'a>( card: &'a card_stuffs::Card, top: bool, highlight: bool, select: bool, ) -> Paragraph<'a> { if !card.visible { return facedown_card(top); } let card_image = card_paragraph(&card); let card_style = match card.suit.colour() { card_stuffs::Colour::Black => Style::new().black().bg(Color::White), card_stuffs::Colour::Red => Style::new().red().bg(Color::White), }; let mut borders = Borders::TOP | Borders::LEFT | Borders::RIGHT; if top { borders |= Borders::BOTTOM; } let mut border_style = Style::new().white().on_black(); if highlight { border_style = border_style.fg(Color::Blue); } else if select { border_style = border_style.fg(Color::Green); } Paragraph::new(card_image).style(card_style).block( Block::new() .style(border_style) .borders(borders) .border_type(BorderType::Rounded), ) } fn deck_widget(cards_in_deck: &Vec) -> Paragraph<'static> { let card_image = format!( "#############\n\ #############\n\ ### Cards ###\n\ ### Left ###\n\ #############\n\ #### {:02} #####\n\ #############\n\ #############\n\ #############", cards_in_deck.len() ); Paragraph::new(card_image).block( Block::new() .borders(Borders::ALL) .border_type(BorderType::Rounded), ) } fn partially_covered_card(card: &card_stuffs::Card) -> Paragraph { let card_image = format!("{value}{suit}", value = card.value, suit = card.suit); let card_style = match card.suit.colour() { card_stuffs::Colour::Black => Style::new().black().bg(Color::White), card_stuffs::Colour::Red => Style::new().red().bg(Color::White), }; let borders = Borders::TOP | Borders::LEFT | Borders::BOTTOM; let border_style = Style::new().white().on_black(); Paragraph::new(card_image).style(card_style).block( Block::new() .style(border_style) .borders(borders) .border_type(BorderType::Rounded), ) } fn facedown_card(top: bool) -> Paragraph<'static> { let hidden_card = format!( "#############\n\ #############\n\ #############\n\ #############\n\ #############\n\ #############\n\ #############\n\ #############\n\ #############" ); let mut borders = Borders::TOP | Borders::LEFT | Borders::RIGHT; if top { borders |= Borders::BOTTOM; } Paragraph::new(hidden_card).block( Block::new() .borders(borders) .border_type(BorderType::Rounded), ) } fn empty_pile(highlight: bool, selected: bool) -> Paragraph<'static> { // made using https://www.asciiart.eu/ let hidden_card = format!( " XX XX XX XX X X X X X X XXXXXXX" ); let mut border_style = Style::new(); if highlight { border_style = border_style.fg(Color::Blue); } else if selected { border_style = border_style.fg(Color::Green); } Paragraph::new(hidden_card).block( Block::new() .style(border_style) .borders(Borders::ALL) .border_type(BorderType::Rounded), ) } fn card_paragraph(c: &card_stuffs::Card) -> String { match c.value { card_stuffs::Value::Ace => { /* XX X X X X X X X XXXXXXX X X X X XX */ format!( "{value}{suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit}{suit}{suit}{suit}{suit}{suit}{suit} {suit} {suit} {suit} {suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::Two => { /* XX X XXX X X XXX X XX */ format!( "{value}{suit} {suit} {suit}{suit}{suit} {suit} {suit} {suit}{suit}{suit} {suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::Three => { /* XX X XXX X X XXX X X XXX X XX */ format!( "{value}{suit} {suit} {suit}{suit}{suit} {suit} {suit} {suit}{suit}{suit} {suit} {suit} {suit}{suit}{suit} {suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::Four => { format!( "{value}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::Five => { format!( "{value}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::Six => { /* XX XX XX XX XX XX XX XX */ format!( "{value}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::Seven => { /* XX XX XX XX XX XX XX XX XX */ format!( "{value}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::Eight => { /* XX X X X X X X X X XX */ format!( "{value}{suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::Nine => { /* XX X X X X X X X X X XX */ format!( "{value}{suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::Ten => { /* XX X X X X X X X X X X XX */ format!( "{value}{suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::Jack => { /* XX XXXXX X X X X XX XX */ format!( "{value}{suit} {suit}{suit}{suit}{suit}{suit} {suit} {suit} {suit} {suit} {suit}{suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::Queen => { /* XX XXXX X X X X X X X XX XXXXX X XX */ format!( "{value}{suit} {suit}{suit}{suit}{suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit} {suit}{suit}{suit}{suit}{suit} {suit} {value}{suit}", value = c.value, suit = c.suit ) } card_stuffs::Value::King => { /* XX X X X XX X XX XX X XX X XX X X XX */ format!( "{value}{suit} {suit} {suit} {suit} {suit}{suit} {suit} {suit}{suit} {suit}{suit} {suit} {suit}{suit} {suit} {suit}{suit} {suit} {suit} {value}{suit}", value = c.value, suit = c.suit ) } } }