Compare commits

..

3 Commits

Author SHA1 Message Date
ee895d81eb Implemented the card moving
Need to actually check it's working though
2025-03-09 23:21:20 +00:00
b3b9fb61a5 Removed a useless Enum and started the moving to piles 2025-03-09 22:19:46 +00:00
e86c705b5b Added a help popup 2025-03-09 00:54:34 +00:00
2 changed files with 198 additions and 123 deletions

View File

@@ -148,8 +148,6 @@ impl Card {
} }
pub fn can_be_placed_on_foundation(&self, top: &Option<Card>) -> Result<(), StackingError> { pub fn can_be_placed_on_foundation(&self, top: &Option<Card>) -> Result<(), StackingError> {
// TODO check suit is correct
println!("hello1");
match top { match top {
None => { None => {
if self.value == Value::Ace { if self.value == Value::Ace {
@@ -160,7 +158,6 @@ impl Card {
}, },
Some(c) => { Some(c) => {
if self.suit != c.suit { if self.suit != c.suit {
println!("hellosuitsame");
return Err(StackingError::WrongSuit); return Err(StackingError::WrongSuit);
} }
if self.value.indexed_values() + 1 == c.value.indexed_values() { if self.value.indexed_values() + 1 == c.value.indexed_values() {
@@ -223,7 +220,6 @@ pub struct CardAndPosition {
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum CardPosition { pub enum CardPosition {
Deck, // I don't think this will need to be used
TopWaste, // I don't think we'd ever interact with anything other than the top of the Waste TopWaste, // I don't think we'd ever interact with anything other than the top of the Waste
Pile(usize, usize), // (PileNumber, Index) Pile(usize, usize), // (PileNumber, Index)
Foundation(usize) Foundation(usize)
@@ -280,7 +276,6 @@ impl Klondike {
// TODO raise errors properly // TODO raise errors properly
assert!(source_card.card.is_some()); assert!(source_card.card.is_some());
assert_eq!(source_card.card.unwrap().visible, true); assert_eq!(source_card.card.unwrap().visible, true);
source_card.pos.is_valid_source();
dest_card.pos.is_valid_dest(); dest_card.pos.is_valid_dest();
// Maybe TODO - check the .cards is the actual card in that position // Maybe TODO - check the .cards is the actual card in that position
@@ -288,12 +283,13 @@ impl Klondike {
match dest_card.pos { match dest_card.pos {
CardPosition::Pile(_, _) => self.move_card_to_pile(&source_card, &dest_card), CardPosition::Pile(_, _) => self.move_card_to_pile(&source_card, &dest_card),
CardPosition::Foundation(_f) => self.move_card_to_foundation(source_card, dest_card), CardPosition::Foundation(_f) => self.move_card_to_foundation(source_card, dest_card),
CardPosition::Deck | CardPosition::TopWaste => unreachable!() CardPosition::TopWaste => unreachable!()
} }
} }
pub fn move_card_to_foundation(mut self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool { pub fn move_card_to_foundation(mut self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool {
// TODO Check whether the card is the top of a pile / waste - it needs to be // TODO check the cards referenced in source and dest are the ones that are actually there
// TODO - ditto this for the "move to pile" function
// TODO actually learn Rust properly so I can figure out why I need to clone the whole struct to check a value // TODO actually learn Rust properly so I can figure out why I need to clone the whole struct to check a value
if source_card.pos != CardPosition::TopWaste && !self.clone().is_card_top_of_pile(&source_card.pos) { if source_card.pos != CardPosition::TopWaste && !self.clone().is_card_top_of_pile(&source_card.pos) {
// TODO as above - make all these proper errors // TODO as above - make all these proper errors
@@ -302,8 +298,7 @@ impl Klondike {
if source_card.card.unwrap().can_be_placed_on_foundation(&dest_card.card).is_err() { if source_card.card.unwrap().can_be_placed_on_foundation(&dest_card.card).is_err() {
return false; return false;
} }
// TODO actually move the cards - it should be possible from here
// There really must be a better way to extract an enum "value" than this...
if let CardPosition::Foundation(foundation_index) = dest_card.pos { if let CardPosition::Foundation(foundation_index) = dest_card.pos {
match source_card.pos { match source_card.pos {
CardPosition::TopWaste => { CardPosition::TopWaste => {
@@ -316,7 +311,7 @@ impl Klondike {
self.foundation[foundation_index].push(card); self.foundation[foundation_index].push(card);
return true; return true;
}, },
CardPosition::Deck | CardPosition::Foundation(_) => { CardPosition::Foundation(_) => {
unreachable!() unreachable!()
}, },
} }
@@ -324,7 +319,42 @@ impl Klondike {
unreachable!(); unreachable!();
} }
pub fn move_card_to_pile(self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool { pub fn move_card_to_pile(mut self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool {
if source_card.card.unwrap().can_be_placed_on_pile(&dest_card.card.unwrap()).is_err() {
return false;
}
let (dpile_index, dcard_index) = match dest_card.pos {
CardPosition::Pile(p, i) => (p, i),
CardPosition::TopWaste | CardPosition::Foundation(_) => return false
};
if dcard_index != self.piles[dpile_index].len() - 1 {
// Can't move to anything other than top of pile
// this should already have been checked in the is_card_top...
// maybe I'll just delete this check...
return false;
}
match source_card.pos {
CardPosition::TopWaste => {
let card = self.waste.pop().unwrap();
self.piles[dpile_index].push(card);
},
CardPosition::Pile(spile_index, scard_index) => {
let num_cards_to_take = self.piles[spile_index].len() - scard_index; // -1 maybe?
let mut cards: Vec<Card> = Vec::new();
for _ in 0..num_cards_to_take {
cards.push(self.piles[spile_index].pop().unwrap());
}
for card in cards {
self.piles[dpile_index].push(card);
}
// TODO Properly learn rust and why I can't use drain & extend methods
// https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain
},
CardPosition::Foundation(s_index) => {
let card = self.foundation[s_index].pop().unwrap();
self.piles[dpile_index].push(card);
},
}
true true
} }
@@ -334,7 +364,6 @@ impl Klondike {
CardPosition::Pile(pile_index, card_index) => { CardPosition::Pile(pile_index, card_index) => {
match self.piles.get(*pile_index) { match self.piles.get(*pile_index) {
Some(pile_index) => { Some(pile_index) => {
// TODO - this is the correct palce to put a - 1
if *card_index == (pile_index.len() - 1) { if *card_index == (pile_index.len() - 1) {
true true
} else { } else {
@@ -344,27 +373,19 @@ impl Klondike {
None => false None => false
} }
}, },
CardPosition::Deck | CardPosition::TopWaste | CardPosition::Foundation(_) => false CardPosition::TopWaste | CardPosition::Foundation(_) => false
} }
} }
} }
impl CardPosition { impl CardPosition {
// Unsure this is "correct" to just panic - but it really shouldn't happen
fn is_valid_dest(&self) { fn is_valid_dest(&self) {
// TODO as with many other places - should probably raise an error instead of panic
match self { match self {
CardPosition::Deck => panic!("You can't move cards to deck"),
CardPosition::TopWaste => panic!("You can't move cards to waste"), CardPosition::TopWaste => panic!("You can't move cards to waste"),
CardPosition::Pile(_, _) | CardPosition::Foundation(_) => (), CardPosition::Pile(_, _) | CardPosition::Foundation(_) => (),
} }
} }
fn is_valid_source(&self) {
match self {
CardPosition::Deck => panic!("You can't move cards from deck"),
CardPosition::TopWaste | CardPosition::Pile(_, _) | CardPosition::Foundation(_) => (),
}
}
} }
impl Default for Klondike { impl Default for Klondike {
@@ -626,8 +647,53 @@ mod tests {
} }
#[test] #[test]
fn klondike() { fn move_pile_card_to_good_pile() {
let k = Klondike::default(); let mut klon = Klondike::default();
//println!("{:#?}", k); let ace = Card {
suit: Suit::Heart,
value: Value::Ace,
.. Default::default()
};
let two = Card {
suit: Suit::Spade,
value: Value::Two,
.. Default::default()
};
klon.piles[0].push(two.clone());
klon.piles[1].push(ace.clone());
let source_card = CardAndPosition {
card: Some(ace.clone()),
pos: CardPosition::Pile(1, 2),
};
let dest_card = CardAndPosition {
card: Some(two.clone()),
pos: CardPosition::Pile(0, 1),
};
assert!(klon.move_card_to_pile(&source_card, &dest_card));
}
#[test]
fn move_pile_card_to_bad_pile() {
let mut klon = Klondike::default();
let ace = Card {
suit: Suit::Heart,
value: Value::Ace,
.. Default::default()
};
let two = Card {
suit: Suit::Diamond,
value: Value::Two,
.. Default::default()
};
klon.piles[0].push(two.clone());
klon.piles[1].push(ace.clone());
let source_card = CardAndPosition {
card: Some(ace.clone()),
pos: CardPosition::Pile(1, 2),
};
let dest_card = CardAndPosition {
card: Some(two.clone()),
pos: CardPosition::Pile(0, 1),
};
assert!(!klon.move_card_to_pile(&source_card, &dest_card));
} }
} }

View File

@@ -6,7 +6,7 @@ use ratatui::{
layout::{Constraint, Layout, Rect, Flex}, layout::{Constraint, Layout, Rect, Flex},
style::{Style, Stylize, Color}, style::{Style, Stylize, Color},
text::Line, text::Line,
widgets::{Block, BorderType, Borders, Paragraph}, widgets::{Block, BorderType, Borders, Paragraph, Clear, Wrap},
DefaultTerminal, Frame, DefaultTerminal, Frame,
}; };
@@ -18,6 +18,7 @@ pub struct App {
// I should think about making this a Vec so I can highlight a whole stack which is about to move // 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::Card>, selected_card: Option<card_stuffs::Card>,
exit: bool, exit: bool,
show_help: bool,
} }
const CARD_HEIGHT: u16 = 11; const CARD_HEIGHT: u16 = 11;
@@ -69,26 +70,6 @@ fn draw_waste(cards_in_waste: &Vec<card_stuffs::Card>, area: Rect, frame: &mut F
} }
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 card_widget<'a>(card: &'a card_stuffs::Card, top: bool, highlight: bool, select: bool) -> Paragraph<'a> { fn card_widget<'a>(card: &'a card_stuffs::Card, top: bool, highlight: bool, select: bool) -> Paragraph<'a> {
if !card.visible { if !card.visible {
return facedown_card(top); return facedown_card(top);
@@ -121,80 +102,6 @@ fn card_widget<'a>(card: &'a card_stuffs::Card, top: bool, highlight: bool, sele
.border_type(BorderType::Rounded)) .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))
}
/*
struct CardWidget {
card: card_stuffs::Card,
// put display stuff in here?
}
impl Widget for CardWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut border_style = Style::default();
}
}
*/
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() -> Paragraph<'static> {
// made using https://www.asciiart.eu/
let hidden_card = format!(
"
XX XX
XX XX
X X
X X
X X
XXXXXXX"
);
Paragraph::new(hidden_card)
.block(Block::new()
.borders(Borders::ALL)
.border_type(BorderType::Rounded))
}
impl App { impl App {
/// runs the application's main loop until the user quits /// runs the application's main loop until the user quits
@@ -207,12 +114,13 @@ impl App {
} }
fn draw(&self, frame: &mut Frame) { fn draw(&self, frame: &mut Frame) {
let area = frame.area();
let vertical = Layout::vertical([ let vertical = Layout::vertical([
Constraint::Length(1), Constraint::Length(1),
Constraint::Min(0), Constraint::Min(0),
Constraint::Length(1), Constraint::Length(1),
]); ]);
let [title_bar, main_area, status_bar] = vertical.areas(frame.area()); let [title_bar, main_area, status_bar] = vertical.areas(area);
frame.render_widget( frame.render_widget(
Block::new() Block::new()
@@ -222,7 +130,7 @@ impl App {
title_bar title_bar
); );
let status_bar_info = format!("Cards Per-Draw: {} ---- Times Through Deck: {} ", self.cards.num_cards_turned, self.cards.current_num_passes_through_deck); 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( frame.render_widget(
Block::new().borders(Borders::TOP).title(status_bar_info), Block::new().borders(Borders::TOP).title(status_bar_info),
status_bar status_bar
@@ -301,7 +209,6 @@ impl App {
let selected = self.selected_card.is_some() && *c == self.selected_card.unwrap(); let selected = self.selected_card.is_some() && *c == self.selected_card.unwrap();
let a_card = match c.visible { let a_card = match c.visible {
true => card_widget(c, is_top_card, highlight, selected), true => card_widget(c, is_top_card, highlight, selected),
//false => turned_over_card(is_top_card),
false => card_widget(c, is_top_card, highlight, selected), false => card_widget(c, is_top_card, highlight, selected),
}; };
frame.render_widget( frame.render_widget(
@@ -313,6 +220,10 @@ impl App {
} }
} }
} }
if self.show_help {
show_help(frame, &area);
}
} }
fn handle_events(&mut self) -> io::Result<()> { fn handle_events(&mut self) -> io::Result<()> {
@@ -334,6 +245,7 @@ impl App {
KeyCode::Char('w') => self.cards.waste_to_deck(), KeyCode::Char('w') => self.cards.waste_to_deck(),
KeyCode::Char('1') => self.cards.num_cards_turned = 1, KeyCode::Char('1') => self.cards.num_cards_turned = 1,
KeyCode::Char('3') => self.cards.num_cards_turned = 3, KeyCode::Char('3') => self.cards.num_cards_turned = 3,
KeyCode::Char('h') => self.show_help = !self.show_help, // toggle
_ => {} _ => {}
} }
} }
@@ -350,6 +262,103 @@ fn main() -> io::Result<()> {
app_result app_result
} }
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 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() -> Paragraph<'static> {
// made using https://www.asciiart.eu/
let hidden_card = format!(
"
XX XX
XX XX
X X
X X
X X
XXXXXXX"
);
Paragraph::new(hidden_card)
.block(Block::new()
.borders(Borders::ALL)
.border_type(BorderType::Rounded))
}
fn card_paragraph(c: &card_stuffs::Card) -> String { fn card_paragraph(c: &card_stuffs::Card) -> String {