| @@ -23,4 +23,5 @@ blackjack_SOURCES = \ | |||
| src/blackjack.cpp \ | |||
| src/cards.cpp \ | |||
| src/stdinout.cpp \ | |||
| src/tty.cpp | |||
| src/tty.cpp \ | |||
| src/internal.cpp | |||
| @@ -1,14 +1,10 @@ | |||
| * handle no readline | |||
| * named pipes (a.k.a FIFOs) | |||
| * eV = expected value = return | |||
| * check that the card distribution is uniform | |||
| * initial bankroll | |||
| * report | |||
| * as a file | |||
| * format (yaml, json, markdown table) | |||
| * size (extra small, small, medium, large, extra large) | |||
| * flat_bet implies =1 | |||
| * parse 1e5 in hands | |||
| * verbosity (extra small, small, medium, large, extra large) | |||
| * trap ctrl+c and write report anyway (not sure how to pass arguments to signal handler) | |||
| * flag to see if a conf string was used or not | |||
| * to_string() for floats | |||
| @@ -228,10 +228,11 @@ class Player { | |||
| }; | |||
| struct reportItem { | |||
| reportItem(std::string k, std::string f, double v) : key(k), format(f), value(v) {}; | |||
| reportItem(int l, std::string k, std::string f, double v) : key(k), format(f), value(v), level(l) {}; | |||
| std::string key; | |||
| std::string format; | |||
| double value; | |||
| int level; | |||
| }; | |||
| class Dealer { | |||
| @@ -274,7 +275,6 @@ class Dealer { | |||
| void reportPrepare(void); | |||
| int writeReportYAML(void); | |||
| protected: | |||
| // TODO: multiple players | |||
| Player *player; | |||
| @@ -284,9 +284,13 @@ class Dealer { | |||
| // std::list <Hand> hands; | |||
| Hand hand; | |||
| double error_standard_deviations = 1.0; | |||
| int n_decks = -1; | |||
| unsigned long int n_hands = 0; | |||
| // how many standard deviations does the reported error mean? | |||
| double error_standard_deviations = 3.0; | |||
| // default infinite number of decks (it's faster) | |||
| int n_decks = -1; | |||
| // default one million hands | |||
| unsigned long int n_hands = 1000000; | |||
| unsigned long int n_hand = 0; | |||
| struct { | |||
| @@ -294,8 +298,8 @@ class Dealer { | |||
| std::list<PlayerHand>::iterator currentHand; | |||
| unsigned int splits = 0; | |||
| // unsigned int currentBet = 0; | |||
| // TODO: separate handsDealt from handsPlayed | |||
| unsigned int n_hands = 0; // this is different from the dealer's due to splitting | |||
| unsigned int handsInsured = 0; | |||
| @@ -313,22 +317,28 @@ class Dealer { | |||
| unsigned int pushes = 0; | |||
| unsigned int losses = 0; | |||
| // TODO: blackjack_pushes? | |||
| // TODO: blackjack_pushes? | |||
| double bankroll = 0; | |||
| double worstBankroll = 0; | |||
| double totalMoneyWaged = 0; | |||
| double result = 0; | |||
| // these variables are used to compute the running mean and variance | |||
| double currentOutcome = 0; | |||
| double mean = 0; | |||
| double M2 = 0; | |||
| double variance = 0; | |||
| } playerStats; | |||
| void updateMeanAndVariance(void); | |||
| private: | |||
| bool done = false; | |||
| std::list<reportItem> report; | |||
| int reportVerbosity = 3; | |||
| }; | |||
| template <typename ... Args> std::string string_format( const std::string& format, Args ... args); | |||
| #endif | |||
| @@ -92,18 +92,12 @@ void Blackjack::deal(void) { | |||
| return; | |||
| } | |||
| // update the uncertainty (knuth citing welford) | |||
| // The Art of Computer Programming, volume 2: Seminumerical Algorithms, 3rd edn., p. 232 | |||
| // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm | |||
| if (n_hand != 0) { | |||
| double delta = playerStats.result - playerStats.mean; | |||
| playerStats.mean += delta / (double)(n_hand); | |||
| playerStats.M2 += delta * (playerStats.result - playerStats.mean); | |||
| playerStats.variance = playerStats.M2 / (double)(n_hand); | |||
| updateMeanAndVariance(); | |||
| } | |||
| i_arranged_cards = 0; | |||
| playerStats.currentOutcome = 0; | |||
| n_hand++; | |||
| // clear dealer's hand | |||
| @@ -229,7 +223,7 @@ void Blackjack::deal(void) { | |||
| // pay him (her) | |||
| playerStats.bankroll += (1.0 + 0.5) * playerStats.currentHand->bet; | |||
| playerStats.result += playerStats.currentHand->bet; | |||
| playerStats.currentOutcome += playerStats.currentHand->bet; | |||
| info(Libreblackjack::Info::PlayerWinsInsurance, 1e3*playerStats.currentHand->bet); | |||
| playerStats.winsInsured++; | |||
| @@ -247,7 +241,7 @@ void Blackjack::deal(void) { | |||
| } else { | |||
| playerStats.result -= playerStats.currentHand->bet; | |||
| playerStats.currentOutcome -= playerStats.currentHand->bet; | |||
| info(Libreblackjack::Info::PlayerLosses, 1e3*playerStats.currentHand->bet); | |||
| playerStats.losses++; | |||
| @@ -261,7 +255,7 @@ void Blackjack::deal(void) { | |||
| // pay him (her) | |||
| playerStats.bankroll += (1.0 + blackjack_pays) * playerStats.currentHand->bet; | |||
| playerStats.result += blackjack_pays * playerStats.currentHand->bet; | |||
| playerStats.currentOutcome += blackjack_pays * playerStats.currentHand->bet; | |||
| info(Libreblackjack::Info::PlayerWins, 1e3 * blackjack_pays*playerStats.currentHand->bet); | |||
| playerStats.blackjacksPlayer++; | |||
| @@ -350,7 +344,7 @@ void Blackjack::deal(void) { | |||
| if (playerHand.busted() == false) { | |||
| // pay him (her) | |||
| playerStats.bankroll += 2 * playerHand.bet; | |||
| playerStats.result += playerHand.bet; | |||
| playerStats.currentOutcome += playerHand.bet; | |||
| info(Libreblackjack::Info::PlayerWins, 1e3*playerHand.bet); | |||
| playerStats.wins++; | |||
| @@ -364,7 +358,7 @@ void Blackjack::deal(void) { | |||
| if (std::abs(player->dealerValue) > std::abs(player->playerValue)) { | |||
| playerStats.result -= playerHand.bet; | |||
| playerStats.currentOutcome -= playerHand.bet; | |||
| info(Libreblackjack::Info::PlayerLosses, 1e3*playerHand.bet, player->playerValue); | |||
| playerStats.losses++; | |||
| @@ -379,7 +373,7 @@ void Blackjack::deal(void) { | |||
| // pay him (her) | |||
| playerStats.bankroll += 2 * playerHand.bet; | |||
| playerStats.result += playerHand.bet; | |||
| playerStats.currentOutcome += playerHand.bet; | |||
| info(Libreblackjack::Info::PlayerWins, 1e3*playerHand.bet, player->playerValue); | |||
| playerStats.wins++; | |||
| playerStats.winsDoubled += playerHand.doubled; | |||
| @@ -469,6 +463,7 @@ int Blackjack::process(void) { | |||
| if (playerStats.bankroll < playerStats.worstBankroll) { | |||
| playerStats.worstBankroll = playerStats.bankroll; | |||
| } | |||
| playerStats.totalMoneyWaged += playerStats.currentHand->bet; | |||
| nextAction = Libreblackjack::DealerAction::DealPlayerFirstCard; | |||
| return 1; | |||
| @@ -541,7 +536,7 @@ int Blackjack::process(void) { | |||
| if (playerStats.currentHand->busted()) { | |||
| info(Libreblackjack::Info::PlayerLosses, 1e3*playerStats.currentHand->bet, player->playerValue); | |||
| playerStats.result -= playerStats.currentHand->bet; | |||
| playerStats.currentOutcome -= playerStats.currentHand->bet; | |||
| playerStats.bustsPlayer++; | |||
| playerStats.losses++; | |||
| } | |||
| @@ -661,7 +656,7 @@ int Blackjack::process(void) { | |||
| if (playerStats.currentHand->busted()) { | |||
| playerStats.result -= playerStats.currentHand->bet; | |||
| playerStats.currentOutcome -= playerStats.currentHand->bet; | |||
| info(Libreblackjack::Info::PlayerLosses, 1e3*playerStats.currentHand->bet); | |||
| playerStats.bustsPlayer++; | |||
| playerStats.losses++; | |||
| @@ -96,7 +96,7 @@ Configuration::Configuration(int argc, char **argv) { | |||
| break; | |||
| case 'f': | |||
| if (optarg != NULL) { | |||
| data["flat_bet"] = optarg; | |||
| data["flat_bet"] = optarg; | |||
| } else { | |||
| data["flat_bet"] = "yes"; | |||
| } | |||
| @@ -151,8 +151,7 @@ Configuration::Configuration(int argc, char **argv) { | |||
| set(&error_standard_deviations, {"error_standard_deviations"}); | |||
| set(yaml_report_path, {"yaml_report", "yaml_report_path"}); | |||
| return; | |||
| } | |||
| @@ -234,7 +233,7 @@ bool Configuration::set(unsigned int *value, std::list<std::string> key) { | |||
| bool Configuration::set(unsigned long int *value, std::list<std::string> key) { | |||
| for (auto it : key) { | |||
| if (exists(*(&it))) { | |||
| *value = std::stoi(data[*(&it)]); | |||
| *value = (unsigned long int)std::stod(data[*(&it)]); | |||
| return true; | |||
| } | |||
| } | |||
| @@ -0,0 +1,58 @@ | |||
| /*------------ -------------- -------- --- ----- --- -- - - | |||
| * Libre Blackjack - tty interactive player | |||
| * | |||
| * Copyright (C) 2020 jeremy theler | |||
| * | |||
| * This file is part of Libre Blackjack. | |||
| * | |||
| * Libre Blackjack is free software: you can redistribute it and/or modify | |||
| * it under the terms of the GNU General Public License as published by | |||
| * the Free Software Foundation, either version 3 of the License, or | |||
| * (at your option) any later version. | |||
| * | |||
| * Libre Blackjack is distributed in the hope that it will be useful, | |||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| * GNU General Public License for more details. | |||
| * | |||
| * You should have received a copy of the GNU General Public License | |||
| * along with Libre Blackjack. If not, see <http://www.gnu.org/licenses/>. | |||
| *------------------- ------------ ---- -------- -- - - - | |||
| */ | |||
| #include "conf.h" | |||
| #include "blackjack.h" | |||
| #include "internal.h" | |||
| Internal::Internal(Configuration &conf) { | |||
| return; | |||
| } | |||
| void Internal::info(Libreblackjack::Info msg, int p1, int p2) { | |||
| return; | |||
| } | |||
| int Internal::play() { | |||
| switch (actionRequired) { | |||
| case Libreblackjack::PlayerActionRequired::Bet: | |||
| currentBet = 1; | |||
| actionTaken = Libreblackjack::PlayerActionTaken::Bet; | |||
| break; | |||
| case Libreblackjack::PlayerActionRequired::Insurance: | |||
| actionTaken = Libreblackjack::PlayerActionTaken::DontInsure; | |||
| break; | |||
| case Libreblackjack::PlayerActionRequired::Play: | |||
| actionTaken = (playerValue < 12) ? Libreblackjack::PlayerActionTaken::Hit : Libreblackjack::PlayerActionTaken::Stand; | |||
| break; | |||
| case Libreblackjack::PlayerActionRequired::None: | |||
| break; | |||
| } | |||
| return 0; | |||
| } | |||
| @@ -0,0 +1,39 @@ | |||
| /*------------ -------------- -------- --- ----- --- -- - - | |||
| * Libre Blackjack - internal automatic player | |||
| * | |||
| * Copyright (C) 2020 jeremy theler | |||
| * | |||
| * This file is part of Libre Blackjack. | |||
| * | |||
| * Libre Blackjack is free software: you can redistribute it and/or modify | |||
| * it under the terms of the GNU General Public License as published by | |||
| * the Free Software Foundation, either version 3 of the License, or | |||
| * (at your option) any later version. | |||
| * | |||
| * Libre Blackjack is distributed in the hope that it will be useful, | |||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| * GNU General Public License for more details. | |||
| * | |||
| * You should have received a copy of the GNU General Public License | |||
| * along with Libre Blackjack. If not, see <http://www.gnu.org/licenses/>. | |||
| *------------------- ------------ ---- -------- -- - - - | |||
| */ | |||
| #ifndef INTERNAL_H | |||
| #define INTERNAL_H | |||
| #include "blackjack.h" | |||
| class Internal : public Player { | |||
| public: | |||
| Internal(Configuration &); | |||
| ~Internal() { }; | |||
| int play(void) override; | |||
| void info(Libreblackjack::Info = Libreblackjack::Info::None, int = 0, int = 0) override; | |||
| private: | |||
| }; | |||
| #endif | |||
| @@ -27,6 +27,7 @@ | |||
| #include "blackjack.h" | |||
| #include "tty.h" | |||
| #include "stdinout.h" | |||
| #include "internal.h" | |||
| int main(int argc, char **argv) { | |||
| @@ -59,8 +60,8 @@ int main(int argc, char **argv) { | |||
| player = new Tty(conf); | |||
| } else if (conf.getPlayerName() == "stdinout" || conf.getPlayerName() == "stdio") { | |||
| player = new StdInOut(conf); | |||
| // TODO: player strategy from file | |||
| } else if (conf.getPlayerName() == "internal") { | |||
| player = new Internal(conf); | |||
| } else { | |||
| std::cerr << "Unknown player '" << conf.getPlayerName() <<".'" << std::endl; | |||
| return -1; | |||
| @@ -1,33 +1,64 @@ | |||
| #include <iostream> | |||
| // #include <sys/time.h> | |||
| // #include <sys/resource.h> | |||
| #include <cmath> | |||
| #include <memory> | |||
| #include <string> | |||
| #include "base.h" | |||
| // https://stackoverflow.com/questions/2342162/stdstring-formatting-like-sprintf | |||
| template<typename ... Args> | |||
| std::string string_format( const std::string& format, Args ... args) | |||
| { | |||
| size_t size = snprintf(nullptr, 0, format.c_str(), args ...) + 1; // Extra space for '\0' | |||
| if (size <= 0) { | |||
| return std::string(""); | |||
| } | |||
| std::unique_ptr<char[]> buf(new char[size]); | |||
| snprintf(buf.get(), size, format.c_str(), args ...); | |||
| return std::string(buf.get(), buf.get() + size - 1); // We don't want the '\0' inside | |||
| } | |||
| void Dealer::updateMeanAndVariance(void) { | |||
| double delta = playerStats.currentOutcome - playerStats.mean; | |||
| playerStats.mean += delta / (double)(n_hand); | |||
| playerStats.M2 += delta * (playerStats.currentOutcome - playerStats.mean); | |||
| playerStats.variance = playerStats.M2 / (double)(n_hand); | |||
| return; | |||
| } | |||
| void Dealer::reportPrepare(void) { | |||
| double ev = (double) playerStats.result / (double) n_hand; | |||
| double error = error_standard_deviations * sqrt (playerStats.variance / (double) n_hand); | |||
| report.push_back(reportItem("bankroll", "%g", playerStats.bankroll)); | |||
| report.push_back(reportItem("result", "%g", playerStats.result)); | |||
| report.push_back(reportItem("ev", "%g", ev)); | |||
| report.push_back(reportItem("hands", "%g", n_hand)); | |||
| report.push_back(reportItem("variance", "%g", playerStats.variance)); | |||
| report.push_back(reportItem("deviation", "%g", sqrt(playerStats.variance))); | |||
| report.push_back(reportItem("error", "%g", error)); | |||
| // we need to update these statistics after the last played hand | |||
| updateMeanAndVariance(); | |||
| report.push_back(reportItem("played_hands", "%g", n_hands)); | |||
| report.push_back(reportItem("total_money_waged", "%g", playerStats.totalMoneyWaged)); | |||
| report.push_back(reportItem("bustsPlayer", "%g", playerStats.bustsPlayer)); | |||
| report.push_back(reportItem("bustsDealer", "%g", playerStats.bustsDealer)); | |||
| double error = error_standard_deviations * sqrt (playerStats.variance / (double) n_hand); | |||
| report.push_back(reportItem("wins", "%g", playerStats.wins)); | |||
| report.push_back(reportItem("pushes", "%g", playerStats.pushes)); | |||
| report.push_back(reportItem("losses", "%g", playerStats.losses)); | |||
| int precision = (int) (std::ceil(-std::log10(error))) - 2; | |||
| if (precision < 0) { | |||
| precision = 0; | |||
| } | |||
| std::string format = "\"(%+." + std::to_string(precision) + "f ± %." + std::to_string(precision) + "f) %%%%\""; | |||
| report.push_back(reportItem(1, "result", string_format(format, 100*playerStats.mean, 100*error), 0.0)); | |||
| report.push_back(reportItem(2, "mean", "%g", playerStats.mean)); | |||
| report.push_back(reportItem(2, "error", "%g", error)); | |||
| report.push_back(reportItem(2, "hands", "%g", n_hand)); | |||
| report.push_back(reportItem(2, "bankroll", "%g", playerStats.bankroll)); | |||
| report.push_back(reportItem(3, "bustsPlayer", "%g", playerStats.bustsPlayer / (double) n_hand)); | |||
| report.push_back(reportItem(3, "bustsDealer", "%g", playerStats.bustsDealer / (double) n_hand)); | |||
| report.push_back(reportItem(3, "wins", "%g", playerStats.wins / (double) n_hand)); | |||
| report.push_back(reportItem(3, "pushes", "%g", playerStats.pushes / (double) n_hand)); | |||
| report.push_back(reportItem(3, "losses", "%g", playerStats.losses / (double) n_hand)); | |||
| report.push_back(reportItem(4, "total_money_waged", "%g", playerStats.totalMoneyWaged)); | |||
| report.push_back(reportItem(5, "variance", "%g", playerStats.variance)); | |||
| report.push_back(reportItem(5, "deviation", "%g", sqrt(playerStats.variance))); | |||
| return; | |||
| } | |||
| @@ -44,11 +75,14 @@ int Dealer::writeReportYAML(void) { | |||
| // } | |||
| // TODO: choose if comments with explanations are to be added | |||
| // TODO: choose verbosity level | |||
| std::cerr << "---" << std::endl; | |||
| for (auto item : report) { | |||
| std::cerr << item.key << ": "; | |||
| fprintf(stderr, item.format.c_str(), item.value); | |||
| std::cerr << std::endl; | |||
| if (item.level <= reportVerbosity) { | |||
| std::cerr << item.key << ": "; | |||
| fprintf(stderr, item.format.c_str(), item.value); | |||
| std::cerr << std::endl; | |||
| } | |||
| } | |||
| // std::cerr << "rules:" << std::endl; | |||
| @@ -21,7 +21,6 @@ | |||
| */ | |||
| #include <iostream> | |||
| #include <cstring> | |||
| #include <thread> | |||
| #include <chrono> | |||
| @@ -106,9 +105,8 @@ void Tty::info(Libreblackjack::Info msg, int p1, int p2) { | |||
| break; | |||
| case Libreblackjack::Info::NewHand: | |||
| // s = "new_hand"; | |||
| std::cout << std::endl; | |||
| s = "Starting new hand #" + std::to_string(p1) + " with bankroll " + std::to_string(1e-3*p2); | |||
| s = "Starting new hand #" + std::to_string(p1) + " with bankroll " + string_format("%g", 1e-3*p2, 0.0); | |||
| // clear dealer's hand | |||
| dealerHand.cards.clear(); | |||
| @@ -152,7 +150,6 @@ void Tty::info(Libreblackjack::Info msg, int p1, int p2) { | |||
| s = "Dealer's up card is " + card[p1].utf8(); | |||
| break; | |||
| default: | |||
| // s = "card_dealer"; | |||
| s = "Dealer's card is " + card[p1].utf8(); | |||
| break; | |||
| } | |||
| @@ -220,45 +217,37 @@ void Tty::info(Libreblackjack::Info msg, int p1, int p2) { | |||
| break; | |||
| case Libreblackjack::Info::PlayerDoubleInvalid: | |||
| // s = "player_double_invalid"; | |||
| s = "Cannot double down"; | |||
| break; | |||
| case Libreblackjack::Info::PlayerNextHand: | |||
| // s = "player_next_hand"; | |||
| s = "Playing next hand #" + std::to_string(p1); | |||
| render = true; | |||
| break; | |||
| case Libreblackjack::Info::PlayerPushes: | |||
| // s = "player_pushes"; | |||
| s = "Player pushes " + std::to_string(1e-3*p1) + ((p2 > 0) ? (" with " + std::to_string(p2)) : ""); | |||
| s = "Player pushes " + string_format("%g", 1e-3*p1, 0.0) + ((p2 > 0) ? (" with " + std::to_string(p2)) : ""); | |||
| render = true; | |||
| break; | |||
| case Libreblackjack::Info::PlayerLosses: | |||
| // s = "player_losses"; | |||
| s = "Player losses " + std::to_string(1e-3*p1) + ((p2 > 0) ? (" with " + std::to_string(p2)) : ""); | |||
| s = "Player losses " + string_format("%g", 1e-3*p1, 0.0) + ((p2 > 0) ? (" with " + std::to_string(p2)) : ""); | |||
| render = true; | |||
| break; | |||
| case Libreblackjack::Info::PlayerBlackjack: | |||
| // s = "blackjack_player"; | |||
| s = "Player has Blackjack"; | |||
| render = true; | |||
| break; | |||
| case Libreblackjack::Info::PlayerWins: | |||
| // s = "player_wins"; | |||
| s = "Player wins " + std::to_string(1e-3*p1) + ((p2 > 0) ? (" with " + std::to_string(p2)) : ""); | |||
| s = "Player wins " + string_format("%g", 1e-3*p1, 0.0) + ((p2 > 0) ? (" with " + std::to_string(p2)) : ""); | |||
| render = true; | |||
| break; | |||
| case Libreblackjack::Info::NoBlackjacks: | |||
| // s = "no_blackjacks"; | |||
| s = "No blackjacks"; | |||
| break; | |||
| case Libreblackjack::Info::DealerBusts: | |||
| // s = "no_blackjacks"; | |||
| s = "Dealer busts with " + std::to_string(p1); | |||
| break; | |||
| @@ -267,17 +256,15 @@ void Tty::info(Libreblackjack::Info msg, int p1, int p2) { | |||
| break; | |||
| case Libreblackjack::Info::Bankroll: | |||
| std::cout << "Your bankroll is " << std::to_string(1e-3*p1) << std::endl; | |||
| std::cout << "Your bankroll is " << string_format("%g", 1e-3*p1, 0.0) << std::endl; | |||
| break; | |||
| case Libreblackjack::Info::CommandInvalid: | |||
| // s = "command_invalid"; | |||
| s = "Invalid command"; | |||
| break; | |||
| case Libreblackjack::Info::Bye: | |||
| // s = "bye"; | |||
| s = "Bye bye! We'll play Blackjack again next time"; | |||
| break; | |||