Compare commits

12 Commits

Author SHA1 Message Date
Aryadev Chavali
656f0cd74d main: our first minigame
We deal out 13 cards to each player from a deck composed of two
playing card decks, then linear search for the first valid pair.
Since the two input hands should be sorted (~deal_tail~), the first
valid pair is also the lowest possible pair.

We compare the two, and whoever has the better pair wins!
2026-04-16 21:25:37 +01:00
Aryadev Chavali
804c632c10 game:mode: A simple enum to represent the different stages of a round
Specifically at the stage of the game where players may start
presenting Items (i.e. a round), this enum allows us to validate if an
Item should be played at that round stage: this is called a mode.
2026-04-16 21:15:29 +01:00
Aryadev Chavali
976e9bf7b6 modes:item: A general variant over all modes
A super simple variant type over all possible game modes currently
supported.  Allows us to take a sequence of cards and try to generate
a Hand of one of the modes based on it.  Also uses Result instead of
Option to give us a bit more detail on the type of errors we can get.
2026-04-16 21:11:26 +01:00
Aryadev Chavali
0d9480d113 modes:mod: docs for Hand trait 2026-04-16 21:10:47 +01:00
Aryadev Chavali
9e5bb81f13 game:deck: some docs 2026-04-16 21:10:32 +01:00
Aryadev Chavali
955e37cfb3 game:playerbuilder: First stage of game complete.
The first stage of the game is collecting the relevant players who
want to play.  We can't deal out any cards, nor do an election, till
this is done.

The way I've codified this is by having an opaque PlayerBuilder struct
with some helpers to add "new players" - essentially just empty decks
of cards.  Once we're done, we "freeze" or "fix" this set of players
by returning a Boxed array representing the contents.

Ridiculously simple API.
2026-04-16 21:06:56 +01:00
Aryadev Chavali
6d1653bb4e game:deck: Prefer Self over naming the type 2026-04-16 18:52:25 +01:00
Aryadev Chavali
8541ee00ea main: Simple 2 player initial deal
Construct a deck made up of two decks of cards, shuffle it, then deal
out 13 cards to each player.
2026-04-16 18:39:17 +01:00
Aryadev Chavali
0839d188ec game:deck: new module for deck management
- construct empty or decks composed of n "decks" of cards
- sort by standard ordering or suit ordering, and shuffle them using
  an RNG
- get, add, remove
- deal from the tail of a deck, or from any sequence of indices

This API will be the cornerstone of how games are managed.
2026-04-16 18:38:59 +01:00
Aryadev Chavali
33d86682be game: new module for game API 2026-04-16 18:36:18 +01:00
Aryadev Chavali
e327d61a18 helper: comment change 2026-04-16 18:35:59 +01:00
Aryadev Chavali
15efa4e1b5 big-c: update for new task 2026-04-16 12:06:16 +01:00
9 changed files with 393 additions and 13 deletions

View File

@@ -1,13 +1,30 @@
#+filetags: big-c
* WIP triples :modes:feat_triples:
Model the concept of a triple (three cards of similar rank).
** DONE Triple::new
** DONE Hand
** DONE Display
** DONE Ord
** DONE Hash
** WIP Testing
* TODO Basic Game mechanics :game:
Now that we have singles, pairs, and triples, let's implement a basic
2-player version of the game.
We'll ignore player election for now in favour of random ordering.
Here's the basic order of play:
1) Round chooser player plays a hand of whatever round type they want
2) Each player takes turns playing a hand of that round type better
than the previous hand
3) If either player skips, they pick up a card, and the other player
becomes the round chooser (allowing them to choose a new round
type)
4) If player (A) uses up all their cards, and the other player does
not footstool them, that player (A) wins.
We must also implement footstooling:
- If player (A) plays a hand, and the next player plays a hand that
half footstools it, then player (A) must pick up a card.
- If player (A) plays a hand, and the next player plays a hand that
full footstools it, then player (A) must pick up 2 cards.
** TODO Game structure and player structure
** TODO Round choice
** TODO Round gameplay
** TODO Win state
* Backlog :backlog:
** TODO Implement player and game structure
A game should have a table of players, a deck of unplayed cards, a
@@ -149,3 +166,11 @@ MVP goals:
It's really bloated - should probably be a subcrate.
*** DONE Split into module
*** DONE Testing
** DONE triples :modes:feat_triples:
Model the concept of a triple (three cards of similar rank).
*** DONE Triple::new
*** DONE Hand
*** DONE Display
*** DONE Ord
*** DONE Hash
*** DONE Testing

