-
-
Save rtsuk/767f7d2c9b90c5e3e6b4ea90cfa91e7f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#![no_std] | |
#![allow(unused_imports, unused_variables, dead_code)] | |
extern crate alloc; | |
use alloc::{boxed::Box, collections::BTreeMap, vec::Vec}; | |
use anyhow::Error; | |
use core::mem; | |
use enum_iterator::IntoEnumIterator; | |
use euclid::{Point2D, Vector2D}; | |
use hashbrown::HashMap; | |
use playdate::{ | |
graphics::{ | |
Bitmap, BitmapDrawMode, BitmapFlip, BitmapTable, Font, Graphics, LCDRect, SolidColor, | |
}, | |
log_to_console, playdate_game, Game, Playdate, | |
}; | |
use playdate_sys::{PDButtons_kButtonA, PDButtons_kButtonB, LCD_COLUMNS, LCD_ROWS}; | |
use rand::{prelude::*, seq::SliceRandom, SeedableRng}; | |
const SCREEN_CLIP: LCDRect = LCDRect { | |
left: 0, | |
right: LCD_COLUMNS as i32, | |
top: 0, | |
bottom: LCD_ROWS as i32, | |
}; | |
const SCREEN_WIDTH: i32 = LCD_COLUMNS as i32; | |
const SCREEN_HEIGHT: i32 = LCD_ROWS as i32; | |
const MARGIN: i32 = 10; | |
const INDEX_MARGIN_X: i32 = 4; | |
const INDEX_MARGIN_Y: i32 = 1; | |
const GUTTER: i32 = 5; | |
const CARD_WIDTH: i32 = 50; | |
const CARD_HEIGHT: i32 = 70; | |
const CRANK_THRESHHOLD: i32 = 10; | |
#[derive(Clone, Copy, Eq, IntoEnumIterator, Ord, PartialEq, PartialOrd)] | |
enum Stacks { | |
Stock, | |
Waste, | |
Foundation1, | |
Foundation2, | |
Foundation3, | |
Foundation4, | |
Tableau1, | |
Tableau2, | |
Tableau3, | |
Tableau4, | |
Tableau5, | |
Tableau6, | |
Tableau7, | |
} | |
impl Stacks { | |
fn next(&self) -> Self { | |
match self { | |
Stacks::Stock => Stacks::Waste, | |
Stacks::Waste => Stacks::Foundation1, | |
Stacks::Foundation1 => Stacks::Foundation2, | |
Stacks::Foundation2 => Stacks::Foundation3, | |
Stacks::Foundation3 => Stacks::Foundation4, | |
Stacks::Foundation4 => Stacks::Tableau1, | |
Stacks::Tableau1 => Stacks::Tableau2, | |
Stacks::Tableau2 => Stacks::Tableau3, | |
Stacks::Tableau3 => Stacks::Tableau4, | |
Stacks::Tableau4 => Stacks::Tableau5, | |
Stacks::Tableau5 => Stacks::Tableau6, | |
Stacks::Tableau6 => Stacks::Tableau7, | |
Stacks::Tableau7 => Stacks::Stock, | |
} | |
} | |
fn previous(&self) -> Self { | |
match self { | |
Stacks::Stock => Stacks::Tableau7, | |
Stacks::Waste => Stacks::Stock, | |
Stacks::Foundation1 => Stacks::Waste, | |
Stacks::Foundation2 => Stacks::Foundation1, | |
Stacks::Foundation3 => Stacks::Foundation2, | |
Stacks::Foundation4 => Stacks::Foundation3, | |
Stacks::Tableau1 => Stacks::Foundation4, | |
Stacks::Tableau2 => Stacks::Tableau1, | |
Stacks::Tableau3 => Stacks::Tableau2, | |
Stacks::Tableau4 => Stacks::Tableau3, | |
Stacks::Tableau5 => Stacks::Tableau4, | |
Stacks::Tableau6 => Stacks::Tableau5, | |
Stacks::Tableau7 => Stacks::Tableau6, | |
} | |
} | |
} | |
const FOUNDATIONS: &[Stacks] = &[ | |
Stacks::Foundation1, | |
Stacks::Foundation2, | |
Stacks::Foundation3, | |
Stacks::Foundation4, | |
]; | |
const TABLEAUX: &[Stacks] = &[ | |
Stacks::Tableau1, | |
Stacks::Tableau2, | |
Stacks::Tableau3, | |
Stacks::Tableau4, | |
Stacks::Tableau5, | |
Stacks::Tableau6, | |
Stacks::Tableau7, | |
]; | |
enum StackType { | |
Stock, | |
Waste, | |
Foundation, | |
Tableau, | |
} | |
#[derive(Clone, Copy, Debug, Eq, Hash, IntoEnumIterator, Ord, PartialEq, PartialOrd)] | |
enum Suit { | |
Diamond, | |
Club, | |
Heart, | |
Spade, | |
} | |
const SUITS: &[Suit] = &[Suit::Diamond, Suit::Club, Suit::Heart, Suit::Spade]; | |
#[derive(Clone, Copy, Debug, Eq, Hash, IntoEnumIterator, Ord, PartialEq, PartialOrd)] | |
enum Rank { | |
Ace, | |
Two, | |
Three, | |
Four, | |
Five, | |
Six, | |
Seven, | |
Eight, | |
Nine, | |
Ten, | |
Jack, | |
Queen, | |
King, | |
} | |
impl From<Rank> for &'static str { | |
fn from(rank: Rank) -> Self { | |
let label = match rank { | |
Rank::Ace => "A", | |
Rank::Two => "2", | |
Rank::Three => "3", | |
Rank::Four => "4", | |
Rank::Five => "5", | |
Rank::Six => "6", | |
Rank::Seven => "7", | |
Rank::Eight => "8", | |
Rank::Nine => "9", | |
Rank::Ten => "T", | |
Rank::Jack => "J", | |
Rank::Queen => "Q", | |
Rank::King => "K", | |
}; | |
label | |
} | |
} | |
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] | |
struct Card { | |
suit: Suit, | |
rank: Rank, | |
face_up: bool, | |
} | |
pub struct ScreenSpace; | |
pub type ScreenPoint = Point2D<i32, ScreenSpace>; | |
pub type ScreenVector = Vector2D<i32, ScreenSpace>; | |
#[derive(Debug)] | |
enum FanDirection { | |
Down, | |
Right, | |
} | |
#[derive(Debug)] | |
enum StackDrawMode { | |
Squared, | |
Fanned(FanDirection, usize), | |
} | |
struct Stack { | |
stack_type: StackType, | |
position: ScreenPoint, | |
cards: Vec<Card>, | |
mode: StackDrawMode, | |
} | |
impl Stack { | |
pub fn get_card_position(&self, index: usize) -> ScreenPoint { | |
let vector = match &self.mode { | |
StackDrawMode::Squared => ScreenVector::zero(), | |
StackDrawMode::Fanned(direction, visible) => match direction { | |
FanDirection::Down => ScreenVector::new(0, MARGIN), | |
FanDirection::Right => ScreenVector::new(MARGIN, 0), | |
}, | |
}; | |
self.position + vector * index as i32 | |
} | |
pub fn get_top_card_position(&self) -> ScreenPoint { | |
let index = if self.cards.is_empty() { | |
0 | |
} else { | |
self.cards.len() - 1 | |
}; | |
self.get_card_position(index) | |
} | |
pub fn next_active_card(&self, index: usize) -> Option<usize> { | |
if self.cards.is_empty() { | |
return None; | |
} | |
let max_index = self.cards.len() - 1; | |
if index < max_index { | |
match self.stack_type { | |
StackType::Stock => Some(max_index), | |
_ => Some(index + 1), | |
} | |
} else { | |
None | |
} | |
} | |
fn draw_empty(&self, resources: &Resources) -> Result<(), Error> { | |
resources.empty.draw( | |
None, | |
None, | |
self.position.x, | |
self.position.y, | |
BitmapDrawMode::Copy, | |
BitmapFlip::Unflipped, | |
SCREEN_CLIP, | |
)?; | |
Ok(()) | |
} | |
fn draw_card_at( | |
card: &Card, | |
postion: &ScreenPoint, | |
resources: &Resources, | |
) -> Result<(), Error> { | |
let bitmap = if card.face_up { | |
if let Some(bitmap) = resources.card_bitmaps.get(&(card.suit, card.rank)) { | |
&bitmap | |
} else { | |
&resources.empty | |
} | |
} else { | |
&resources.back | |
}; | |
bitmap.draw( | |
None, | |
None, | |
postion.x, | |
postion.y, | |
BitmapDrawMode::Copy, | |
BitmapFlip::Unflipped, | |
SCREEN_CLIP, | |
)?; | |
Ok(()) | |
} | |
fn draw_squared(&self, resources: &Resources) -> Result<(), Error> { | |
let card = &self.cards[self.cards.len() - 1]; | |
let bitmap = if card.face_up { | |
resources | |
.card_bitmaps | |
.get(&(card.suit, card.rank)) | |
.unwrap_or(&resources.empty) | |
} else { | |
&resources.back | |
}; | |
bitmap.draw( | |
None, | |
None, | |
self.position.x, | |
self.position.y, | |
BitmapDrawMode::Copy, | |
BitmapFlip::Unflipped, | |
SCREEN_CLIP, | |
)?; | |
Ok(()) | |
} | |
fn draw_fanned( | |
&self, | |
resources: &Resources, | |
direction: &FanDirection, | |
visible: usize, | |
) -> Result<(), Error> { | |
let cards_to_draw = self.cards.len().min(visible); | |
let mut card_pos = self.position; | |
let fan_vector = match direction { | |
FanDirection::Down => ScreenVector::new(0, MARGIN), | |
FanDirection::Right => ScreenVector::new(MARGIN, 0), | |
}; | |
for card in &self.cards[0..cards_to_draw] { | |
Self::draw_card_at(card, &card_pos, resources)?; | |
card_pos += fan_vector; | |
} | |
Ok(()) | |
} | |
fn draw(&self, resources: &Resources) -> Result<(), Error> { | |
if self.cards.len() == 0 { | |
self.draw_empty(resources)?; | |
} else { | |
match &self.mode { | |
StackDrawMode::Squared => self.draw_squared(resources)?, | |
StackDrawMode::Fanned(direction, visible) => { | |
self.draw_fanned(resources, direction, *visible)? | |
} | |
} | |
} | |
Ok(()) | |
} | |
fn flip_top_card(&mut self) { | |
if !self.cards.is_empty() { | |
let index = self.cards.len() - 1; | |
let card = &mut self.cards[index]; | |
card.face_up = !card.face_up; | |
} | |
} | |
} | |
fn make_deck() -> Vec<Card> { | |
let mut rng = rand_pcg::Pcg32::seed_from_u64(321); | |
let mut cards: Vec<Card> = Suit::into_enum_iter() | |
.map(move |suit| { | |
Rank::into_enum_iter().map(move |rank| Card { | |
suit, | |
rank, | |
face_up: false, | |
}) | |
}) | |
.flatten() | |
.collect(); | |
cards.shuffle(&mut rng); | |
cards | |
} | |
struct Resources { | |
card_bitmaps: HashMap<(Suit, Rank), Bitmap>, | |
back: Bitmap, | |
empty: Bitmap, | |
graphics: Graphics, | |
font: Font, | |
point: Bitmap, | |
grab: Bitmap, | |
} | |
struct KlondikeGame { | |
stock: Stack, | |
waste: Stack, | |
foundations: Vec<Stack>, | |
tableaux: Vec<Stack>, | |
target: Stacks, | |
target_index: usize, | |
cards_table: BitmapTable, | |
resources: Resources, | |
crank_threshhold: i32, | |
} | |
impl KlondikeGame { | |
pub fn load_resources( | |
cards_table: &BitmapTable, | |
graphics: Graphics, | |
) -> Result<Resources, Error> { | |
let mut card_bitmaps = HashMap::new(); | |
for suit in Suit::into_enum_iter() { | |
let row = match suit { | |
Suit::Diamond => 2, | |
Suit::Heart => 1, | |
Suit::Spade => 3, | |
Suit::Club => 4, | |
}; | |
let mut col = 0; | |
for rank in Rank::into_enum_iter() { | |
let index = row * 13 + col; | |
log_to_console!("{:?} {:?} index {}", suit, rank, index); | |
let bitmap = cards_table.get_bitmap(index)?; | |
card_bitmaps.insert((suit, rank), bitmap); | |
col += 1; | |
} | |
} | |
let back = cards_table.get_bitmap(1)?; | |
let empty = cards_table.get_bitmap(0)?; | |
let font = graphics.load_font("assets/klondike/Oklahoma-Bold")?; | |
let grab = graphics.load_bitmap("assets/klondike/grab")?; | |
let point = graphics.load_bitmap("assets/klondike/point")?; | |
Ok(Resources { | |
card_bitmaps, | |
back, | |
empty, | |
graphics, | |
font, | |
grab, | |
point, | |
}) | |
} | |
pub fn new(playdate: &Playdate) -> Result<Box<Self>, Error> { | |
let mut cards = make_deck(); | |
let graphics = playdate.graphics(); | |
let cards_table = graphics.load_bitmap_table("assets/klondike/cards")?; | |
let foundation_gutter_count = (FOUNDATIONS.len() - 1) as i32; | |
let mut position = ScreenPoint::new( | |
SCREEN_WIDTH | |
- FOUNDATIONS.len() as i32 * 50 | |
- foundation_gutter_count * GUTTER | |
- MARGIN, | |
MARGIN, | |
); | |
let foundations: Vec<Stack> = FOUNDATIONS | |
.iter() | |
.map(|foundation| { | |
let stack = Stack { | |
stack_type: StackType::Foundation, | |
position, | |
cards: Vec::new(), | |
mode: StackDrawMode::Squared, | |
}; | |
position.x += CARD_WIDTH + GUTTER; | |
stack | |
}) | |
.collect(); | |
let mut position = ScreenPoint::new(MARGIN, MARGIN + CARD_HEIGHT + GUTTER); | |
let mut stack_count = 1; | |
let tableaux: Vec<Stack> = TABLEAUX | |
.iter() | |
.map(|tableau| { | |
let start = cards.len() - stack_count; | |
let mut stack = Stack { | |
stack_type: StackType::Tableau, | |
position, | |
cards: cards.split_off(start), | |
mode: StackDrawMode::Fanned(FanDirection::Down, 52), | |
}; | |
stack.flip_top_card(); | |
stack_count += 1; | |
position.x += 55; | |
stack | |
}) | |
.collect(); | |
let stock = Stack { | |
stack_type: StackType::Stock, | |
position: ScreenPoint::new(MARGIN, MARGIN), | |
cards: cards, | |
mode: StackDrawMode::Squared, | |
}; | |
let waste = Stack { | |
stack_type: StackType::Waste, | |
position: ScreenPoint::new(MARGIN + GUTTER + CARD_WIDTH, MARGIN), | |
cards: Vec::new(), | |
mode: StackDrawMode::Fanned(FanDirection::Right, 3), | |
}; | |
let resources = Self::load_resources(&cards_table, playdate.graphics())?; | |
let target_index = stock.next_active_card(0).unwrap_or(0); | |
Ok(Box::new(Self { | |
stock, | |
waste, | |
foundations, | |
tableaux, | |
target: Stacks::Stock, | |
target_index, | |
cards_table, | |
resources, | |
crank_threshhold: 0, | |
})) | |
} | |
fn get_stack(&self, stack_type: Stacks) -> &Stack { | |
match stack_type { | |
Stacks::Stock => &self.stock, | |
Stacks::Waste => &self.waste, | |
Stacks::Foundation1 => &self.foundations[0], | |
Stacks::Foundation2 => &self.foundations[1], | |
Stacks::Foundation3 => &self.foundations[2], | |
Stacks::Foundation4 => &self.foundations[3], | |
Stacks::Tableau1 => &self.tableaux[0], | |
Stacks::Tableau2 => &self.tableaux[1], | |
Stacks::Tableau3 => &self.tableaux[2], | |
Stacks::Tableau4 => &self.tableaux[3], | |
Stacks::Tableau5 => &self.tableaux[4], | |
Stacks::Tableau6 => &self.tableaux[5], | |
Stacks::Tableau7 => &self.tableaux[6], | |
} | |
} | |
fn deal_from_stock(&mut self) { | |
let amount_to_deal = 3.min(self.stock.cards.len()); | |
if amount_to_deal == 0 { | |
mem::swap(&mut self.waste.cards, &mut self.stock.cards); | |
for mut card in &mut self.stock.cards { | |
card.face_up = false; | |
} | |
} else { | |
let start = self.stock.cards.len() - amount_to_deal; | |
let mut dealt_cards = self.stock.cards.split_off(start); | |
for mut card in &mut dealt_cards { | |
card.face_up = true; | |
} | |
dealt_cards.append(&mut self.waste.cards); | |
self.waste.cards = dealt_cards; | |
} | |
} | |
fn check_crank(&mut self, playdate: &mut Playdate) -> Result<(), Error> { | |
let change = playdate.system().get_crank_change()? as i32; | |
self.crank_threshhold += change; | |
let target = self.get_stack(self.target); | |
if self.crank_threshhold > CRANK_THRESHHOLD { | |
if let Some(target_index) = target.next_active_card(self.target_index) { | |
self.target_index = target_index; | |
} else { | |
self.target = self.target.next(); | |
self.target_index = 0; | |
} | |
self.crank_threshhold = -CRANK_THRESHHOLD; | |
} else if self.crank_threshhold < -CRANK_THRESHHOLD { | |
self.target = self.target.previous(); | |
self.crank_threshhold = CRANK_THRESHHOLD; | |
} | |
Ok(()) | |
} | |
fn check_buttons(&mut self, playdate: &mut Playdate) -> Result<(), Error> { | |
let (_, pushed, _) = playdate.system().get_button_state()?; | |
if (pushed & PDButtons_kButtonA) != 0 || (pushed & PDButtons_kButtonB) != 0 { | |
match self.target { | |
Stacks::Stock => self.deal_from_stock(), | |
_ => (), | |
} | |
} | |
Ok(()) | |
} | |
} | |
impl Game for KlondikeGame { | |
fn update( | |
&mut self, | |
playdate: &mut playdate::Playdate, | |
) -> core::result::Result<(), anyhow::Error> { | |
self.check_crank(playdate)?; | |
self.check_buttons(playdate)?; | |
playdate.graphics().clear(SolidColor::White)?; | |
self.stock.draw(&self.resources)?; | |
self.waste.draw(&self.resources)?; | |
for stack in &self.foundations { | |
stack.draw(&self.resources)?; | |
} | |
for stack in &self.tableaux { | |
stack.draw(&self.resources)?; | |
} | |
let target = self.get_stack(self.target); | |
let position = target.get_card_position(self.target_index); | |
self.resources.point.draw( | |
None, | |
None, | |
position.x + CARD_WIDTH / 2, | |
position.y + CARD_HEIGHT / 2, | |
BitmapDrawMode::Copy, | |
BitmapFlip::Unflipped, | |
SCREEN_CLIP, | |
)?; | |
Ok(()) | |
} | |
} | |
#[cfg(not(test))] | |
playdate_game!(KlondikeGame); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment