Files
mini_projects/card_stuffs/src/main.rs

788 lines
22 KiB
Rust

use core::panic;
use std::io;
use card_stuffs::{self, CardPosition};
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use ratatui::{
layout::{Constraint, Layout, Rect, Flex},
style::{Style, Stylize, Color},
text::Line,
widgets::{Block, BorderType, Borders, Paragraph, Clear, 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<card_stuffs::CardPosition>,
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<card_stuffs::Card>, area: Rect, frame: &mut Frame, highlight: 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.len() == 0 {
frame.render_widget(
empty_pile(highlight),
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
);
draw_waste(&self.cards.waste, waste_area, frame, self.highlighted_card == CardPosition::TopWaste);
for (i, fa) in foundation_areas.iter().enumerate() {
frame.render_widget(
empty_pile(self.highlighted_card == CardPosition::Foundation(i)),
*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 = true; // FIXME
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::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 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 => { CardPosition:: Pile(0, 0) }
}
},
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 {
CardPosition::TopWaste // FIXME - should move to the appropriate Waste or Foundation
} 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(2, 0);
CardPosition::Pile(2, i)
},
1 => {
let i = cards.clone().lowest_visible_card_in_pile_from_index(3, 0);
CardPosition::Pile(3, i)
},
2 => {
let i = cards.clone().lowest_visible_card_in_pile_from_index(4, 0);
CardPosition::Pile(4, i)
},
3 => {
let i = cards.clone().lowest_visible_card_in_pile_from_index(5, 0);
CardPosition::Pile(5, 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<card_stuffs::Card>) -> 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) -> 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);
}
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)
},
}
}