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_macros::EnumIter;
use std::fmt;
use rand::seq::SliceRandom;
use rand::rng;
use thiserror::Error;
#[derive(PartialEq, Debug, EnumIter, Copy, Clone)]
pub enum Suit {
Heart,
Diamond,
Club,
Spade
Spade,
}
#[derive(PartialEq, Debug)]
@@ -53,7 +53,7 @@ pub enum Value {
Ten,
Jack,
Queen,
King
King,
}
impl Value {
@@ -97,7 +97,6 @@ impl fmt::Display for Value {
}
}
#[derive(PartialEq, Debug, Copy, Clone)]
pub struct Card {
pub suit: Suit,
@@ -105,7 +104,6 @@ pub struct Card {
pub visible: bool,
}
impl fmt::Display for Card {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}{}", self.value, self.suit)
@@ -140,8 +138,13 @@ impl Card {
}
// Needs to be adjacent
if self.value == Value::King || self.value.indexed_values() + 1 != top.value.indexed_values() {
return Err(StackingError::NotAdjacent(self.to_string(), top.to_string()));
if self.value == Value::King
|| self.value.indexed_values() + 1 != top.value.indexed_values()
{
return Err(StackingError::NotAdjacent(
self.to_string(),
top.to_string(),
));
}
Ok(())
@@ -153,9 +156,12 @@ impl Card {
if self.value == Value::Ace {
return Ok(());
} 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) => {
if self.suit != c.suit {
return Err(StackingError::WrongSuit);
@@ -172,7 +178,7 @@ impl Card {
#[derive(Debug)]
pub struct Deck {
pub cards: Vec<Card>
pub cards: Vec<Card>,
}
impl Default for Deck {
@@ -180,18 +186,14 @@ impl Default for Deck {
let mut array = Vec::new();
for suit in Suit::iter() {
for value in Value::iter() {
array.push(
Card {
array.push(Card {
suit,
value,
..Default::default()
}
);
});
}
}
Deck {
cards: array,
}
Deck { cards: array }
}
}
@@ -222,7 +224,7 @@ pub struct CardAndPosition {
pub enum CardPosition {
TopWaste, // I don't think we'd ever interact with anything other than the top of the Waste
Pile(usize, usize), // (PileNumber, Index)
Foundation(usize)
Foundation(usize),
}
#[derive(Debug, Copy, Clone)]
@@ -245,7 +247,7 @@ pub struct 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 {
let card = self.deck.pop();
match card {
@@ -255,13 +257,15 @@ impl Klondike {
}
}
// 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 {
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!
return
},
return;
}
}
}
if self.deck.len() != 0 {
// no!
@@ -275,7 +279,7 @@ impl Klondike {
pub fn move_card(self, source_card: &CardAndPosition, dest_card: &CardAndPosition) -> bool {
// TODO raise errors properly
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();
// Maybe TODO - check the .cards is the actual card in that position
@@ -283,19 +287,30 @@ impl Klondike {
match dest_card.pos {
CardPosition::Pile(_, _) => self.move_card_to_pile(&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 - 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
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
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;
}
@@ -305,27 +320,36 @@ impl Klondike {
let card = self.waste.pop().unwrap();
self.foundation[foundation_index].push(card);
return true;
},
}
CardPosition::Pile(pile_index, _) => {
let card = self.piles[pile_index].pop().unwrap();
self.foundation[foundation_index].push(card);
return true;
},
}
CardPosition::Foundation(_) => {
unreachable!()
},
}
}
}
unreachable!();
}
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() {
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
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
@@ -337,7 +361,7 @@ impl Klondike {
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();
@@ -349,11 +373,11 @@ impl Klondike {
}
// 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
}
@@ -361,8 +385,7 @@ impl Klondike {
fn is_card_top_of_pile(self, pos: &CardPosition) -> bool {
// TODO consider, at which point the Pos::Pile() ranges etc are correct
match pos {
CardPosition::Pile(pile_index, card_index) => {
match self.piles.get(*pile_index) {
CardPosition::Pile(pile_index, card_index) => match self.piles.get(*pile_index) {
Some(pile_index) => {
if *card_index == (pile_index.len() - 1) {
true
@@ -370,10 +393,9 @@ impl Klondike {
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 {
let mut deck = Deck::default();
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 num in 0..pile+1 {
for num in 0..pile + 1 {
let mut c = deck.cards.pop().unwrap();
if num == pile {
c.visible = true;
@@ -445,19 +475,28 @@ mod tests {
value: Value::Six,
..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 {
suit: Suit::Diamond,
value: Value::Six,
..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 {
suit: Suit::Club,
value: Value::Six,
..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 {
suit: Suit::Club,
value: Value::Seven,
@@ -467,7 +506,9 @@ mod tests {
if let Err(e) = not_adj_error {
match e {
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!(),
}
} else {
@@ -498,12 +539,12 @@ mod tests {
let ace = Card {
suit: Suit::Spade,
value: Value::Ace,
.. Default::default()
..Default::default()
};
klon.piles[0].push(ace.clone());
let source_card = CardAndPosition {
card: Some(ace.clone()),
pos: CardPosition::Pile(0, 1)
pos: CardPosition::Pile(0, 1),
};
let dest_card = CardAndPosition {
card: None,
@@ -516,12 +557,12 @@ mod tests {
let two = Card {
suit: Suit::Spade,
value: Value::Two,
.. Default::default()
..Default::default()
};
klon.piles[0].push(two.clone());
let source_card = CardAndPosition {
card: Some(two.clone()),
pos: CardPosition::Pile(0, 1)
pos: CardPosition::Pile(0, 1),
};
let dest_card = CardAndPosition {
card: None,
@@ -534,7 +575,7 @@ mod tests {
let ace = Card {
suit: Suit::Spade,
value: Value::Ace,
.. Default::default()
..Default::default()
};
klon.waste.push(ace.clone());
let source_card = CardAndPosition {
@@ -552,7 +593,7 @@ mod tests {
let two = Card {
suit: Suit::Spade,
value: Value::Two,
.. Default::default()
..Default::default()
};
klon.waste.push(two.clone());
let source_card = CardAndPosition {
@@ -570,12 +611,12 @@ mod tests {
let ace = Card {
suit: Suit::Spade,
value: Value::Ace,
.. Default::default()
..Default::default()
};
let two = Card {
suit: Suit::Spade,
value: Value::Two,
.. Default::default()
..Default::default()
};
klon.waste.push(two.clone());
klon.piles[0].push(ace.clone());
@@ -589,8 +630,6 @@ mod tests {
};
assert!(klon.move_card_to_foundation(&source_card, &dest_card));
// TODO the following cases:
// - moving a card from pile to foundation when something is already there
// - moving Ace from waste to top of pile
@@ -604,12 +643,12 @@ mod tests {
let ace = Card {
suit: Suit::Heart,
value: Value::Ace,
.. Default::default()
..Default::default()
};
let two = Card {
suit: Suit::Spade,
value: Value::Two,
.. Default::default()
..Default::default()
};
klon.waste.push(two.clone());
klon.piles[0].push(ace.clone());
@@ -631,12 +670,12 @@ mod tests {
let ace_heart = Card {
suit: Suit::Heart,
value: Value::Ace,
.. Default::default()
..Default::default()
};
let ace_spade = Card {
suit: Suit::Spade,
value: Value::Ace,
.. Default::default()
..Default::default()
};
klon.waste.push(ace_heart.clone());
klon.piles[0].push(ace_spade.clone());
@@ -664,12 +703,12 @@ mod tests {
let ace = Card {
suit: Suit::Heart,
value: Value::Ace,
.. Default::default()
..Default::default()
};
let two = Card {
suit: Suit::Spade,
value: Value::Two,
.. Default::default()
..Default::default()
};
klon.piles[0].push(two.clone());
klon.piles[1].push(ace.clone());
@@ -689,12 +728,12 @@ mod tests {
let ace = Card {
suit: Suit::Heart,
value: Value::Ace,
.. Default::default()
..Default::default()
};
let two = Card {
suit: Suit::Diamond,
value: Value::Two,
.. Default::default()
..Default::default()
};
klon.piles[0].push(two.clone());
klon.piles[1].push(ace.clone());

View File

@@ -1,13 +1,13 @@
use card_stuffs::{self, CardPosition};
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},
layout::{Constraint, Flex, Layout, Rect},
style::{Color, Style, Stylize},
text::Line,
widgets::{Block, BorderType, Borders, Paragraph, Clear, Wrap},
widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap},
DefaultTerminal, Frame,
};
@@ -39,54 +39,70 @@ impl Default for App {
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) {
fn draw_waste(
cards_in_waste: &Vec<card_stuffs::Card>,
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.len() == 0 {
frame.render_widget(
empty_pile(highlight),
top_waste
);
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
w1,
);
frame.render_widget(
partially_covered_card(&cards_in_waste[cards_in_waste.len() - 2]),
w2
w2,
);
frame.render_widget(
card_widget(&cards_in_waste[cards_in_waste.len() - 1], true, highlight, false),
top_waste
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
w2,
);
frame.render_widget(
card_widget(&cards_in_waste[cards_in_waste.len() - 1], true, highlight, false),
top_waste
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
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<()> {
@@ -111,19 +127,19 @@ impl App {
.borders(Borders::TOP)
.border_type(BorderType::Thick)
.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(
Block::new().borders(Borders::TOP).title(status_bar_info),
status_bar
status_bar,
);
let vertical = Layout::vertical([
Constraint::Length(CARD_HEIGHT),
Constraint::Min(0),
]);
let vertical = Layout::vertical([Constraint::Length(CARD_HEIGHT), Constraint::Min(0)]);
let [dwf_area, piles_area] = vertical.areas(main_area);
let horizontal = Layout::horizontal([
@@ -133,23 +149,34 @@ impl App {
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 foundation_areas = [fa, fb, fc, fd];
frame.render_widget(
deck_widget(&self.cards.deck),
deck_area
);
frame.render_widget(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() {
frame.render_widget(
empty_pile(self.highlighted_card == CardPosition::Foundation(i)),
*fa
);
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([
@@ -160,10 +187,10 @@ impl App {
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);
for pile in 0..card_stuffs::NUM_PILES_KLONDIKE {
let mut constraints = Vec::new();
for card in 0..(card_stuffs::NUM_PILES_KLONDIKE + 13) {
@@ -182,7 +209,8 @@ impl App {
}
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() {
match self.cards.piles[pile].get(i) {
@@ -190,17 +218,18 @@ impl App {
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 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
)
frame.render_widget(&a_card, *card)
}
}
}
}
@@ -227,19 +256,34 @@ impl App {
fn handle_key_event(&mut self, key_event: KeyEvent) {
match key_event.code {
KeyCode::Char('q') => {
if self.show_exit { self.exit() }
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),
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)
}
_ => {}
}
}
@@ -247,6 +291,12 @@ impl App {
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<()> {
@@ -263,16 +313,18 @@ enum Direction {
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 {
CardPosition::TopWaste => {
match direction {
Direction::Up | Direction::Left => { CardPosition::TopWaste },
Direction::Right => { CardPosition::Foundation(0) }
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(1, lowest_shown_card)
}
},
CardPosition::Pile(p, i) => {
@@ -283,9 +335,10 @@ fn handle_move_highlighted(current_position: &CardPosition, direction: Direction
} else {
CardPosition::Pile(*p, *i + 1)
}
},
}
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 {
match *p {
0 | 1 => CardPosition::TopWaste,
@@ -293,79 +346,84 @@ fn handle_move_highlighted(current_position: &CardPosition, direction: Direction
4 => CardPosition::Foundation(1),
5 => CardPosition::Foundation(2),
6 => CardPosition::Foundation(3),
_ => panic!("Should be on Pile over 6")
_ => 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);
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);
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) },
}
}
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)}
if *f == 0 {
CardPosition::TopWaste
} 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 => {
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"),
},
},
_ => 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
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
@@ -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) {
let block = Block::bordered().title("Exit?");
let text =
"Really want to exit Legend of Soltar?
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);
@@ -393,8 +450,12 @@ Press 'q' to Quit or 'b' to go back";
frame.render_widget(p.block(block), area);
}
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 {
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);
}
Paragraph::new(card_image)
.style(card_style)
.block(Block::new()
Paragraph::new(card_image).style(card_style).block(
Block::new()
.style(border_style)
.borders(borders)
.border_type(BorderType::Rounded))
.border_type(BorderType::Rounded),
)
}
fn deck_widget(cards_in_deck: &Vec<card_stuffs::Card>) -> Paragraph<'static> {
let card_image = format!(
"#############\n\
@@ -437,31 +497,31 @@ fn deck_widget(cards_in_deck: &Vec<card_stuffs::Card>) -> Paragraph<'static> {
#### {:02} #####\n\
#############\n\
#############\n\
#############", cards_in_deck.len()
#############",
cards_in_deck.len()
);
Paragraph::new(card_image)
.block(Block::new()
Paragraph::new(card_image).block(
Block::new()
.borders(Borders::ALL)
.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_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()
Paragraph::new(card_image).style(card_style).block(
Block::new()
.style(border_style)
.borders(borders)
.border_type(BorderType::Rounded))
.border_type(BorderType::Rounded),
)
}
fn facedown_card(top: bool) -> Paragraph<'static> {
@@ -482,16 +542,17 @@ fn facedown_card(top: bool) -> Paragraph<'static> {
borders |= Borders::BOTTOM;
}
Paragraph::new(hidden_card)
.block(Block::new()
Paragraph::new(hidden_card).block(
Block::new()
.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/
let hidden_card = format!(
"
"
XX XX
XX XX
@@ -504,21 +565,23 @@ fn empty_pile(highlight: bool) -> Paragraph<'static> {
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()
Paragraph::new(hidden_card).block(
Block::new()
.style(border_style)
.borders(Borders::ALL)
.border_type(BorderType::Rounded))
.border_type(BorderType::Rounded),
)
}
fn card_paragraph(c: &card_stuffs::Card) -> String {
match c.value {
card_stuffs::Value::Ace => {
/*
XX
/*
XX
X
X X
X X
@@ -529,7 +592,7 @@ XX
XX
*/
format!(
"{value}{suit}
"{value}{suit}
{suit}
{suit} {suit}
{suit} {suit}
@@ -537,11 +600,14 @@ XX
{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 => {
/*
XX
/*
XX
X
XXX
X
@@ -552,7 +618,7 @@ XX
XX
*/
format!(
"{value}{suit}
"{value}{suit}
{suit}
{suit}{suit}{suit}
{suit}
@@ -560,11 +626,14 @@ XX
{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 => {
/*
XX X
/*
XX X
XXX
X
X
@@ -575,7 +644,7 @@ XX X
X XX
*/
format!(
"{value}{suit} {suit}
"{value}{suit} {suit}
{suit}{suit}{suit}
{suit}
{suit}
@@ -583,11 +652,14 @@ XX X
{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 => {
format!(
"{value}{suit}
"{value}{suit}
{suit}{suit} {suit}{suit}
@@ -595,11 +667,14 @@ XX X
{suit}{suit} {suit}{suit}
{value}{suit}", value=c.value, suit=c.suit)
},
{value}{suit}",
value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Five => {
format!(
"{value}{suit}
"{value}{suit}
{suit}{suit} {suit}{suit}
@@ -607,11 +682,14 @@ XX X
{suit}{suit} {suit}{suit}
{value}{suit}", value=c.value, suit=c.suit)
},
{value}{suit}",
value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Six => {
/*
XX
/*
XX
XX XX
@@ -620,9 +698,9 @@ XX
XX XX
XX
*/
*/
format!(
"{value}{suit}
"{value}{suit}
{suit}{suit} {suit}{suit}
@@ -630,11 +708,14 @@ XX
{suit}{suit} {suit}{suit}
{value}{suit}", value=c.value, suit=c.suit)
},
{value}{suit}",
value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Seven => {
/*
XX
/*
XX
XX XX
XX
@@ -643,9 +724,9 @@ XX
XX XX
XX
*/
*/
format!(
"{value}{suit}
"{value}{suit}
{suit}{suit} {suit}{suit}
{suit}{suit}
@@ -653,11 +734,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::Eight => {
/*
XX
/*
XX
X X
X
@@ -666,9 +750,9 @@ XX
X X
XX
*/
*/
format!(
"{value}{suit}
"{value}{suit}
{suit} {suit}
{suit}
@@ -676,11 +760,14 @@ XX
{suit}
{suit} {suit}
{value}{suit}", value=c.value, suit=c.suit)
},
card_stuffs::Value::Nine=> {
/*
XX
{value}{suit}",
value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Nine => {
/*
XX
X X
X X
@@ -689,9 +776,9 @@ XX
X X
XX
*/
*/
format!(
"{value}{suit}
"{value}{suit}
{suit} {suit}
{suit} {suit}
@@ -699,11 +786,14 @@ XX
{suit} {suit}
{suit} {suit}
{value}{suit}", value=c.value, suit=c.suit)
},
{value}{suit}",
value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Ten => {
/*
XX
/*
XX
X X
X
X X
@@ -712,9 +802,9 @@ XX
X
X X
XX
*/
*/
format!(
"{value}{suit}
"{value}{suit}
{suit} {suit}
{suit}
{suit} {suit}
@@ -722,11 +812,14 @@ XX
{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 => {
/*
XX
/*
XX
XXXXX
X
@@ -735,9 +828,9 @@ XX
XX
XX
*/
*/
format!(
"{value}{suit}
"{value}{suit}
{suit}{suit}{suit}{suit}{suit}
{suit}
@@ -745,11 +838,14 @@ XX
{suit} {suit}
{suit}{suit}
{value}{suit}", value=c.value, suit=c.suit)
},
{value}{suit}",
value = c.value,
suit = c.suit
)
}
card_stuffs::Value::Queen => {
/*
XX
/*
XX
XXXX
X X
X X
@@ -758,9 +854,9 @@ XX
XXXXX
X
XX
*/
*/
format!(
"{value}{suit}
"{value}{suit}
{suit}{suit}{suit}{suit}
{suit} {suit}
{suit} {suit}
@@ -768,11 +864,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::King => {
/*
XX
/*
XX
X X
X XX
X XX
@@ -781,9 +880,9 @@ XX
X XX
X X
XX
*/
*/
format!(
"{value}{suit}
"{value}{suit}
{suit} {suit}
{suit} {suit}{suit}
{suit} {suit}{suit}
@@ -791,7 +890,10 @@ 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
)
}
}
}

View File

@@ -7,6 +7,10 @@ edition = "2021"
[dependencies]
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_json = "1.0.138"
tempfile = "3.20.0"
ureq = { version = "3.0.12", features = ["json"] }
uuid = { version = "1.12.1", features = ["v4", "serde"] }

View File

@@ -1,8 +1,83 @@
use chrono::NaiveDate;
use serde::Deserialize;
use serde_json::Value;
use std::fs;
use std::io::{Read, Seek, SeekFrom, Write};
use tempfile::NamedTempFile;
use ureq;
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:
// https://scryfall.com/docs/api/cards
#[allow(dead_code)]
@@ -110,7 +185,6 @@ struct ScryfallCard {
pub watermark: Option<String>,
pub preview: Option<Preview>,
// These aren't in the Scryfall docs, but some cards do have 'em
pub foil: Option<bool>,
pub nonfoil: Option<bool>,
@@ -785,4 +859,14 @@ mod tests {
let ac = fs::read_to_string(f).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);
}