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
|
#+filetags: big-c
|
||||||
|
|
||||||
* WIP triples :modes:feat_triples:
|
* TODO Basic Game mechanics :game:
|
||||||
Model the concept of a triple (three cards of similar rank).
|
Now that we have singles, pairs, and triples, let's implement a basic
|
||||||
** DONE Triple::new
|
2-player version of the game.
|
||||||
** DONE Hand
|
|
||||||
** DONE Display
|
We'll ignore player election for now in favour of random ordering.
|
||||||
** DONE Ord
|
|
||||||
** DONE Hash
|
Here's the basic order of play:
|
||||||
** WIP Testing
|
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:
|
* Backlog :backlog:
|
||||||
** TODO Implement player and game structure
|
** TODO Implement player and game structure
|
||||||
A game should have a table of players, a deck of unplayed cards, a
|
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.
|
It's really bloated - should probably be a subcrate.
|
||||||
*** DONE Split into module
|
*** DONE Split into module
|
||||||
*** DONE Testing
|
*** 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.
|
/// destructuring.
|
||||||
pub fn ordered<T: Ord, const N: usize>(mut xs: [T; N]) -> [T; N] {
|
pub fn ordered<T: Ord, const N: usize>(mut xs: [T; N]) -> [T; N] {
|
||||||
xs.sort();
|
xs.sort();
|
||||||
|
|||||||
56
src/main.rs
56
src/main.rs
@@ -1,12 +1,64 @@
|
|||||||
// permit dead code when not using clippy
|
// permit dead code when not using clippy
|
||||||
#![cfg_attr(not(clippy), allow(dead_code))]
|
#![cfg_attr(not(clippy), allow(dead_code))]
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
game::{deck::Deck, playerbuilder::PlayerBuilder},
|
||||||
|
modes::{item::Item, pair::Pair},
|
||||||
|
};
|
||||||
|
|
||||||
mod card;
|
mod card;
|
||||||
mod exactsizearr;
|
mod exactsizearr;
|
||||||
|
mod game;
|
||||||
mod helper;
|
mod helper;
|
||||||
mod modes;
|
mod modes;
|
||||||
mod zipcartesian;
|
mod zipcartesian;
|
||||||
|
|
||||||
fn main() {
|
fn find_first_pair(deck: &Deck) -> Pair {
|
||||||
println!("Hello, world!");
|
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;
|
use crate::card::Card;
|
||||||
|
|
||||||
|
pub mod item;
|
||||||
pub mod pair;
|
pub mod pair;
|
||||||
pub mod single;
|
pub mod single;
|
||||||
pub mod triple;
|
pub mod triple;
|
||||||
@@ -12,16 +13,18 @@ pub enum Footstool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait Hand: Ord {
|
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;
|
fn is_proper(&self) -> bool;
|
||||||
|
|
||||||
|
/// Return true if the current hand has at least one Joker.
|
||||||
fn is_improper(&self) -> bool {
|
fn is_improper(&self) -> bool {
|
||||||
!self.is_proper()
|
!self.is_proper()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the high card of the current hand.
|
||||||
fn high_card(&self) -> Card;
|
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`.
|
/// footstools `other`.
|
||||||
fn footstool(&self, other: &Self) -> Footstool;
|
fn footstool(&self, other: &Self) -> Footstool;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user