Compare commits
12 Commits
main
...
basic-game
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
656f0cd74d | ||
|
|
804c632c10 | ||
|
|
976e9bf7b6 | ||
|
|
0d9480d113 | ||
|
|
9e5bb81f13 | ||
|
|
955e37cfb3 | ||
|
|
6d1653bb4e | ||
|
|
8541ee00ea | ||
|
|
0839d188ec | ||
|
|
33d86682be | ||
|
|
e327d61a18 | ||
|
|
15efa4e1b5 |
41
big-c.org
41
big-c.org
@@ -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
150
src/game/deck.rs
Normal 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
3
src/game/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod deck;
|
||||
pub mod mode;
|
||||
pub mod playerbuilder;
|
||||
53
src/game/mode.rs
Normal file
53
src/game/mode.rs
Normal 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
31
src/game/playerbuilder.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
56
src/main.rs
56
src/main.rs
@@ -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
63
src/modes/item.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user