Contents:
As stated in the README, this is a simulation of the card game Crazy Eights written in Rust. The game is played with either a deck of cards, or two decks with more than five players. In either case, a deck consists of 52 cards with the jokers removed. A deck contains four suits: Hearts, Diamonds, Spades, and Clubs, which all contain the following values of cards:
| Value | Name |
|---|---|
| 1 | Ace |
| 2 | Two |
| 3 | Three |
| 4 | Four |
| 5 | Five |
| 6 | Six |
| 7 | Seven |
| 8 | Eight |
| 9 | Nine |
| 10 | Ten |
| 11 | Jack |
| 12 | Queen |
| 13 | King |
The deck(s) are shuffled before the game begins so that the cards are mostly out of order.
To start the game, each player is dealt five cards from the shuffled deck (for two players, seven cards are dealt) and the top card of the remaining deck is overturned and placed face-up next to the deck, which will become the discard pile. The player to the left of the dealer is the first to start playing.
The game is played in turns, where each player takes a turn in clockwise order. On their turn, a player will look for a card in their hand that matches either the suit or value of the top card in the discard pile. If a matching card is found, it is placed on top of the discard pile face-up and the next player starts their turn. If the player does not have a matching card in their hand, they take cards from the deck until they find a match. If the deck runs out of cards, the top card of the discard pile is put aside and the rest of the discard pile is shuffled to become the new deck. The player will keep drawing until they find a match or an eight. Eights of any suit can be played at any time on a player's turn. Playing an eight gives the player the option to change the suit in play. For example, if a player has no cards that match the top of the discard pile but their hand contains an eight, they can play the eight and change the suit to one that they have in their hand. The game is played until a player discards all of the cards in their hand and becomes the winner. In games of more than two people, the remaining players can decide to keep playing until another player discards their entire hand, claiming second place.
This program will simulate the Crazy Eights card game with all of the gameplay elements described above. The game's functions will be implemented, and the game will be fully playable with 2-10 players1. It is not currently within the scope of the project to include networking for multiple human players, but it may be added in the future. There will be automated players for cases where there are not enough human players for a game. The game will be accessible either through a CLI or GUI2, which will be decided once the core game functionality is completed. Because this program is written in Rust, it should be runnable on every platform Rust supports.
To enable multiplayer play across computers, the program should include some networking functionality. There are multiple ways this could be done. Below, I will explore a couple of approaches to game networking I've considered:
Peer-To-Peer (P2P): The program is capable of running a lightweight server along with a client. Each player's computer stores data about their game's state. This allows players to connect to each other's computers to play together. This approach requires no infrastructure since each player's computer is also a server, but it could make the program more intensive to run and it opens a possibility for cheating as players could modify the code to lie about their game state data.
Client-Server: The program can be run in client mode or server mode. Client mode is run by players looking to play the game by connecting to a computer running in server mode. A number of clients can connect to a given server, which facilitates play between players and stores game state data. This approach would require some server infrastructure, or at least one of the players would have to run the server on their computer. It would be easier to prevent cheating because the server is a central point of trust, but securing the server would open a new set of challenges.
Hybrid: P2P mode is enabled for LAN parties/local games and Client-Server mode is enabled for public games. This would allow players more flexibility through playing with their friends at home or on public servers with strangers in potentially different countries, like Minecraft and many other popular games. This approach would require me to implement both P2P and Client-Server functionality, but it would be the most flexible option.
I've decided that I would like the game to run P2P across networks. I don't want to invest in any infrastructure for this project, but I still want to play with friends who might be in different locations. I also think the idea of P2P stuff is very cool in how it's decentralized and anyone can download the game and play with anyone. It'll be more difficult to prevent cheating, but I'll figure it out. To actually implement it, I can reason about how to make a program P2P across one network for LAN parties, but I need to do some more research to make it work across the internet.
The program will be separated into various modules to divide the game into discrete parts. This should make development and maintenance easier, and the code will be more organized this way. The first modules to be defined are the card deck, the player structure, the game itself, and finally a module for defining error types. The main function will be used to launch the game and associated components. Functions shall be designed to do one thing such that complex tasks are broken down into procedures. Functions will aim to be relatively short (< 20 lines) to assist in this goal. Some things such as user input and display code will inherently take more lines than algorithmic code, so this rule may have some exceptions in the actual implementation. Named things shall be named according to their purpose, and units shall be appended to variable names where applicable. The goal of this is to create clear, readable code. The program is designed to be self-documenting, i.e. readable. This documentation exists to assist the developers in defining the program's design and to communicate the intended design to readers and potential contributors.
This section details each function, struct, and method contained within each module. It emphasizes implementation details such as the inner workings of functions and how modules and functions will relate to one another.
fn promp_user_for_number_of_players() -> i32: prompts the user for a number of players and returns the numberfn main(): callspompt_user_for_number_of_playersto get the number of players. Then, creates a deck withdeck::new_deck()and shuffles it.Game::new()is called to create a Game struct with the appropriate number of players andGame::initialize()is called to create the game's initial state, with errors being caught. ThenGame::play()is called to start the game.
-
pub enum Value: will contain the variants for each value Name in the Setup table -
pub enum Suit: will contain variants for each suit, namelyHearts,Diamonds,Spades, andClubs. -
pub struct Cardwill contain the following fields:pub value: Value: contains the value of the cardpub suit: Suit: contains the suit of the card
Implementations for Card:
pub fn is_similar(some: &Card, other: &Card) -> bool: returns true if either the Suit or Value fields ofsomematchother.pub fn print(&self): prints the value and suit of the card
Deck functions:
pub fn new() -> Vec<Self>: returns a Vec containing 52 Cards representing a standard card deck, henceforth referred to as a deckpub fn shuffle_discard_pile(deck: &mut Vec<Card>, discard_pile: &mut Vec<Card>): adds all of the cards except for the top card from the discard pile to the deck, then shuffles the deck.pub fn print_deck(&Vec<Card>): callsCard::printon the contents of the deck
-
pub struct Playerwill contain the following fields:pub name: String: the name of the Playerpub hand: Vec<Card>: a list of cards in the player's possession
Implementations for Player:
pub fn new(name: String, hand: Option<Vec<Card>>) -> Self: Given a name and optionally a hand, returns a new Player.fn draw_card(hand: &mut Vec<Card>, deck: &mut Vec<Card>): Given a list of cards (the deck), the player pops the top of the deck and adds it to their hand.fn get_playable_cards(hand: &Vec<Card>, top_card: Card, suit_in_play: &mut Suit) -> Vec<Card>: given a player's hand and the top card of the discard pile, returns a list of all the cards in the player's hand that can be played usingCard::is_similar. If the top card's suit differs from the suit in play, i.e. a crazy eight has changed the suit, a list of cards matching the suit in play is returned.fn prompt_user_for_card(cards: Vec<Card>) -> Card: prompts the user to choose a card to play from a list of possible cards and returns the chosen card.fn prompt_user_for_suit() -> Suit: prompts the user to choose a suit to change the discard pile to and returns the chosen suit.fn change_suit_in_play(mut old_suit: &Suit, new_suit: &Suit): assigns the value ofnew_suittoold_suit.fn play_card(hand: &mut Vec<Card>, discard_pile: &mut Vec<Card>, card: Card, suit_in_play: &mut Suit): Given the discard pile and a Card to be played, the card is added to the top of the discard pile and removed from the hand. The current suit in play is changed withchange_suit_in_play. If the card's value is an Eight, thenprompt_user_for_suitis called and the suit in play is changed appropriately.fn take_turn(hand: &mut Vec<Card>, deck: &mut Vec<Card>, discard_pile: &mut Vec<Card>, suit_in_play: &mut Suit): Given the deck and discard pile as parameters, callsget_playable_cardsto determine if the user can play any cards from their deck. If the list is empty,draw_cardis called until the list of playable cards is not empty. Once a card can be played,prompt_user_for_cardis called with a list of the possible cards. Once the user chooses a card,play_cardis called.
-
pub enum Gamewill contain the following variants:Running: contains the following fields:players: Vec<Player>: contains a list of each player and their handdeck: Vec<Card>: holds the cards in the play deck, to be dealt to each player and added to the discard pile.discard_pile: Vec<Card>: holds the cards in the discard pilesuit_in_play: str: contains a string of the current suit in play
Over: TODO: either contains no fields or contains the field of the winning player
Implementations for Game:
fn new(number_of_players: i32, deck: Vec<Card>, pile: Vec,Card>) -> mut Self: creates a new Game with theplayersfield initialized to thenumber_of_playersparameter. If the number of players is less than two or greater than ten, it panics.fn get_player_names(number_of_players: i32) -> Vec<String>: given the number of players in the game, prompts the user for a name for each player and returns the list of names.fn initialize_players(number_of_players: i32, names: Vec<String>) -> Vec<Player>: given the number of players and list of names, initializes the appropriate number of Players to the list of names with emptyhandfields and returns a list of the players.fn deal_cards(players: &mut Vec<Player>, deck: &mut Vec<Card>): given the size of theplayersVec, pops an appropriate amount of cards (see Setup) from thedeckinto each player's hand in alternating order.fn initialize_discard_pile(deck: &mut Vec<Card>, discard_pile: &mut Vec<Card>) -> Result<Card, DeckError>: adds the top card of the deck to the discard pile. Returns aDeckEmptyerror ifdeck.pop()returns None, and returns the top card otherwise.fn initialize(&mut self) -> Result<&mut Self, Box<dyn Error>>: If theselfreference is an instance ofGame::Running, callsdeal_cardswith the fields ofselfand callsinitialize_discard_pile, proaogating any errors to the caller.update_suit_in_playis then called with the suit of the card in the discard pile to create the game's initial state. If theselfreference is an instance ofGame::Over, aGameOvererror is returned.fn end_game(&mut self): setsselfto an instance ofGame::Over.fn play(&mut self): iterates through theplayersand callsplayer::take_turnon each player, passing the play deck and discard pile as parameters until the game ends, whereend_gameis called.
pub enum DeckErrorcontains the following variants:InvalidValue: returned byValue.try_from()InvalidSuit: returned bySuit.try_from()DeckEmpty: returned inGame::initialize_discard_pile()if the deck is empty.
pub enum GameErrorcontains the following variants:GameOver: returned byGame::initialize()when the game is over and nothing can be initialized.
- A tui will be added once the game functions (defined above) are written
- Once that is done, networking may be added to enable multiplayer features
Footnotes
-
This range was chosen because the game cannot be played with less than two players, and the game would be confusing and time-consuming with more than ten players. ↩
-
The GUI would likely use a library crate, with art and graphics designed by project contributors. A CLI will likely be written for development and testing, and the GUI will be created once the game is fully tested. ↩