Skip to content

Instantly share code, notes, and snippets.

@rtsuk
Last active May 10, 2024 06:22
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rtsuk/767f7d2c9b90c5e3e6b4ea90cfa91e7f to your computer and use it in GitHub Desktop.
Save rtsuk/767f7d2c9b90c5e3e6b4ea90cfa91e7f to your computer and use it in GitHub Desktop.
#![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