150
src/game/deck.rs Normal file
View File

@@ -0,0 +1,150 @@
use std::fmt::{Display, Formatter};
use crate::card::Card;
use rand::{seq::SliceRandom, Rng};
#[derive(Debug)]
/// A Deck of Cards - essentially a container of cards.
pub struct Deck(Vec<Card>);
#[derive(Debug, Clone, PartialEq, Eq)]
/// Reasons for why an operation may fail.
pub enum Reason {
NotSorted,
OutOfBounds(usize),
}
impl Deck {
pub fn new_empty() -> Self {
Self(Vec::new())
}
/// Create a new deck composed of `n` of decks of cards (using
/// `Card::iter_all`). Guaranteed to have 54`n` cards by construction.
pub fn new_full(n: usize) -> Self {
assert!(n > 0);
Self(Card::iter_all(n as i64).collect())
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn shuffle<T: Rng>(&mut self, rng: &mut T) {
self.0.shuffle(rng);
}
/// Sort cards within the deck by the ordering implementation for Cards.
/// See [[file:../card/ord.rs::impl Ord for Card {]].
pub fn sort(&mut self) {
self.0.sort();
}
/// Sort cards within the deck by suit in ascending order.
pub fn sort_by_suit(&mut self) {
// Sort by suit then rank
self.0.sort_by(|x, y| {
x.suit()
.cmp(&y.suit())
.then_with(|| x.rank().cmp(&y.rank()))
})
}
/// Add a set of cards to the end of `self`.
pub fn add(&mut self, cards: &[Card]) {
self.0.extend_from_slice(cards);
}
/// Get a set of cards at indices `indices` as a vector.
/// Returns Err if any index is out of bounds.
pub fn get(&self, indices: &[usize]) -> Result<Vec<Card>, Reason> {
let mut collector = Vec::with_capacity(indices.len());
for &ind in indices {
if ind >= self.len() {
return Err(Reason::OutOfBounds(ind));
}
collector.push(self.0[ind]);
}
Ok(collector)
}
/// Remove cards at indices `indices` from `self`. Order is not preserved.
/// Returns Err if `indices` is not sorted in ascending order or if any
/// index is out of bounds.
pub fn remove(&mut self, indices: &[usize]) -> Result<(), Reason> {
if !indices.is_sorted() {
Err(Reason::NotSorted)
} else if let Some(index) = indices.iter().find(|&&x| x >= self.len()) {
Err(Reason::OutOfBounds(*index))
} else {
for &index in indices.iter().rev() {
self.0.swap_remove(index);
}
Ok(())
}
}
/// Remove `n` cards from the end of `self`, and append them to the deck
/// `other`.
/// Returns Err if the number of cards requested exceed the size of the
/// current deck.
pub fn deal_tail(
&mut self,
other: &mut Self,
n: usize,
) -> Result<(), Reason> {
if n > self.0.len() {
Err(Reason::OutOfBounds(n))
} else {
let mut tail = self.0.split_off(self.len() - n);
tail.sort();
other.0.append(&mut tail);
Ok(())
}
}
/// Remove cards at indices `indices` from `self` and append them onto the
/// deck `other`. Order is not preserved.
/// Returns Err if `indices` are not sorted in ascending order or if any
/// indices are out of bounds.
pub fn deal_any(
&mut self,
other: &mut Self,
indices: &[usize],
) -> Result<(), Reason> {
let mut removed_cards = self.get(indices)?;
self.remove(indices)?;
other.0.append(&mut removed_cards);
Ok(())
}
/// Given two indices (`a` and `b`) in the deck, swap the cards in `self`.
/// Returns Err if either `a` or `b` are out of bounds.
pub fn swap(&mut self, a: usize, b: usize) -> Result<(), Reason> {
if a >= self.len() {
Err(Reason::OutOfBounds(a))
} else if b >= self.len() {
Err(Reason::OutOfBounds(b))
} else {
self.0.swap(a, b);
Ok(())
}
}
}
impl Display for Deck {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{{")?;
for (i, c) in self.0.iter().enumerate() {
write!(f, "{}", c)?;
if i < self.0.len() - 1 {
write!(f, ", ")?
}
}
write!(f, "}}")
}
}

3
src/game/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod deck;
pub mod mode;
pub mod playerbuilder;

53
src/game/mode.rs Normal file
View File

@@ -0,0 +1,53 @@
use crate::{
card::Card,
modes::item::{Item, ItemParseError},
};
#[derive(Debug, Clone, PartialEq, Eq)]
/// All possible stages of a Round where an Item may be presented by a player,
/// distinguished by Mode (Single, Pair, Triples, etc). `Any` represents the
/// "decision" stage where a single player may play any Item of any of the
/// available Round Modes to start off the new round.
pub enum Mode {
Any,
Single,
Pair,
Triple,
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// Types of errors which may occur when validating some set of cards against a
/// Mode.
pub enum ModeValidateError {
ParseError(ItemParseError),
ModeMismatch,
}
impl Mode {
/// Given mode `self`, validate if `cards` creates a valid Item for that
/// mode.
/// Returns `Ok(Item)` if so, otherwise return `Err(ModeValidateError)`.
pub fn validate_cards(
&self,
cards: &[Card],
) -> Result<Item, ModeValidateError> {
let item = Item::parse(cards).map_err(ModeValidateError::ParseError)?;
if *self == Mode::Any || *self == item.mode() {
Ok(item)
} else {
Err(ModeValidateError::ModeMismatch)
}
}
}
impl Item {
/// Return the Mode we'd expect this Item to be played in - trivial enum
/// conversion.
fn mode(&self) -> Mode {
match self {
Self::Single(_) => Mode::Single,
Self::Pair(_) => Mode::Pair,
Self::Triple(_) => Mode::Triple,
}
}
}

31
src/game/playerbuilder.rs Normal file
View File

@@ -0,0 +1,31 @@
use crate::game::deck::Deck;
/// Player Builder, which allows the adding of new players.
pub struct PlayerBuilder {
players: Vec<Deck>,
}
/// A fixed set of players that cannot grow - occurs once players have been
/// picked.
pub type FixedPlayers = Box<[Deck]>;
impl PlayerBuilder {
/// Construct a new player builder.
pub fn new() -> Self {
Self {
players: Vec::new(),
}
}
/// Add a new player with an empty deck to the builder, returning its ID.
pub fn add(&mut self) -> usize {
let id = self.players.len();
self.players.push(Deck::new_empty());
id
}
/// Fix the current number of players for later game stages.
pub fn into_fixed(self) -> FixedPlayers {
self.players.into_boxed_slice()
}
}

View File

@@ -1,4 +1,4 @@
/// Given an array of arguments, return them sorted. Best utilised with array
/// Given an array of items, return them sorted. Best utilised with array
/// destructuring.
pub fn ordered<T: Ord, const N: usize>(mut xs: [T; N]) -> [T; N] {
xs.sort();

View File

@@ -1,12 +1,64 @@
// permit dead code when not using clippy
#![cfg_attr(not(clippy), allow(dead_code))]
use crate::{
game::{deck::Deck, playerbuilder::PlayerBuilder},
modes::{item::Item, pair::Pair},
};
mod card;
mod exactsizearr;
mod game;
mod helper;
mod modes;
mod zipcartesian;
fn main() {
println!("Hello, world!");
fn find_first_pair(deck: &Deck) -> Pair {
for i in 0..deck.len() - 1 {
let cards = deck.get(&[i, i + 1]).unwrap();
match Item::parse(&cards) {
Ok(Item::Pair(pair)) => return pair,
_ => continue,
}
}
// FIXME: There is totally a way this happens, however unlikely. If we have
// no jokers and at most one instance of each rank. But I've got my fingers
// in my ears.
panic!("Shouldn't happen mate....");
}
fn main() {
let mut rng = rand::rng();
let (p1, p2, mut players) = {
let mut players = PlayerBuilder::new();
let p1 = players.add();
let p2 = players.add();
(p1, p2, players.into_fixed())
};
// Deal out some cards based for our two players.
{
let mut deck = Deck::new_full(2);
deck.shuffle(&mut rng);
// Since we know Deck::new_full(2) must have 108 cards, dealing 26 cards
// off the tail should be just fine.
deck.deal_tail(&mut players[p1], 13).unwrap();
deck.deal_tail(&mut players[p2], 13).unwrap();
};
// Let's try to parse a pair off player 1 and player 2.
let [p1pair, p2pair] = [p1, p2].map(|x| &players[x]).map(find_first_pair);
println!(
"{} vs {}\n{}",
p1pair,
p2pair,
match p1pair.cmp(&p2pair) {
std::cmp::Ordering::Less => "p2 won!",
std::cmp::Ordering::Greater => "p1 won!",
std::cmp::Ordering::Equal => "p1 and p2 lost",
}
);
println!("{}\n{}", players[p1], players[p2]);
}

63
src/modes/item.rs Normal file
View File

@@ -0,0 +1,63 @@
use crate::{
card::Card,
modes::{pair::Pair, single::Single, triple::Triple},
};
#[derive(Debug, Clone, PartialEq, Eq)]
/// An Item is a validated cardset, variant over all different modes.
pub enum Item {
Single(Single),
Pair(Pair),
Triple(Triple),
}
#[derive(Debug, Clone, PartialEq, Eq)]
/// Possible errors from attempting to generate a new Item from a set of cards.
pub enum ItemParseError {
InvalidSingle(Card),
InvalidPair(Card, Card),
InvalidTriple(Card, Card, Card),
InvalidArity,
}
impl Item {
pub fn parse(cards: &[Card]) -> Result<Item, ItemParseError> {
match cards {
[a] => Single::new(*a)
.map(Self::Single)
.ok_or(ItemParseError::InvalidSingle(*a)),
[a, b] => Pair::new(*a, *b)
.map(Self::Pair)
.ok_or(ItemParseError::InvalidPair(*a, *b)),
[a, b, c] => Triple::new(*a, *b, *c)
.map(Self::Triple)
.ok_or(ItemParseError::InvalidTriple(*a, *b, *c)),
_ => Err(ItemParseError::InvalidArity),
}
}
}
use std::fmt::{self, Display, Formatter};
impl Display for ItemParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidSingle(a) => write!(f, "invalid single {}", a),
Self::InvalidPair(a, b) => write!(f, "invalid pair {} + {}", a, b),
Self::InvalidTriple(a, b, c) => {
write!(f, "invalid triple {} + {} + {}", a, b, c)
}
Self::InvalidArity => write!(f, "invalid arity"),
}
}
}
impl Display for Item {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Single(x) => write!(f, "{}", x),
Self::Pair(x) => write!(f, "{}", x),
Self::Triple(x) => write!(f, "{}", x),
}
}
}

View File

@@ -1,5 +1,6 @@
use crate::card::Card;
pub mod item;
pub mod pair;
pub mod single;
pub mod triple;
@@ -12,16 +13,18 @@ pub enum Footstool {
}
pub trait Hand: Ord {
// Only need to implement is_proper.
/// Return true if the current hand doesn't have any jokers.
fn is_proper(&self) -> bool;
/// Return true if the current hand has at least one Joker.
fn is_improper(&self) -> bool {
!self.is_proper()
}
/// Get the high card of the current hand.
fn high_card(&self) -> Card;
/// Given two instances of a Hand (`self` and `other`), verify if `self`
/// Given two instances of a Hand (`self` and `other`), return if `self`
/// footstools `other`.
fn footstool(&self, other: &Self) -> Footstool;
}