Compare commits

...

2 Commits

Author SHA1 Message Date
906aaa1e59 Using ureq to download files
I might just throw out the TempFile thing - should actually save them somewhere. As
I think it would likely be useful for testing too
2025-08-13 20:22:49 +01:00
6b4105ecd9 Selection and Highlighting appears okay
Still need to appropriately test with piles of visible cards > 1
2025-06-28 00:44:03 +01:00
5 changed files with 616 additions and 365 deletions

View File

@@ -1,16 +1,16 @@
use thiserror::Error; use rand::rng;
use rand::seq::SliceRandom;
use std::fmt;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use strum_macros::EnumIter; use strum_macros::EnumIter;
use std::fmt; use thiserror::Error;
use rand::seq::SliceRandom;
use rand::rng;
#[derive(PartialEq, Debug, EnumIter, Copy, Clone)] #[derive(PartialEq, Debug, EnumIter, Copy, Clone)]
pub enum Suit { pub enum Suit {
Heart, Heart,
Diamond, Diamond,
Club, Club,
Spade Spade,
} }
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
@@ -53,7 +53,7 @@ pub enum Value {
Ten, Ten,
Jack, Jack,
Queen, Queen,
King King,
} }
impl Value { impl Value {
@@ -97,7 +97,6 @@ impl fmt::Display for Value {
} }
} }
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
pub struct Card { pub struct Card {
pub suit: Suit, pub suit: Suit,
@@ -105,7 +104,6 @@ pub struct Card {
pub visible: bool, pub visible: bool,
} }
impl fmt::Display for Card { impl fmt::Display for Card {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}{}", self.value, self.suit) write!(f, "{}{}", self.value, self.suit)
@@ -140,8 +138,13 @@ impl Card {
} }
// Needs to be adjacent // Needs to be adjacent
if self.value == Value::King || self.value.indexed_values() + 1 != top.value.indexed_values() { if self.value == Value::King
return Err(StackingError::NotAdjacent(self.to_string(), top.to_string())); || self.value.indexed_values() + 1 != top.value.indexed_values()
{
return Err(StackingError::NotAdjacent(
self.to_string(),
top.to_string(),
));
} }
Ok(()) Ok(())
@@ -153,9 +156,12 @@ impl Card {
if self.value == Value::Ace { if self.value == Value::Ace {
return Ok(()); return Ok(());
} else { } else {
return Err(StackingError::NotAdjacent(self.to_string(), "an empty foundation".to_string())); return Err(StackingError::NotAdjacent(
self.to_string(),
"an empty foundation".to_string(),
));
}
} }
},
Some(c) => { Some(c) => {
if self.suit != c.suit { if self.suit != c.suit {
return Err(StackingError::WrongSuit); return Err(StackingError::WrongSuit);
@@ -172,7 +178,7 @@ impl Card {
#[derive(Debug)] #[derive(Debug)]
pub struct Deck { pub struct Deck {
pub cards: Vec<Card> pub cards: Vec<Card>,
} }
impl Default for Deck { impl Default for Deck {
@@ -180,18 +186,14 @@ impl Default for Deck {
let mut array = Vec::new(); let mut array = Vec::new();
for suit in Suit::iter() { for suit in Suit::iter() {
for value in Value::iter() { for value in Value::iter() {
array.push( array.push(Card {
Card {
suit, suit,
value, value,
..Default::default() ..Default::default()
} });
);
} }
} }
Deck { Deck { cards: array }
cards: array,
}
} }
} }
@@ -222,7 +224,7 @@ pub struct CardAndPosition {
pub enum CardPosition { pub enum CardPosition {
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),
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
@@ -245,7 +247,7 @@ pub struct Klondike {
} }
impl Klondike { impl Klondike {
pub fn deck_to_waste(self: &mut Self) { pub fn deck_to_waste(&mut self) {
for _ in 0..self.num_cards_turned { for _ in 0..self.num_cards_turned {
let card = self.deck.pop(); let card = self.deck.pop();
match card { match card {
@@ -255,13 +257,15 @@ impl Klondike {
} }
} }
// TODO return some sort of error // TODO return some sort of error
pub fn waste_to_deck(self: &mut Self) { pub fn waste_to_deck(&mut self) {
match self.max_num_passes_through_deck { match self.max_num_passes_through_deck {
NumPassesThroughDeck::Unlimited => (), NumPassesThroughDeck::Unlimited => (),
NumPassesThroughDeck::Limited(n) => if n >= self.current_num_passes_through_deck { NumPassesThroughDeck::Limited(n) => {
if n >= self.current_num_passes_through_deck {
// no! // no!
return return;
}, }
}
} }
if self.deck.len() != 0 { if self.deck.len() != 0 {
// no! // no!
@@ -275,7 +279,7 @@ impl Klondike {
pub fn move_card(self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool { pub fn move_card(self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool {
// 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!(source_card.card.unwrap().visible);
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
@@ -283,19 +287,30 @@ 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::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 the cards referenced in source and dest are the ones that are actually there // 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 - 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
return false; return false;
} }
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;
} }
@@ -305,27 +320,36 @@ impl Klondike {
let card = self.waste.pop().unwrap(); let card = self.waste.pop().unwrap();
self.foundation[foundation_index].push(card); self.foundation[foundation_index].push(card);
return true; return true;
}, }
CardPosition::Pile(pile_index, _) => { CardPosition::Pile(pile_index, _) => {
let card = self.piles[pile_index].pop().unwrap(); let card = self.piles[pile_index].pop().unwrap();
self.foundation[foundation_index].push(card); self.foundation[foundation_index].push(card);
return true; return true;
}, }
CardPosition::Foundation(_) => { CardPosition::Foundation(_) => {
unreachable!() unreachable!()
}, }
} }
} }
unreachable!(); unreachable!();
} }
pub fn move_card_to_pile(mut self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool { pub fn move_card_to_pile(
if source_card.card.unwrap().can_be_placed_on_pile(&dest_card.card.unwrap()).is_err() { 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; return false;
} }
let (dpile_index, dcard_index) = match dest_card.pos { let (dpile_index, dcard_index) = match dest_card.pos {
CardPosition::Pile(p, i) => (p, i), CardPosition::Pile(p, i) => (p, i),
CardPosition::TopWaste | CardPosition::Foundation(_) => return false CardPosition::TopWaste | CardPosition::Foundation(_) => return false,
}; };
if dcard_index != self.piles[dpile_index].len() - 1 { if dcard_index != self.piles[dpile_index].len() - 1 {
// Can't move to anything other than top of pile // Can't move to anything other than top of pile
@@ -337,7 +361,7 @@ impl Klondike {
CardPosition::TopWaste => { CardPosition::TopWaste => {
let card = self.waste.pop().unwrap(); let card = self.waste.pop().unwrap();
self.piles[dpile_index].push(card); self.piles[dpile_index].push(card);
}, }
CardPosition::Pile(spile_index, scard_index) => { CardPosition::Pile(spile_index, scard_index) => {
let num_cards_to_take = self.piles[spile_index].len() - scard_index; // -1 maybe? let num_cards_to_take = self.piles[spile_index].len() - scard_index; // -1 maybe?
let mut cards: Vec<Card> = Vec::new(); let mut cards: Vec<Card> = Vec::new();
@@ -349,11 +373,11 @@ impl Klondike {
} }
// TODO Properly learn rust and why I can't use drain & extend methods // 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 // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.drain
}, }
CardPosition::Foundation(s_index) => { CardPosition::Foundation(s_index) => {
let card = self.foundation[s_index].pop().unwrap(); let card = self.foundation[s_index].pop().unwrap();
self.piles[dpile_index].push(card); self.piles[dpile_index].push(card);
}, }
} }
true true
} }
@@ -361,8 +385,7 @@ impl Klondike {
fn is_card_top_of_pile(self, pos: &CardPosition) -> bool { fn is_card_top_of_pile(self, pos: &CardPosition) -> bool {
// TODO consider, at which point the Pos::Pile() ranges etc are correct // TODO consider, at which point the Pos::Pile() ranges etc are correct
match pos { match pos {
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) => {
if *card_index == (pile_index.len() - 1) { if *card_index == (pile_index.len() - 1) {
true true
@@ -370,10 +393,9 @@ impl Klondike {
false false
} }
} }
None => false None => false,
}
}, },
CardPosition::TopWaste | CardPosition::Foundation(_) => false CardPosition::TopWaste | CardPosition::Foundation(_) => false,
} }
} }
@@ -404,9 +426,17 @@ impl Default for Klondike {
fn default() -> Self { fn default() -> Self {
let mut deck = Deck::default(); let mut deck = Deck::default();
deck.shuffle(Seed::Unseeded); deck.shuffle(Seed::Unseeded);
let mut piles: [Vec<Card>; NUM_PILES_KLONDIKE] = [Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()]; let mut piles: [Vec<Card>; NUM_PILES_KLONDIKE] = [
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
];
for pile in 0..NUM_PILES_KLONDIKE { for pile in 0..NUM_PILES_KLONDIKE {
for num in 0..pile+1 { for num in 0..pile + 1 {
let mut c = deck.cards.pop().unwrap(); let mut c = deck.cards.pop().unwrap();
if num == pile { if num == pile {
c.visible = true; c.visible = true;
@@ -445,19 +475,28 @@ mod tests {
value: Value::Six, value: Value::Six,
..Default::default() ..Default::default()
}; };
assert_eq!(testing_card.can_be_placed_on_pile(&bad_same_suit), Err(StackingError::SameColour)); assert_eq!(
testing_card.can_be_placed_on_pile(&bad_same_suit),
Err(StackingError::SameColour)
);
let bad_same_colour = Card { let bad_same_colour = Card {
suit: Suit::Diamond, suit: Suit::Diamond,
value: Value::Six, value: Value::Six,
..Default::default() ..Default::default()
}; };
assert_eq!(testing_card.can_be_placed_on_pile(&bad_same_colour), Err(StackingError::SameColour)); assert_eq!(
testing_card.can_be_placed_on_pile(&bad_same_colour),
Err(StackingError::SameColour)
);
let should_stack_card = Card { let should_stack_card = Card {
suit: Suit::Club, suit: Suit::Club,
value: Value::Six, value: Value::Six,
..Default::default() ..Default::default()
}; };
assert_eq!(testing_card.can_be_placed_on_pile(&should_stack_card), Ok(())); assert_eq!(
testing_card.can_be_placed_on_pile(&should_stack_card),
Ok(())
);
let value_too_high = Card { let value_too_high = Card {
suit: Suit::Club, suit: Suit::Club,
value: Value::Seven, value: Value::Seven,
@@ -467,7 +506,9 @@ mod tests {
if let Err(e) = not_adj_error { if let Err(e) = not_adj_error {
match e { match e {
StackingError::NotAdjacent(_, _) => assert!(true), StackingError::NotAdjacent(_, _) => assert!(true),
StackingError::SameColour => assert!(false, "Colour is different - incorrect error"), StackingError::SameColour => {
assert!(false, "Colour is different - incorrect error")
}
StackingError::WrongSuit => unreachable!(), StackingError::WrongSuit => unreachable!(),
} }
} else { } else {
@@ -498,12 +539,12 @@ mod tests {
let ace = Card { let ace = Card {
suit: Suit::Spade, suit: Suit::Spade,
value: Value::Ace, value: Value::Ace,
.. Default::default() ..Default::default()
}; };
klon.piles[0].push(ace.clone()); klon.piles[0].push(ace.clone());
let source_card = CardAndPosition { let source_card = CardAndPosition {
card: Some(ace.clone()), card: Some(ace.clone()),
pos: CardPosition::Pile(0, 1) pos: CardPosition::Pile(0, 1),
}; };
let dest_card = CardAndPosition { let dest_card = CardAndPosition {
card: None, card: None,
@@ -516,12 +557,12 @@ mod tests {
let two = Card { let two = Card {
suit: Suit::Spade, suit: Suit::Spade,
value: Value::Two, value: Value::Two,
.. Default::default() ..Default::default()
}; };
klon.piles[0].push(two.clone()); klon.piles[0].push(two.clone());
let source_card = CardAndPosition { let source_card = CardAndPosition {
card: Some(two.clone()), card: Some(two.clone()),
pos: CardPosition::Pile(0, 1) pos: CardPosition::Pile(0, 1),
}; };
let dest_card = CardAndPosition { let dest_card = CardAndPosition {
card: None, card: None,
@@ -534,7 +575,7 @@ mod tests {
let ace = Card { let ace = Card {
suit: Suit::Spade, suit: Suit::Spade,
value: Value::Ace, value: Value::Ace,
.. Default::default() ..Default::default()
}; };
klon.waste.push(ace.clone()); klon.waste.push(ace.clone());
let source_card = CardAndPosition { let source_card = CardAndPosition {
@@ -552,7 +593,7 @@ mod tests {
let two = Card { let two = Card {
suit: Suit::Spade, suit: Suit::Spade,
value: Value::Two, value: Value::Two,
.. Default::default() ..Default::default()
}; };
klon.waste.push(two.clone()); klon.waste.push(two.clone());
let source_card = CardAndPosition { let source_card = CardAndPosition {
@@ -570,12 +611,12 @@ mod tests {
let ace = Card { let ace = Card {
suit: Suit::Spade, suit: Suit::Spade,
value: Value::Ace, value: Value::Ace,
.. Default::default() ..Default::default()
}; };
let two = Card { let two = Card {
suit: Suit::Spade, suit: Suit::Spade,
value: Value::Two, value: Value::Two,
.. Default::default() ..Default::default()
}; };
klon.waste.push(two.clone()); klon.waste.push(two.clone());
klon.piles[0].push(ace.clone()); klon.piles[0].push(ace.clone());
@@ -589,8 +630,6 @@ mod tests {
}; };
assert!(klon.move_card_to_foundation(&source_card, &dest_card)); assert!(klon.move_card_to_foundation(&source_card, &dest_card));
// TODO the following cases: // TODO the following cases:
// - moving a card from pile to foundation when something is already there // - moving a card from pile to foundation when something is already there
// - moving Ace from waste to top of pile // - moving Ace from waste to top of pile
@@ -604,12 +643,12 @@ mod tests {
let ace = Card { let ace = Card {
suit: Suit::Heart, suit: Suit::Heart,
value: Value::Ace, value: Value::Ace,
.. Default::default() ..Default::default()
}; };
let two = Card { let two = Card {
suit: Suit::Spade, suit: Suit::Spade,
value: Value::Two, value: Value::Two,
.. Default::default() ..Default::default()
}; };
klon.waste.push(two.clone()); klon.waste.push(two.clone());
klon.piles[0].push(ace.clone()); klon.piles[0].push(ace.clone());
@@ -631,12 +670,12 @@ mod tests {
let ace_heart = Card { let ace_heart = Card {
suit: Suit::Heart, suit: Suit::Heart,
value: Value::Ace, value: Value::Ace,
.. Default::default() ..Default::default()
}; };
let ace_spade = Card { let ace_spade = Card {
suit: Suit::Spade, suit: Suit::Spade,
value: Value::Ace, value: Value::Ace,
.. Default::default() ..Default::default()
}; };
klon.waste.push(ace_heart.clone()); klon.waste.push(ace_heart.clone());
klon.piles[0].push(ace_spade.clone()); klon.piles[0].push(ace_spade.clone());
@@ -664,12 +703,12 @@ mod tests {
let ace = Card { let ace = Card {
suit: Suit::Heart, suit: Suit::Heart,
value: Value::Ace, value: Value::Ace,
.. Default::default() ..Default::default()
}; };
let two = Card { let two = Card {
suit: Suit::Spade, suit: Suit::Spade,
value: Value::Two, value: Value::Two,
.. Default::default() ..Default::default()
}; };
klon.piles[0].push(two.clone()); klon.piles[0].push(two.clone());
klon.piles[1].push(ace.clone()); klon.piles[1].push(ace.clone());
@@ -689,12 +728,12 @@ mod tests {
let ace = Card { let ace = Card {
suit: Suit::Heart, suit: Suit::Heart,
value: Value::Ace, value: Value::Ace,
.. Default::default() ..Default::default()
}; };
let two = Card { let two = Card {
suit: Suit::Diamond, suit: Suit::Diamond,
value: Value::Two, value: Value::Two,
.. Default::default() ..Default::default()
}; };
klon.piles[0].push(two.clone()); klon.piles[0].push(two.clone());
klon.piles[1].push(ace.clone()); klon.piles[1].push(ace.clone());

View File

@@ -1,13 +1,13 @@
use card_stuffs::{self, CardPosition};
use core::panic; use core::panic;
use std::io; use std::io;
use card_stuffs::{self, CardPosition};
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use ratatui::{ use ratatui::{
layout::{Constraint, Layout, Rect, Flex}, layout::{Constraint, Flex, Layout, Rect},
style::{Style, Stylize, Color}, style::{Color, Style, Stylize},
text::Line, text::Line,
widgets::{Block, BorderType, Borders, Paragraph, Clear, Wrap}, widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap},
DefaultTerminal, Frame, DefaultTerminal, Frame,
}; };
@@ -39,54 +39,70 @@ impl Default for App {
const CARD_HEIGHT: u16 = 11; const CARD_HEIGHT: u16 = 11;
const CARD_WIDTH: u16 = 15; const CARD_WIDTH: u16 = 15;
fn draw_waste(cards_in_waste: &Vec<card_stuffs::Card>, area: Rect, frame: &mut Frame, highlight: bool) { fn draw_waste(
cards_in_waste: &Vec<card_stuffs::Card>,
area: Rect,
frame: &mut Frame,
highlight: bool,
selected: bool,
) {
let horizontal = Layout::horizontal([ let horizontal = Layout::horizontal([
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(CARD_WIDTH), Constraint::Length(CARD_WIDTH),
]); ]);
let [w1, w2, top_waste] = horizontal.areas(area); let [w1, w2, top_waste] = horizontal.areas(area);
// There must be a better way to do all of this // There must be a better way to do all of this
if cards_in_waste.len() == 0 { if cards_in_waste.is_empty() {
frame.render_widget( frame.render_widget(empty_pile(highlight, selected), top_waste);
empty_pile(highlight),
top_waste
);
} }
if cards_in_waste.len() >= 3 { if cards_in_waste.len() >= 3 {
frame.render_widget( frame.render_widget(
partially_covered_card(&cards_in_waste[cards_in_waste.len() - 3]), partially_covered_card(&cards_in_waste[cards_in_waste.len() - 3]),
w1 w1,
); );
frame.render_widget( frame.render_widget(
partially_covered_card(&cards_in_waste[cards_in_waste.len() - 2]), partially_covered_card(&cards_in_waste[cards_in_waste.len() - 2]),
w2 w2,
); );
frame.render_widget( frame.render_widget(
card_widget(&cards_in_waste[cards_in_waste.len() - 1], true, highlight, false), card_widget(
top_waste &cards_in_waste[cards_in_waste.len() - 1],
true,
highlight,
false,
),
top_waste,
); );
} else if cards_in_waste.len() == 2 { } else if cards_in_waste.len() == 2 {
frame.render_widget( frame.render_widget(
partially_covered_card(&cards_in_waste[cards_in_waste.len() - 2]), partially_covered_card(&cards_in_waste[cards_in_waste.len() - 2]),
w2 w2,
); );
frame.render_widget( frame.render_widget(
card_widget(&cards_in_waste[cards_in_waste.len() - 1], true, highlight, false), card_widget(
top_waste &cards_in_waste[cards_in_waste.len() - 1],
true,
highlight,
false,
),
top_waste,
); );
} else if cards_in_waste.len() == 1 { } else if cards_in_waste.len() == 1 {
frame.render_widget( frame.render_widget(
card_widget(&cards_in_waste[cards_in_waste.len() - 1], true, highlight, false), card_widget(
top_waste &cards_in_waste[cards_in_waste.len() - 1],
true,
highlight,
false,
),
top_waste,
); );
} }
} }
impl App { impl App {
/// runs the application's main loop until the user quits /// runs the application's main loop until the user quits
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
@@ -111,19 +127,19 @@ impl App {
.borders(Borders::TOP) .borders(Borders::TOP)
.border_type(BorderType::Thick) .border_type(BorderType::Thick)
.title(Line::from("Legends of Soltar").bold()), .title(Line::from("Legends of Soltar").bold()),
title_bar 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); 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,
); );
let vertical = Layout::vertical([ let vertical = Layout::vertical([Constraint::Length(CARD_HEIGHT), Constraint::Min(0)]);
Constraint::Length(CARD_HEIGHT),
Constraint::Min(0),
]);
let [dwf_area, piles_area] = vertical.areas(main_area); let [dwf_area, piles_area] = vertical.areas(main_area);
let horizontal = Layout::horizontal([ let horizontal = Layout::horizontal([
@@ -133,23 +149,34 @@ impl App {
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); ])
.flex(Flex::SpaceAround);
let [deck_area, waste_area, fa, fb, fc, fd] = horizontal.areas(dwf_area); let [deck_area, waste_area, fa, fb, fc, fd] = horizontal.areas(dwf_area);
let foundation_areas = [fa, fb, fc, fd]; let foundation_areas = [fa, fb, fc, fd];
frame.render_widget( frame.render_widget(deck_widget(&self.cards.deck), deck_area);
deck_widget(&self.cards.deck),
deck_area
);
draw_waste(&self.cards.waste, waste_area, frame, self.highlighted_card == CardPosition::TopWaste); {
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() { for (i, fa) in foundation_areas.iter().enumerate() {
frame.render_widget( let highlight = self.highlighted_card == CardPosition::Foundation(i);
empty_pile(self.highlighted_card == CardPosition::Foundation(i)), let selected = {
*fa match self.selected_card {
); None => false,
Some(pos) => pos == CardPosition::Foundation(i),
}
};
frame.render_widget(empty_pile(highlight, selected), *fa);
} }
let horizontal = Layout::horizontal([ let horizontal = Layout::horizontal([
@@ -160,10 +187,10 @@ impl App {
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); ])
.flex(Flex::SpaceAround);
let pileses: [Rect; 7] = horizontal.areas(piles_area); let pileses: [Rect; 7] = horizontal.areas(piles_area);
for pile in 0..card_stuffs::NUM_PILES_KLONDIKE { for pile in 0..card_stuffs::NUM_PILES_KLONDIKE {
let mut constraints = Vec::new(); let mut constraints = Vec::new();
for card in 0..(card_stuffs::NUM_PILES_KLONDIKE + 13) { for card in 0..(card_stuffs::NUM_PILES_KLONDIKE + 13) {
@@ -182,7 +209,8 @@ impl App {
} }
let vertical = Layout::vertical(constraints); let vertical = Layout::vertical(constraints);
let card_display: [Rect; card_stuffs::NUM_PILES_KLONDIKE + 13] = vertical.areas(pileses[pile]); let card_display: [Rect; card_stuffs::NUM_PILES_KLONDIKE + 13] =
vertical.areas(pileses[pile]);
for (i, card) in card_display.iter().enumerate() { for (i, card) in card_display.iter().enumerate() {
match self.cards.piles[pile].get(i) { match self.cards.piles[pile].get(i) {
@@ -190,17 +218,18 @@ impl App {
Some(c) => { Some(c) => {
let is_top_card = i == self.cards.piles[pile].len() - 1; let is_top_card = i == self.cards.piles[pile].len() - 1;
let highlight = self.highlighted_card == CardPosition::Pile(pile, i); let highlight = self.highlighted_card == CardPosition::Pile(pile, i);
let selected = true; // FIXME let selected = {
match self.selected_card {
None => false,
Some(pos) => pos == CardPosition::Pile(pile, i),
}
};
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 => card_widget(c, is_top_card, highlight, selected), false => card_widget(c, is_top_card, highlight, selected),
}; };
frame.render_widget( frame.render_widget(&a_card, *card)
&a_card,
*card
)
} }
} }
} }
} }
@@ -227,19 +256,34 @@ impl App {
fn handle_key_event(&mut self, key_event: KeyEvent) { fn handle_key_event(&mut self, key_event: KeyEvent) {
match key_event.code { match key_event.code {
KeyCode::Char('q') => { KeyCode::Char('q') => {
if self.show_exit { self.exit() } if self.show_exit {
self.exit()
}
self.show_exit = true; self.show_exit = true;
}, }
KeyCode::Char('b') => self.show_exit = false, KeyCode::Char('b') => self.show_exit = false,
KeyCode::Char('d') => self.cards.deck_to_waste(), KeyCode::Char('d') => self.cards.deck_to_waste(),
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 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::Char(' ') => self.select_card(),
KeyCode::Right => self.highlighted_card = handle_move_highlighted(&self.highlighted_card, Direction::Right, &self.cards), KeyCode::Left => {
KeyCode::Up => self.highlighted_card = handle_move_highlighted(&self.highlighted_card, Direction::Up, &self.cards), self.highlighted_card =
KeyCode::Down => self.highlighted_card = handle_move_highlighted(&self.highlighted_card, Direction::Down, &self.cards), 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)
}
_ => {} _ => {}
} }
} }
@@ -247,6 +291,12 @@ impl App {
fn exit(&mut self) { fn exit(&mut self) {
self.exit = true; 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<()> { fn main() -> io::Result<()> {
@@ -263,16 +313,18 @@ enum Direction {
Right, Right,
} }
fn handle_move_highlighted(current_position: &CardPosition, direction: Direction, cards: &card_stuffs::Klondike) -> CardPosition { fn handle_move_highlighted(
current_position: &CardPosition,
direction: Direction,
cards: &card_stuffs::Klondike,
) -> CardPosition {
match current_position { match current_position {
CardPosition::TopWaste => { CardPosition::TopWaste => match direction {
match direction { Direction::Up | Direction::Left => CardPosition::TopWaste,
Direction::Up | Direction::Left => { CardPosition::TopWaste }, Direction::Right => CardPosition::Foundation(0),
Direction::Right => { CardPosition::Foundation(0) }
Direction::Down => { Direction::Down => {
let lowest_shown_card = cards.clone().lowest_visible_card_in_pile_from_index(1, 0); let lowest_shown_card = cards.clone().lowest_visible_card_in_pile_from_index(1, 0);
CardPosition:: Pile(1, lowest_shown_card) CardPosition::Pile(1, lowest_shown_card)
}
} }
}, },
CardPosition::Pile(p, i) => { CardPosition::Pile(p, i) => {
@@ -283,9 +335,10 @@ fn handle_move_highlighted(current_position: &CardPosition, direction: Direction
} else { } else {
CardPosition::Pile(*p, *i + 1) CardPosition::Pile(*p, *i + 1)
} }
}, }
Direction::Up => { Direction::Up => {
let lowest_shown_card = cards.clone().lowest_visible_card_in_pile_from_index(*p, 0); let lowest_shown_card =
cards.clone().lowest_visible_card_in_pile_from_index(*p, 0);
if *i == lowest_shown_card { if *i == lowest_shown_card {
match *p { match *p {
0 | 1 => CardPosition::TopWaste, 0 | 1 => CardPosition::TopWaste,
@@ -293,79 +346,84 @@ fn handle_move_highlighted(current_position: &CardPosition, direction: Direction
4 => CardPosition::Foundation(1), 4 => CardPosition::Foundation(1),
5 => CardPosition::Foundation(2), 5 => CardPosition::Foundation(2),
6 => CardPosition::Foundation(3), 6 => CardPosition::Foundation(3),
_ => panic!("Should be on Pile over 6") _ => panic!("Should be on Pile over 6"),
} }
} else { } else {
CardPosition::Pile(*p, *i - 1) CardPosition::Pile(*p, *i - 1)
} }
}, }
Direction::Left => { Direction::Left => {
if *p == 0 { if *p == 0 {
CardPosition::Pile(*p, *i) CardPosition::Pile(*p, *i)
} else { } else {
let lowest_shown_card = cards.clone().lowest_visible_card_in_pile_from_index(*p - 1, 0); 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 { if lowest_shown_card <= *i && cards.piles[*p].len() <= *i {
CardPosition::Pile(*p - 1, *i) // CHECK CardPosition::Pile(*p - 1, *i) // CHECK
} else { } else {
CardPosition::Pile(*p - 1, lowest_shown_card) CardPosition::Pile(*p - 1, lowest_shown_card)
} }
} }
}, }
Direction::Right => { Direction::Right => {
if *p == 6 { if *p == 6 {
CardPosition::Pile(*p, *i) CardPosition::Pile(*p, *i)
} else { } else {
let lowest_shown_card = cards.clone().lowest_visible_card_in_pile_from_index(*p + 1, 0); 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 { if lowest_shown_card <= *i && cards.piles[*p].len() <= *i {
CardPosition::Pile(*p + 1, *i) // CHECK CardPosition::Pile(*p + 1, *i) // CHECK
} else { } else {
CardPosition::Pile(*p + 1, lowest_shown_card) CardPosition::Pile(*p + 1, lowest_shown_card)
} }
} }
},
} }
}, }
CardPosition::Foundation(f) => { }
match direction { CardPosition::Foundation(f) => match direction {
Direction::Up => { CardPosition::Foundation(*f) }, Direction::Up => CardPosition::Foundation(*f),
Direction::Left => { Direction::Left => {
if *f == 0 { CardPosition::TopWaste } if *f == 0 {
else { CardPosition::Foundation(f - 1) } CardPosition::TopWaste
}, } else {
Direction::Right => { CardPosition::Foundation(f - 1)
if *f >= 3 { CardPosition::Foundation(3) }
else { CardPosition::Foundation(*f + 1)}
} }
Direction::Down => { }
match f { Direction::Right => {
if *f >= 3 {
CardPosition::Foundation(3)
} else {
CardPosition::Foundation(*f + 1)
}
}
Direction::Down => match f {
0 => { 0 => {
let i = cards.clone().lowest_visible_card_in_pile_from_index(3, 0); let i = cards.clone().lowest_visible_card_in_pile_from_index(3, 0);
CardPosition::Pile(3, i) CardPosition::Pile(3, i)
}, }
1 => { 1 => {
let i = cards.clone().lowest_visible_card_in_pile_from_index(4, 0); let i = cards.clone().lowest_visible_card_in_pile_from_index(4, 0);
CardPosition::Pile(4, i) CardPosition::Pile(4, i)
}, }
2 => { 2 => {
let i = cards.clone().lowest_visible_card_in_pile_from_index(5, 0); let i = cards.clone().lowest_visible_card_in_pile_from_index(5, 0);
CardPosition::Pile(5, i) CardPosition::Pile(5, i)
}, }
3 => { 3 => {
let i = cards.clone().lowest_visible_card_in_pile_from_index(6, 0); let i = cards.clone().lowest_visible_card_in_pile_from_index(6, 0);
CardPosition::Pile(6, i) CardPosition::Pile(6, i)
}
_ => panic!("Can't be on a foundation this high"),
},
}, },
_ => panic!("Can't be on a foundation this high")
}
}
}
}
} }
} }
fn show_help(frame: &mut Frame, area: &Rect) { fn show_help(frame: &mut Frame, area: &Rect) {
let block = Block::bordered().title("Help"); let block = Block::bordered().title("Help");
let text = let text = "You are playing \"Legends of Soltar\" - a Klondike thingy
"You are playing \"Legends of Soltar\" - a Klondike thingy
Press 'q' to Quit Press 'q' to Quit
Press '1' or '3' to change the number of cards you draw from the deck Press '1' or '3' to change the number of cards you draw from the deck
Press 'd' to draw from your deck Press 'd' to draw from your deck
@@ -381,8 +439,7 @@ Press 'w' to put the waste pile back into the deck (you can only do this when th
fn show_exit(frame: &mut Frame, area: &Rect) { fn show_exit(frame: &mut Frame, area: &Rect) {
let block = Block::bordered().title("Exit?"); let block = Block::bordered().title("Exit?");
let text = let text = "Really want to exit Legend of Soltar?
"Really want to exit Legend of Soltar?
Press 'q' to Quit or 'b' to go back"; Press 'q' to Quit or 'b' to go back";
let p = Paragraph::new(text).wrap(Wrap { trim: true }); let p = Paragraph::new(text).wrap(Wrap { trim: true });
let vertical = Layout::vertical([Constraint::Max(10)]).flex(Flex::Center); let vertical = Layout::vertical([Constraint::Max(10)]).flex(Flex::Center);
@@ -393,8 +450,12 @@ Press 'q' to Quit or 'b' to go back";
frame.render_widget(p.block(block), area); frame.render_widget(p.block(block), area);
} }
fn card_widget<'a>(
fn card_widget<'a>(card: &'a card_stuffs::Card, top: bool, highlight: bool, select: bool) -> Paragraph<'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);
} }
@@ -418,15 +479,14 @@ fn card_widget<'a>(card: &'a card_stuffs::Card, top: bool, highlight: bool, sele
border_style = border_style.fg(Color::Green); border_style = border_style.fg(Color::Green);
} }
Paragraph::new(card_image) Paragraph::new(card_image).style(card_style).block(
.style(card_style) Block::new()
.block(Block::new()
.style(border_style) .style(border_style)
.borders(borders) .borders(borders)
.border_type(BorderType::Rounded)) .border_type(BorderType::Rounded),
)
} }
fn deck_widget(cards_in_deck: &Vec<card_stuffs::Card>) -> Paragraph<'static> { fn deck_widget(cards_in_deck: &Vec<card_stuffs::Card>) -> Paragraph<'static> {
let card_image = format!( let card_image = format!(
"#############\n\ "#############\n\
@@ -437,31 +497,31 @@ fn deck_widget(cards_in_deck: &Vec<card_stuffs::Card>) -> Paragraph<'static> {
#### {:02} #####\n\ #### {:02} #####\n\
#############\n\ #############\n\
#############\n\ #############\n\
#############", cards_in_deck.len() #############",
cards_in_deck.len()
); );
Paragraph::new(card_image) Paragraph::new(card_image).block(
.block(Block::new() Block::new()
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded)) .border_type(BorderType::Rounded),
)
} }
fn partially_covered_card(card: &card_stuffs::Card) -> Paragraph { fn partially_covered_card(card: &card_stuffs::Card) -> Paragraph {
let card_image = format!( let card_image = format!("{value}{suit}", value = card.value, suit = card.suit);
"{value}{suit}", value=card.value, suit=card.suit
);
let card_style = match card.suit.colour() { let card_style = match card.suit.colour() {
card_stuffs::Colour::Black => Style::new().black().bg(Color::White), card_stuffs::Colour::Black => Style::new().black().bg(Color::White),
card_stuffs::Colour::Red => Style::new().red().bg(Color::White), card_stuffs::Colour::Red => Style::new().red().bg(Color::White),
}; };
let borders = Borders::TOP | Borders::LEFT | Borders::BOTTOM; let borders = Borders::TOP | Borders::LEFT | Borders::BOTTOM;
let border_style = Style::new().white().on_black(); let border_style = Style::new().white().on_black();
Paragraph::new(card_image) Paragraph::new(card_image).style(card_style).block(
.style(card_style) Block::new()
.block(Block::new()
.style(border_style) .style(border_style)
.borders(borders) .borders(borders)
.border_type(BorderType::Rounded)) .border_type(BorderType::Rounded),
)
} }
fn facedown_card(top: bool) -> Paragraph<'static> { fn facedown_card(top: bool) -> Paragraph<'static> {
@@ -482,16 +542,17 @@ fn facedown_card(top: bool) -> Paragraph<'static> {
borders |= Borders::BOTTOM; borders |= Borders::BOTTOM;
} }
Paragraph::new(hidden_card) Paragraph::new(hidden_card).block(
.block(Block::new() Block::new()
.borders(borders) .borders(borders)
.border_type(BorderType::Rounded)) .border_type(BorderType::Rounded),
)
} }
fn empty_pile(highlight: bool) -> Paragraph<'static> { fn empty_pile(highlight: bool, selected: bool) -> Paragraph<'static> {
// made using https://www.asciiart.eu/ // made using https://www.asciiart.eu/
let hidden_card = format!( let hidden_card = format!(
" "
XX XX XX XX
XX XX XX XX
@@ -504,21 +565,23 @@ fn empty_pile(highlight: bool) -> Paragraph<'static> {
let mut border_style = Style::new(); let mut border_style = Style::new();
if highlight { if highlight {
border_style = border_style.fg(Color::Blue); border_style = border_style.fg(Color::Blue);
} else if selected {
border_style = border_style.fg(Color::Green);
} }
Paragraph::new(hidden_card) Paragraph::new(hidden_card).block(
.block(Block::new() Block::new()
.style(border_style) .style(border_style)
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded)) .border_type(BorderType::Rounded),
)
} }
fn card_paragraph(c: &card_stuffs::Card) -> String { fn card_paragraph(c: &card_stuffs::Card) -> String {
match c.value { match c.value {
card_stuffs::Value::Ace => { card_stuffs::Value::Ace => {
/* /*
XX XX
X X
X X X X
X X X X
@@ -529,7 +592,7 @@ XX
XX XX
*/ */
format!( format!(
"{value}{suit} "{value}{suit}
{suit} {suit}
{suit} {suit} {suit} {suit}
{suit} {suit} {suit} {suit}
@@ -537,11 +600,14 @@ XX
{suit}{suit}{suit}{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) {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Two => { card_stuffs::Value::Two => {
/* /*
XX XX
X X
XXX XXX
X X
@@ -552,7 +618,7 @@ XX
XX XX
*/ */
format!( format!(
"{value}{suit} "{value}{suit}
{suit} {suit}
{suit}{suit}{suit} {suit}{suit}{suit}
{suit} {suit}
@@ -560,11 +626,14 @@ XX
{suit} {suit}
{suit}{suit}{suit} {suit}{suit}{suit}
{suit} {suit}
{value}{suit}", value=c.value, suit=c.suit) {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Three => { card_stuffs::Value::Three => {
/* /*
XX X XX X
XXX XXX
X X
X X
@@ -575,7 +644,7 @@ XX X
X XX X XX
*/ */
format!( format!(
"{value}{suit} {suit} "{value}{suit} {suit}
{suit}{suit}{suit} {suit}{suit}{suit}
{suit} {suit}
{suit} {suit}
@@ -583,11 +652,14 @@ XX X
{suit} {suit}
{suit} {suit}
{suit}{suit}{suit} {suit}{suit}{suit}
{suit} {value}{suit}", value=c.value, suit=c.suit) {suit} {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Four => { card_stuffs::Value::Four => {
format!( format!(
"{value}{suit} "{value}{suit}
{suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit}
@@ -595,11 +667,14 @@ XX X
{suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit}
{value}{suit}", value=c.value, suit=c.suit) {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Five => { card_stuffs::Value::Five => {
format!( format!(
"{value}{suit} "{value}{suit}
{suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit}
@@ -607,11 +682,14 @@ XX X
{suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit}
{value}{suit}", value=c.value, suit=c.suit) {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Six => { card_stuffs::Value::Six => {
/* /*
XX XX
XX XX XX XX
@@ -620,9 +698,9 @@ XX
XX XX XX XX
XX XX
*/ */
format!( format!(
"{value}{suit} "{value}{suit}
{suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit}
@@ -630,11 +708,14 @@ XX
{suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit}
{value}{suit}", value=c.value, suit=c.suit) {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Seven => { card_stuffs::Value::Seven => {
/* /*
XX XX
XX XX XX XX
XX XX
@@ -643,9 +724,9 @@ XX
XX XX XX XX
XX XX
*/ */
format!( format!(
"{value}{suit} "{value}{suit}
{suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit}
{suit}{suit} {suit}{suit}
@@ -653,11 +734,14 @@ XX
{suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit}
{suit}{suit} {suit}{suit} {suit}{suit} {suit}{suit}
{value}{suit}", value=c.value, suit=c.suit) {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Eight => { card_stuffs::Value::Eight => {
/* /*
XX XX
X X X X
X X
@@ -666,9 +750,9 @@ XX
X X X X
XX XX
*/ */
format!( format!(
"{value}{suit} "{value}{suit}
{suit} {suit} {suit} {suit}
{suit} {suit}
@@ -676,11 +760,14 @@ XX
{suit} {suit}
{suit} {suit} {suit} {suit}
{value}{suit}", value=c.value, suit=c.suit) {value}{suit}",
}, value = c.value,
card_stuffs::Value::Nine=> { suit = c.suit
/* )
XX }
card_stuffs::Value::Nine => {
/*
XX
X X X X
X X X X
@@ -689,9 +776,9 @@ XX
X X X X
XX XX
*/ */
format!( format!(
"{value}{suit} "{value}{suit}
{suit} {suit} {suit} {suit}
{suit} {suit} {suit} {suit}
@@ -699,11 +786,14 @@ XX
{suit} {suit} {suit} {suit}
{suit} {suit} {suit} {suit}
{value}{suit}", value=c.value, suit=c.suit) {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Ten => { card_stuffs::Value::Ten => {
/* /*
XX XX
X X X X
X X
X X X X
@@ -712,9 +802,9 @@ XX
X X
X X X X
XX XX
*/ */
format!( format!(
"{value}{suit} "{value}{suit}
{suit} {suit} {suit} {suit}
{suit} {suit}
{suit} {suit} {suit} {suit}
@@ -722,11 +812,14 @@ XX
{suit} {suit} {suit} {suit}
{suit} {suit}
{suit} {suit} {suit} {suit}
{value}{suit}", value=c.value, suit=c.suit) {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Jack => { card_stuffs::Value::Jack => {
/* /*
XX XX
XXXXX XXXXX
X X
@@ -735,9 +828,9 @@ XX
XX XX
XX XX
*/ */
format!( format!(
"{value}{suit} "{value}{suit}
{suit}{suit}{suit}{suit}{suit} {suit}{suit}{suit}{suit}{suit}
{suit} {suit}
@@ -745,11 +838,14 @@ XX
{suit} {suit} {suit} {suit}
{suit}{suit} {suit}{suit}
{value}{suit}", value=c.value, suit=c.suit) {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Queen => { card_stuffs::Value::Queen => {
/* /*
XX XX
XXXX XXXX
X X X X
X X X X
@@ -758,9 +854,9 @@ XX
XXXXX XXXXX
X X
XX XX
*/ */
format!( format!(
"{value}{suit} "{value}{suit}
{suit}{suit}{suit}{suit} {suit}{suit}{suit}{suit}
{suit} {suit} {suit} {suit}
{suit} {suit} {suit} {suit}
@@ -768,11 +864,14 @@ XX
{suit} {suit} {suit} {suit}
{suit}{suit}{suit}{suit}{suit} {suit}{suit}{suit}{suit}{suit}
{suit} {suit}
{value}{suit}", value=c.value, suit=c.suit) {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
card_stuffs::Value::King => { card_stuffs::Value::King => {
/* /*
XX XX
X X X X
X XX X XX
X XX X XX
@@ -781,9 +880,9 @@ XX
X XX X XX
X X X X
XX XX
*/ */
format!( format!(
"{value}{suit} "{value}{suit}
{suit} {suit} {suit} {suit}
{suit} {suit}{suit} {suit} {suit}{suit}
{suit} {suit}{suit} {suit} {suit}{suit}
@@ -791,7 +890,10 @@ XX
{suit} {suit}{suit} {suit} {suit}{suit}
{suit} {suit}{suit} {suit} {suit}{suit}
{suit} {suit} {suit} {suit}
{value}{suit}", value=c.value, suit=c.suit) {value}{suit}",
}, value = c.value,
suit = c.suit
)
}
} }
} }

View File

@@ -7,6 +7,10 @@ edition = "2021"
[dependencies] [dependencies]
chrono = { version = "0.4.39", features = ["serde"] } chrono = { version = "0.4.39", features = ["serde"] }
clap = { version = "4.5.42", features = ["derive"] }
closestmatch = "0.1.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.138" serde_json = "1.0.138"
tempfile = "3.20.0"
ureq = { version = "3.0.12", features = ["json"] }
uuid = { version = "1.12.1", features = ["v4", "serde"] } uuid = { version = "1.12.1", features = ["v4", "serde"] }

View File

@@ -1,8 +1,83 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use std::fs;
use std::io::{Read, Seek, SeekFrom, Write};
use tempfile::NamedTempFile;
use ureq;
use uuid::Uuid; use uuid::Uuid;
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct ScryfallBulkData {
pub id: Uuid,
pub uri: String,
#[serde(rename = "type")]
pub stype: String,
pub name: String,
pub description: String,
pub download_uri: String,
pub updated_at: String,
pub content_type: String,
pub content_encoding: String,
}
#[allow(dead_code)]
#[derive(Deserialize, Debug)]
struct ScryfallBulk {
pub object: String,
pub has_more: bool,
pub data: Vec<ScryfallBulkData>,
}
#[derive(Deserialize, PartialEq, Debug)]
pub enum ScryfallBulkType {
#[serde(rename = "oracle_cards")]
OracleCards,
#[serde(rename = "unique_artwork")]
UniqueArtwork,
#[serde(rename = "default_cards")]
DefaultCards,
#[serde(rename = "all_cards")]
AllCards,
#[serde(rename = "rulings")]
Rulings,
}
const SCRYFALL_BULK_API: &str = "https://api.scryfall.com/bulk-data";
pub fn download_latest(
_stype: ScryfallBulkType,
mut dest_file: &NamedTempFile,
) -> Result<(), Box<dyn std::error::Error>> {
let bulk_body: ScryfallBulk = ureq::get(SCRYFALL_BULK_API)
.header("User-Agent", "Arthur's Card Finger Testing v0.1")
.header("Accept", "application/json")
.call()?
.body_mut()
.read_json::<ScryfallBulk>()?;
let mut download_uri = String::new();
for scryfall_bulk in bulk_body.data {
// TODO: Actually implement getting different types
if scryfall_bulk.stype == "oracle_cards" {
download_uri = scryfall_bulk.download_uri;
}
}
assert!(!download_uri.is_empty());
let cards_response = ureq::get(download_uri)
.header("User-Agent", "Arthur's Card Finger Testing v0.1")
.header("Accept", "application/json")
.call()?
.body_mut()
.with_config()
.limit(700 * 1024 * 1024)
.read_to_string()?;
write!(dest_file, "{}", cards_response)?;
Ok(())
}
// Info from here: // Info from here:
// https://scryfall.com/docs/api/cards // https://scryfall.com/docs/api/cards
#[allow(dead_code)] #[allow(dead_code)]
@@ -110,7 +185,6 @@ struct ScryfallCard {
pub watermark: Option<String>, pub watermark: Option<String>,
pub preview: Option<Preview>, pub preview: Option<Preview>,
// These aren't in the Scryfall docs, but some cards do have 'em // These aren't in the Scryfall docs, but some cards do have 'em
pub foil: Option<bool>, pub foil: Option<bool>,
pub nonfoil: Option<bool>, pub nonfoil: Option<bool>,
@@ -785,4 +859,14 @@ mod tests {
let ac = fs::read_to_string(f).unwrap(); let ac = fs::read_to_string(f).unwrap();
let _ac: Vec<ScryfallCard> = serde_json::from_str(&ac).unwrap(); let _ac: Vec<ScryfallCard> = serde_json::from_str(&ac).unwrap();
} }
#[test]
#[ignore]
fn get_scryfall_bulk_page() {
let mut file = NamedTempFile::new().unwrap();
let _ = download_latest(ScryfallBulkType::OracleCards, &file);
let file_size = file.seek(SeekFrom::End(0)).unwrap();
assert!(file_size > 4092);
println!("File size: {}", file_size);
}
} }

View File

@@ -0,0 +1,22 @@
use clap::Parser;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
#[arg(short, long)]
update: bool,
remainder: Vec<String>,
}
fn main() {
let mut args = Args::parse();
if args.update {
unimplemented!("Haven't implemented update yet");
}
let card_name = args.remainder;
if card_name.is_empty() {
panic!("You need to put some card text to search");
}
let search_string = card_name.join(" ");
dbg!(search_string);
}