Add some test comment, remove original draft comments.
// #[allow(dead_code, unused)]
// vigyazz6! autoplayer
//
// This is a program automagically playing a card game called "Vigyazz6!".
//
// The rules are rather simple, but I'mrather lazy to reprint it in detail,
// so it'll be brief.
//
// The code is based on a deck of 104 numbered cards, all of them having
// a face `value` and punishment `points` (the less the better).
// There are 2 to 10 players (but above 8 the code shall be modified to
// deal less cards), and the game has 5 rows of maximum 5 cards on the
// table.
// All players throw cards at once, and they are put in the row with
// closest less value; if there is none, they have to pick and take
// any row. When a row is full (5) they have to take the row and their
// card starts a new row head.
// When players are out of cards the least points collected wins.
//
// Important: I do not use `unsafe` code, this is all tightly controlled.
//
/// We use panic in several places where the code shall not go.
use core::panic;
/// `VecDeque` is a Vec which can be pushed/popped at both ends
/// `time::Instant` is used to measure running time.
/// `cmp::Reverse` used for reverse sorting.
use std::{collections::VecDeque, time::Instant, cmp::Reverse};
/// We use this `pretty_env_logger` as a logging framework.
extern crate pretty_env_logger;
#[macro_use] extern crate log;
// I import structures from my module files to use here.
use crate::{deck::Deck, player::{Player, PlayerCard}, table::Table};
// Here we read the files and "get to know" what's in them. This isn't including, only scanning.
mod deck;
mod table;
mod player;
mod card;
mod row;
// We play this many game rounds. You want to keep this under 3 for any debug level above "warnings".
const GAME_ROUNDS: i32 = 100_000;
fn main() {
// Env variable makes the logger to log more or less:
// RUST_LOG=debug cargo run
pretty_env_logger::init();
info!("Program starting."); // info level logging
game();
info!("End of run.");
}
/*** Game ****/
/// `GameStat` structure collects global statistics about the run.
struct GameStat {
game_count: i64,
shuffle_count: i64,
start_time: Instant, // use start_time.elapsed().as_nanos() or .as_secs()
}
/// Play all the game rounds.
fn game() {
// Whole game statistics stored here
let mut stats = GameStat { game_count:0, shuffle_count: 0, start_time: Instant::now() };
// Create the game deck with all the card (ordered). Open box. Remove shrink wrap. :-)
let mut deck = Deck::new();
// Create the players (up to 8 w/o changing the hand cards)
let player_names = vec![ "peter", "moni", "icbalint", "orsi", "topi", "kgb", "zsu", "csilla" ];
// Iterate on player names, create Player struct from them and collect them into a Vec'tor.
// (On the borrow level we don't consume the player_names vector [using `iter()` instead of `into_iter()`].)
let mut players: Vec<Player> = player_names.iter().map( |n| Player::new(n.to_string()) ).collect();
// We use this often, so take it now.
let player_count = players.len(); // = <number of players> - 1
// Start playing - GAME_ROUNDS rounds.
for cnt_game in 1..=GAME_ROUNDS {
debug!("Game round {} starts", cnt_game); // debug level logging
// Shuffle the deck (using various methods)
deck.shuffle();
stats.shuffle_count += 1;
stats.game_count += 1;
// dealing 10 cards for each player
debug!("Dealing.");
for _i in 1..=10 {
for player in 0 .. player_count {
players[player].get_card( deck.pop().expect("Deck is empty while dealing to players") );
}
}
// we need 5 cards from deck to build the rows
debug!("Building the rows.");
let mut cards = VecDeque::new();
// This is another way to write "for _ in (1..=5) { ... }"
(1..=5).for_each(|_| {
cards.push_back( deck.pop().expect("deck empty before starting the game") );
});
trace!("We pushed 5 cards to rows: {:?}\n", cards);
// Create the Table (contains the Deck, the Rows (with 5 starting `cards`) and occasional cards thrown by Players)
let mut table = Table::new(cards);
/* debug!("Table: {:?}\n", table);
debug!("Players: {:?}\n", players);
debug!("Deck: {:?}\n", deck);
*/
debug!("We have the table ready: {:?}", table);
// playing one game, players taking turns
debug!("Players start taking turns");
// Since players have 10 cards dealt we have exactly 10 turns.
for turn in 1..=10 {
debug!("Turn {}!", turn);
trace!("The table: {:?}", table);
// Everyone puts a card "face down" (Rust promised not to peek).
// If I used the `players.for_each()` form I would hit the borrow checker:
// `players` would be repeatedly borrowed mutable and program wouldn't compile.
for player in 0 .. player_count {
// get a card from the player
let player_id: i32 = player.try_into().unwrap(); // convert `usize` array index into `i32` [that was an unnecessary hassle: in fact I should rewrite this using usize everywhere]
let topcard = players[player].throw_card(); // If Player [module] was smart this would be a chosen card, but it would need player
// not just brains but possibility to peek at the rows. This wasn't in the plan, so
// we just take the top card in hand.
// put it on the table ("turned face down")
table.lay_player_card( topcard, player_id ); // We actually move the card variable with its ownership everywhere, so no &refs.
}
// Process cards on the Table.
debug!("The Table process the throws.");
// Sort the cards by `value` the players have thrown.
table.sort_cards();
// Pick them one by one, from smallest to larger ones
while table.has_player_cards() {
let smallest: PlayerCard = table.get_smallest_player_card(); // I just wrote the PlayerCard type to show; Rust is smart enough to know.
trace!("Take smallest card {:?}", &smallest);
// Find the row with the closest smallest card, or None if smaller than any row head.
let closest_row = table.get_closest_row(&smallest); // That is an Option<usize>.
trace!("Choose closest row: {:?}", closest_row);
match closest_row {
Some(rowid) => {
// We have to put it at the end of `rowid`.
debug!("Putting down card into row {}", rowid);
let player_id: usize = smallest.player_id.try_into().unwrap(); // By "just" indexing `players` (by `player_id`) I have avoided borrowing it and get into trouble later.
// Try to put the card to the end of the row, or collect full row first
let overflow = table.put_card_into_row(smallest, rowid); // And this is Option<VecDeque<Card>>.
if let Some(cards) = overflow {
// Row is full, we got pile ("bust")
debug!("Row is busted, {} collects", players[player_id].get_name());
// Player gets pile, thrown card was put into row head
players[ player_id ].give_pile( cards );
}
},
None => {
// Card too small, need to pick any row to take!
let player_id: usize = smallest.player_id.try_into().unwrap(); // We _directly_ access `PlayerCard.player_id` structure member, because borrow checker wasn't helping my mental hygiene.
debug!("Too small from {}, picking row", players[player_id].get_name());
// Pick any row to take
let rowid = players[ player_id ].pick_row_for_small_card(table.peek_rows(), &smallest.card);
trace!("Picked row {}", rowid);
// take the row cards ("bust")
let cards = table.take_row(rowid);
trace!("Took cards: {:?}", cards);
// and give them to the player who owns the card
players[ player_id ].give_pile( cards );
// put new card in the row
let overflow = table.put_card_into_row(smallest, rowid);
if let Some(_) = overflow {
// If this fires then I have fucked something up. :-)
panic!("Player took whole row and it's already full");
}
}
}
}
}
// end of round
info!("Round finished, len is {} ??sec", stats.start_time.elapsed().as_micros());
debug!("End of round, counting and collecting back piles");
// We collect the winner(s) and their score here
let mut winners: Vec<usize> = Vec::new();
let mut winscore: i32 = i32::MAX-1; // Getting facy here about "larger than any valid value".
for i in 0..player_count {
info!("Player {} has {} points", players[i].get_name(), players[i].get_points());
if players[i].get_points() < winscore {
trace!("New winner {} with score {}", players[i].get_name(), players[i].get_points());
winners.clear();
winners.push(i);
winscore = players[i].get_points();
} else if players[i].get_points() == winscore {
trace!("New co-winner {} with score {}", players[i].get_name(), players[i].get_points());
winners.push(i);
}
trace!("The list of winners is {:?}", winners);
// get pile from Player
let cards = players[i].get_pile();
// and give it back to the Deck
deck.push_cards(cards);
// close Player round and update stats
players[i].close_round();
}
// collect remaining Cards from Table
deck.push_cards( table.collect_rows() );
trace!("Shall have full deck now, len {}", deck.len());
// This is a Vec of `player_id`s, i is a &ref
for i in winners.iter() {
players[*i].inc_wins(); // Increment win count of &Vec<usize> player_id
}
// Collect the names of the winners.
let mut finals = Vec::new();
for i in winners {
finals.push( players[i].get_name() );
}
info!("The winner(s): {:?}", &finals);
}
// Do the type conversions _very_ visibly. `as_micros()` results `u128`
let elapsed_micro: f64 = stats.start_time.elapsed().as_micros() as f64;
// and `GAME_ROUNDS` is `i32`. We'll use `elapsed_micro/game_rounds` later.
let game_rounds: f64 = GAME_ROUNDS.into();
// ???which is same as the slightly uglier:
let _res: f64 = stats.start_time.elapsed().as_micros() as f64 / <i32 as Into<f64>>::into(GAME_ROUNDS);
println!("Totals (game time {} ??s, or {} s; {} ??s/game), {} games played ({} shuffles):",
stats.start_time.elapsed().as_micros(),
stats.start_time.elapsed().as_secs(),
elapsed_micro / game_rounds,
stats.game_count,
stats.shuffle_count,
);
// players.sort_by( |a, b| a.total_point.partial_cmp(&b.total_point).unwrap() ); // ASC points
// players.sort_by( |a, b| b.wins.partial_cmp(&a.wins).unwrap() ); // DESC wins
players.sort_by_cached_key( |x| Reverse(x.get_wins()) ); // DESC wins (caching is just for the show); using std::cmp::Reverse
// Print out totals
for i in 0..players.len() {
let p = &players[i]; // the rest looks simpler by using this. (Seems to confuse rust-analyzer in codium though.)
println!("Player {} has wins {}, score {} (busted {} times)",
p.get_name(),
p.get_wins(),
p.get_total_points(),
p.get_rows_busted());
}
}
// Tests. Try `cargo test`.
// They have criminally low coverage, mostly due to the simplicity of the code.
// Take it as some examples.
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use rand::Rng;
use crate::{card::Card, player::{Player, PlayerCard}, row::Row, table::Table};
#[test]
/// Test Card, and point generation logic.
fn card_values() {
let card_values = vec![1,2,5,10,33,55,77];
let card_points = vec![1,1,2,3, 5, 7, 5];
for i in 1 .. card_values.len() {
let c = Card::new( card_values[i] );
let p = c.points;
assert!(p == card_points[i], "card={} card points={} i={} expected point={}", card_values[i], p, i, card_points[i]);
}
}
#[test]
fn player_take_pile() {
// create a player
let mut p = Player::new("bob".to_string());
// create a pile
let mut pile = VecDeque::new();
let mut refpile = VecDeque::new();
for i in 5..10 {
let c = Card::new(i);
pile.push_back(c);
let c = Card::new(i);
refpile.push_back(c);
}
// give the pile to player
p.give_pile(pile);
assert!( p.get_rows_busted() == 1 );
// get back the pile from player
// p = p.gimme_pile();
let pile = p.get_pile();
// the pile we got shall be same as the pile we gave
// this check is O(n^2), doesn't matter for less than 100 items
assert_eq!( pile, refpile );
assert!( pile.iter().all( |item| refpile.contains(item)) );
let mut pile = VecDeque::new();
for i in 4..=9 {
let c = Card::new(i);
pile.push_back(c);
}
p.give_pile(pile);
assert!( p.get_rows_busted() == 2 );
}
#[test]
fn row_push() {
let mut row = Row::new();
let mut refcard = VecDeque::new(); // reference vec to check
for i in 1..=7 {
let cval = i+5;
let card = Card::new(cval);
// push a card into the row
if let Some(cards) = row.push_or_collect(card) {
// got the overflow
println!("Got overflow row at {}!", i);
assert!( i == 6, "Overflow at wrong position: {} != 6", i );
// we need to get the proper vec
assert!( cards.iter().all( |item| refcard.contains(item) ), "Got cards {:?}", cards );
} else {
println!("push success {}", i);
}
// remember the correct vec for checking
let card = Card::new(cval);
refcard.push_back(card);
// check card value
assert!( row.last_card_value() == cval, "Last card value mismatch: got {} vs expected {}", row.last_card_value(), cval );
}
assert!( row.len() == 2, "Row contains wrong amount of cards: {}", row.len() );
}
#[test]
fn sort_cards() {
let mut cards: VecDeque<Card> = VecDeque::new();
let mut rng = rand::thread_rng();
for _ in 1..50 {
let n = rng.gen_range(1..104);
cards.push_back( Card::new(n) );
}
cards.make_contiguous().sort();
for i in 1..cards.len() {
assert!( cards[i-1].value <= cards[i].value, "Bad ordering: {} > {}", cards[i-1].value,cards[i].value );
}
}
#[test]
fn check_closest_row() {
let table = generate_table();
let pcard = PlayerCard{ player_id: 42, card: Card::new(42) };
let closest = table.get_closest_row(&pcard);
assert_eq!( closest, Some(3) ); // index from 0
}
#[test]
fn check_smallest_player_card() {
let mut table = generate_table();
let mut player_id = 1;
vec![103, 8, 71, 93, 6].into_iter().for_each(|c| {
table.lay_player_card( Card::new(c), player_id);
player_id += 1;
});
let smallest = table.get_smallest_player_card();
assert_eq!( smallest, PlayerCard{ player_id: 5, card: Card::new(6) } );
}
fn generate_table() -> Table {
let mut row_cards = VecDeque::new();
vec![5,7,10,33,70].into_iter().for_each(|c| row_cards.push_back( Card::new(c) ));
let table = Table::new(row_cards);
table
}
}