From 884f51108fa54ec606e5dc04de3797b4973c8245 Mon Sep 17 00:00:00 2001 From: That_One_Nerd Date: Wed, 18 Sep 2024 11:28:04 -0400 Subject: [PATCH] Added the blackjack simulator. --- .gitignore | 3 + BlackjackSim/BlackjackSim.sln | 25 ++ BlackjackSim/BlackjackSim/BlackjackSim.csproj | 10 + BlackjackSim/BlackjackSim/Card.cs | 72 ++++ BlackjackSim/BlackjackSim/DealerBase.cs | 16 + .../BlackjackSim/Dealers/DealerStandard.cs | 13 + BlackjackSim/BlackjackSim/Game.cs | 312 ++++++++++++++++++ BlackjackSim/BlackjackSim/Hand.cs | 57 ++++ BlackjackSim/BlackjackSim/IPerson.cs | 3 + BlackjackSim/BlackjackSim/PlayerBase.cs | 56 ++++ .../BlackjackSim/Players/PlayerAlwaysStand.cs | 14 + .../Players/PlayerCardCountingSimple.cs | 28 ++ .../Players/PlayerPercentageBet.cs | 17 + .../Players/PlayerProbabilistic.cs | 80 +++++ .../BlackjackSim/Players/PlayerRandomized.cs | 30 ++ .../BlackjackSim/Players/PlayerStandard.cs | 17 + .../BlackjackSim/Players/PlayerTabular.cs | 134 ++++++++ BlackjackSim/BlackjackSim/Program.cs | 275 +++++++++++++++ BlackjackSim/BlackjackSim/Shoe.cs | 44 +++ BlackjackSim/BlackjackSim/SuitKind.cs | 9 + BlackjackSim/BlackjackSim/ValueKind.cs | 18 + README.md | 5 + 22 files changed, 1238 insertions(+) create mode 100644 BlackjackSim/BlackjackSim.sln create mode 100644 BlackjackSim/BlackjackSim/BlackjackSim.csproj create mode 100644 BlackjackSim/BlackjackSim/Card.cs create mode 100644 BlackjackSim/BlackjackSim/DealerBase.cs create mode 100644 BlackjackSim/BlackjackSim/Dealers/DealerStandard.cs create mode 100644 BlackjackSim/BlackjackSim/Game.cs create mode 100644 BlackjackSim/BlackjackSim/Hand.cs create mode 100644 BlackjackSim/BlackjackSim/IPerson.cs create mode 100644 BlackjackSim/BlackjackSim/PlayerBase.cs create mode 100644 BlackjackSim/BlackjackSim/Players/PlayerAlwaysStand.cs create mode 100644 BlackjackSim/BlackjackSim/Players/PlayerCardCountingSimple.cs create mode 100644 BlackjackSim/BlackjackSim/Players/PlayerPercentageBet.cs create mode 100644 BlackjackSim/BlackjackSim/Players/PlayerProbabilistic.cs create mode 100644 BlackjackSim/BlackjackSim/Players/PlayerRandomized.cs create mode 100644 BlackjackSim/BlackjackSim/Players/PlayerStandard.cs create mode 100644 BlackjackSim/BlackjackSim/Players/PlayerTabular.cs create mode 100644 BlackjackSim/BlackjackSim/Program.cs create mode 100644 BlackjackSim/BlackjackSim/Shoe.cs create mode 100644 BlackjackSim/BlackjackSim/SuitKind.cs create mode 100644 BlackjackSim/BlackjackSim/ValueKind.cs diff --git a/.gitignore b/.gitignore index 5ed0b2c..710ed0d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,10 @@ # Build files. */bin/ */obj/ +*/*/bin/ +*/*/obj/ convimg.out # Other stuff. .DS_Store +*/.vs/ diff --git a/BlackjackSim/BlackjackSim.sln b/BlackjackSim/BlackjackSim.sln new file mode 100644 index 0000000..6196512 --- /dev/null +++ b/BlackjackSim/BlackjackSim.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34309.116 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlackjackSim", "BlackjackSim\BlackjackSim.csproj", "{2A2464A5-9F6B-464A-A980-20DFB65250A8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2A2464A5-9F6B-464A-A980-20DFB65250A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A2464A5-9F6B-464A-A980-20DFB65250A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A2464A5-9F6B-464A-A980-20DFB65250A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A2464A5-9F6B-464A-A980-20DFB65250A8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4B86B10A-5893-4CB6-9542-D8974BFC75EC} + EndGlobalSection +EndGlobal diff --git a/BlackjackSim/BlackjackSim/BlackjackSim.csproj b/BlackjackSim/BlackjackSim/BlackjackSim.csproj new file mode 100644 index 0000000..a222ba7 --- /dev/null +++ b/BlackjackSim/BlackjackSim/BlackjackSim.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + disable + enable + + + diff --git a/BlackjackSim/BlackjackSim/Card.cs b/BlackjackSim/BlackjackSim/Card.cs new file mode 100644 index 0000000..52a0801 --- /dev/null +++ b/BlackjackSim/BlackjackSim/Card.cs @@ -0,0 +1,72 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace BlackjackSim; + +public readonly struct Card : IEquatable +{ + public readonly SuitKind suit; + public readonly ValueKind value; + + public Card() + { + suit = SuitKind.Spades; + value = ValueKind.Ace; + } + public Card(SuitKind suit, ValueKind value) + { + this.suit = suit; + this.value = value; + } + public Card(ValueKind value, SuitKind suit) + { + this.suit = suit; + this.value = value; + } + + public static Card[] GetDeck() + { + SuitKind[] suits = Enum.GetValues(); + ValueKind[] values = Enum.GetValues(); + + Card[] totalCards = new Card[suits.Length * values.Length]; + for (int s = 0; s < suits.Length; s++) + { + for (int v = 0; v < values.Length; v++) + { + totalCards[s * values.Length + v] = (suits[s], values[v]); + } + } + return totalCards; + } + + public int GetValue(bool acesAreEleven) => value switch + { + ValueKind.Ace => acesAreEleven ? 11 : 1, + ValueKind.Two => 2, + ValueKind.Three => 3, + ValueKind.Four => 4, + ValueKind.Five => 5, + ValueKind.Six => 6, + ValueKind.Seven => 7, + ValueKind.Eight => 8, + ValueKind.Nine => 9, + ValueKind.Ten or ValueKind.Jack or ValueKind.Queen or ValueKind.King => 10, + _ => 0 + }; + + public override bool Equals(object? obj) => obj is Card objCard && Equals(objCard); + public bool Equals(Card other) => suit == other.suit && value == other.value; + public override int GetHashCode() => suit.GetHashCode() ^ value.GetHashCode(); + public override string ToString() => $"{value} of {suit}"; + + public static bool operator ==(Card card, SuitKind suit) => card.suit == suit; + public static bool operator ==(Card card, ValueKind value) => card.value == value; + public static bool operator !=(Card card, SuitKind suit) => card.suit != suit; + public static bool operator !=(Card card, ValueKind value) => card.value != value; + + public static implicit operator Card(ValueTuple tuple) => + new(tuple.Item1, tuple.Item2); + public static implicit operator Card(ValueTuple tuple) => + new(tuple.Item1, tuple.Item2); +} diff --git a/BlackjackSim/BlackjackSim/DealerBase.cs b/BlackjackSim/BlackjackSim/DealerBase.cs new file mode 100644 index 0000000..774b6c7 --- /dev/null +++ b/BlackjackSim/BlackjackSim/DealerBase.cs @@ -0,0 +1,16 @@ +namespace BlackjackSim; + +public abstract class DealerBase : IPerson +{ + public required int DrawTo { get; init; } + public required double BlackjackPayment { get; init; } + public required double WinPayment { get; init; } + + public double HouseEdge => HouseWins / (HouseLossesRegular * WinPayment + HouseLossesBlackjack * BlackjackPayment); + public int HouseWins { get; set; } + public int HouseLossesRegular { get; set; } + public int HouseLossesBlackjack { get; set; } + + public abstract void OnGameBegin(Game game); + public abstract bool ShouldResetShoe(); +} diff --git a/BlackjackSim/BlackjackSim/Dealers/DealerStandard.cs b/BlackjackSim/BlackjackSim/Dealers/DealerStandard.cs new file mode 100644 index 0000000..6fb5455 --- /dev/null +++ b/BlackjackSim/BlackjackSim/Dealers/DealerStandard.cs @@ -0,0 +1,13 @@ +namespace BlackjackSim.Dealers; + +public class DealerStandard : DealerBase +{ + public override void OnGameBegin(Game game) + { + + } + public override bool ShouldResetShoe() + { + return false; + } +} diff --git a/BlackjackSim/BlackjackSim/Game.cs b/BlackjackSim/BlackjackSim/Game.cs new file mode 100644 index 0000000..a054ae3 --- /dev/null +++ b/BlackjackSim/BlackjackSim/Game.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BlackjackSim; + +public class Game +{ + public DealerBase Dealer { get; private init; } + public PlayerBase[] Players { get; private init; } + + public int ShoeSize { get; init; } = 6; + + private Shoe? shoe; + + public Game(DealerBase dealer, params PlayerBase[] players) + { + Dealer = dealer; + Players = players; + } + + public void PlayRound(bool debug = false) + { + if (Players.Length == 0) + { + if (debug) Console.WriteLine("!! No players! Ignoring this round."); + return; + } + if (debug) + { + Console.WriteLine(" Starting a game of blackjack."); + Console.WriteLine($" Dealer is {Dealer.GetType().Name}"); + + Dictionary playerTypes = []; + foreach (PlayerBase p in Players) + { + Type pType = p.GetType(); + if (playerTypes.TryGetValue(pType, out int count)) playerTypes[pType] = count + 1; + else playerTypes.Add(pType, 1); + } + + if (playerTypes.Count == 1) + { + Console.WriteLine($" Players are {Players.Length} {Players[0].GetType().Name}"); + } + else + { + Console.WriteLine(" Players:"); + foreach (KeyValuePair type in playerTypes) + { + Console.WriteLine($" {type.Value} {type.Key.Name}"); + } + } + } + + Dealer.OnGameBegin(this); + foreach (PlayerBase p in Players) + { + p.OnGameBegin(); + p.DeltaMoneyThisRound = 0; + } + if (Dealer.ShouldResetShoe()) + { + if (debug) Console.WriteLine("! Dealer has requested a shoe reset!"); + shoe = resetShoe(); + } + else shoe ??= resetShoe(); + + // STEP 0: Bets are collected. + Hand dealerHand = new(Dealer); + Dictionary> playerHands = []; + List totalHands = []; + foreach (PlayerBase p in Players) + { + Hand hand = new(p) + { + bet = Math.Min(p.PlaceInitialBet(), p.Money) + }; + totalHands.Add(hand); + if (playerHands.TryGetValue(p, out List? hands)) hands.Add(hand); + else + { + List newHands = [hand]; + playerHands.Add(p, newHands); + p.YourGivenHands(newHands); + } + } + + // STEP 1: Deal out the cards. + bool dealerVisibleAce = false; + for (int i = 0; i < 2; i++) + { + // Dealer card first. + Card dealerCard = tryGetFromShoe(); + if (i == 0) + { + // Show the first dealer card to the other players. + if (debug) Console.WriteLine($" Dealer drew {dealerCard}"); + foreach (PlayerBase p in Players) + { + p.OnSeenCard(dealerCard, false); + p.InitialVisibleDealerCard(dealerCard); + } + if (dealerCard == ValueKind.Ace) dealerVisibleAce = true; // For the insurance step. + } + else + { + // Don't show any future dealer cards to the players. + if (debug) Console.WriteLine($" (Hidden) Dealer drew {dealerCard}"); + } + dealerHand.cards.Add(dealerCard); + + // Deal one card to each hand. + foreach (Hand h in totalHands) + { + Card playerCard = tryGetFromShoe(); + h.cards.Add(playerCard); + + // Notify players. + foreach (PlayerBase p in Players) + { + p.OnSeenCard(playerCard, h.player == p); + } + } + } + + // STEP 1b: Check for insurance. + if (dealerVisibleAce) + { + if (debug) Console.WriteLine($"! Dealer has a visible ace! Insurance will commence."); + bool isBlackjack = dealerHand.IsBlackjack(); + + foreach (PlayerBase p in Players) + { + double insuranceBet = Math.Min(p.MakeInsuranceBet(), p.Money); + if (isBlackjack) + { + p.DeltaMoneyThisRound += insuranceBet * 2; + p.InsuranceWon++; + p.InsuranceDelta = insuranceBet * 2; + } + else + { + p.DeltaMoneyThisRound -= insuranceBet; + p.InsuranceLost++; + p.InsuranceDelta = -insuranceBet; + } + } + + if (isBlackjack) goto _handCompare; // You can't beat a dealer blackjack. + } + + // STEP 2: Play blackjack. + foreach (KeyValuePair> ph in playerHands) + { + PlayerBase player = ph.Key; + List hands = ph.Value; + for (int i = 0; i < hands.Count; i++) + { + Hand h = hands[i]; + + bool hasDoubled = false; + _retry: + if (h.cards.Count == 2) + { + if ((h.cards[0].GetValue(false) == h.cards[1].GetValue(false) || + h.GetValue() == 16) && player.ShouldSplit(h)) + { + // Split the hand into two. You can split multiple times. + Hand other = new(player) + { + bet = h.bet, + cards = [h.cards[1]] + }; + h.cards.RemoveAt(1); + hands.Add(other); + player.HandsSplit++; + + goto _retry; + } + + if (player.ShouldDouble(h) && !hasDoubled) + { + // Double bet, get one card, and call it done. + h.bet *= 2; + Card newCard = tryGetFromShoe(); + + // Notify players of new card. + foreach (PlayerBase p in Players) p.OnSeenCard(newCard, player == p); + h.cards.Add(newCard); + player.OnDouble(h); + hasDoubled = true; + player.HandsDoubled++; + } + } + + while (h.GetValue() < 21 && player.ShouldHit(h)) + { + // Add new card to hand. + Card newCard = tryGetFromShoe(); + + // Notify players of new card. + foreach (PlayerBase p in Players) p.OnSeenCard(newCard, player == p); + h.cards.Add(newCard); + player.OnHit(h); + } + } + } + + // STEP 3a: Dealer reveals hidden card. + if (debug) Console.WriteLine($" Dealer reveals hand: {dealerHand.GetValue()}"); + for (int i = 1; i < dealerHand.cards.Count; i++) + { + // Notify players. + foreach (PlayerBase p in Players) p.OnSeenCard(dealerHand.cards[i], false); + } + + // STEP 3b: Dealer draws until its limit (hit on value). + while (dealerHand.GetValue() <= Dealer.DrawTo) + { + // Add new card to hand. + Card newCard = tryGetFromShoe(); + dealerHand.cards.Add(newCard); + if (debug) Console.WriteLine($" Dealer drawing new card: {newCard} (total {dealerHand.GetValue()})"); + + // Notify players of new card. + foreach (PlayerBase p in Players) p.OnSeenCard(newCard, false); + } + + // STEP 4: Compare hands. + _handCompare: + int dealerValue = dealerHand.GetValue(); + int handIndex = 0; + foreach (Hand h in totalHands) + { + int yourValue = h.GetValue(); + HandStatus status; + if (dealerHand.IsBlackjack()) + { + if (h.IsBlackjack()) status = HandStatus.Push; + else status = HandStatus.Lose; + } + else if (yourValue > 21) status = HandStatus.Lose; // You've bust. + else if (dealerValue > 21) status = HandStatus.Won; // Dealer bust. + else + { + if (h.IsBlackjack()) status = HandStatus.WonBlackjack; + else if (dealerValue > yourValue) status = HandStatus.Lose; + else if (dealerValue < yourValue) status = HandStatus.Won; + else status = HandStatus.Push; + } + + if (debug) Console.WriteLine($" Hand {handIndex} has {status} ({yourValue})."); + + h.status = status; + PlayerBase player = (PlayerBase)h.player; + switch (status) + { + case HandStatus.Won: + player.DeltaMoneyThisRound += h.bet * Dealer.WinPayment; + player.HandsWon++; + Dealer.HouseLossesRegular++; + break; + + case HandStatus.WonBlackjack: + player.DeltaMoneyThisRound += h.bet * Dealer.BlackjackPayment; + player.HandsWon++; + player.HandsBlackjacked++; + Dealer.HouseLossesBlackjack++; + break; + + case HandStatus.Lose: + player.DeltaMoneyThisRound -= h.bet; + player.HandsLost++; + Dealer.HouseWins++; + break; + + case HandStatus.Push: // Push means money back. + player.HandsPushed++; + break; + + default: break; + } + + handIndex++; + } + + if (totalHands.Count(x => x.status == HandStatus.Won || x.status == HandStatus.WonBlackjack) >= 2) Console.ReadKey(); + + // STEP 5: Apply money deltas. + foreach (PlayerBase p in Players) p.Money += p.DeltaMoneyThisRound; + // Now we're done. + + Card tryGetFromShoe() + { + if (shoe!.CardsRemaining > 0) return shoe.Get(); + else + { + if (debug) Console.WriteLine("!! Shoe has unexpectedly run out of cards!"); + shoe = resetShoe(); + return shoe.Get(); + } + } + Shoe resetShoe() + { + if (debug) Console.WriteLine($" Resetting shoe ({ShoeSize} decks)."); + Shoe shoe = new(ShoeSize); + foreach (PlayerBase p in Players) p.OnShoeReset(ShoeSize); + return shoe; + } + } +} diff --git a/BlackjackSim/BlackjackSim/Hand.cs b/BlackjackSim/BlackjackSim/Hand.cs new file mode 100644 index 0000000..b439603 --- /dev/null +++ b/BlackjackSim/BlackjackSim/Hand.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; + +namespace BlackjackSim; + +public class Hand +{ + public IPerson player; + public List cards = []; + public double bet; + + public HandStatus status; + + public Hand(IPerson player) + { + this.player = player; + } + + public bool IsBlackjack() => cards.Count == 2 && + ((cards[0] == ValueKind.Ace && cards[1].GetValue(false) == 10) || + (cards[1] == ValueKind.Ace && cards[0].GetValue(false) == 10)); + public int GetValue() + { + // Count all non-aces first. + int aceCount = cards.Count(x => x == ValueKind.Ace); + int sum = 0; + foreach (Card c in cards) + { + if (c == ValueKind.Ace) continue; + sum += c.GetValue(false); + } + + // You will never count two aces as 11, so we don't need to + // search too deep. All other aces count as 1, the last one + // is the only one we need to compare for. + for (int i = 0; i < aceCount; i++) + { + if (i < aceCount - 1) sum += 1; + else + { + if (sum >= 21) sum += 1; + else sum += 11; + } + } + + return sum; + } +} + +public enum HandStatus +{ + Incomplete, + Won, + WonBlackjack, + Push, + Lose +} diff --git a/BlackjackSim/BlackjackSim/IPerson.cs b/BlackjackSim/BlackjackSim/IPerson.cs new file mode 100644 index 0000000..a0bd98f --- /dev/null +++ b/BlackjackSim/BlackjackSim/IPerson.cs @@ -0,0 +1,3 @@ +namespace BlackjackSim; + +public interface IPerson; diff --git a/BlackjackSim/BlackjackSim/PlayerBase.cs b/BlackjackSim/BlackjackSim/PlayerBase.cs new file mode 100644 index 0000000..2fea031 --- /dev/null +++ b/BlackjackSim/BlackjackSim/PlayerBase.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; + +namespace BlackjackSim; + +public abstract class PlayerBase : IPerson +{ + public double InitialMoney { get; set; } = 10_000; + public double Money { get; set; } + public double DeltaMoneyThisRound { get; set; } + public string Name { get; set; } + + public PlayerBase(string name) + { + Money = InitialMoney; + Name = name; + } + + public int HandsPlayed => HandsWon + HandsLost + HandsPushed; + public int HandsWon { get; set; } + public int HandsLost { get; set; } + public int HandsPushed { get; set; } + public int HandsBlackjacked { get; set; } + + public int HandsDoubled { get; set; } + public int HandsSplit { get; set; } + + public int InsurancePlayed => InsuranceWon + InsuranceLost; + public int InsuranceWon { get; set; } + public int InsuranceLost { get; set; } + public double InsuranceDelta { get; set; } + + public virtual void OnGameBegin() { } + public virtual void OnShoeReset(int decks) { } + + public abstract double PlaceInitialBet(); + + public virtual void YourGivenHands(List hands) { } + public virtual void OnSeenCard(Card card, bool yours) { } + public virtual void InitialVisibleDealerCard(Card card) { } + + public abstract double MakeInsuranceBet(); + + public abstract bool ShouldHit(Hand hand); + public virtual void OnHit(Hand hand) { } + + public abstract bool ShouldDouble(Hand hand); + public virtual void OnDouble(Hand hand) { } + + public abstract bool ShouldSplit(Hand hand); + public virtual void OnSplit(Hand hand) { } + + public virtual void OnWonHand(Hand hand) { } + public virtual void OnLostHand(Hand hand) { } + public virtual void OnHandIsBlackjack(Hand hand) { } + public virtual void OnHandPush(Hand hand) { } +} diff --git a/BlackjackSim/BlackjackSim/Players/PlayerAlwaysStand.cs b/BlackjackSim/BlackjackSim/Players/PlayerAlwaysStand.cs new file mode 100644 index 0000000..a4f9f4d --- /dev/null +++ b/BlackjackSim/BlackjackSim/Players/PlayerAlwaysStand.cs @@ -0,0 +1,14 @@ +namespace BlackjackSim.Players; + +public class PlayerAlwaysStand : PlayerBase +{ + public double BetSize { get; set; } = 100; + + public PlayerAlwaysStand() : base("Always Stand") { } + + public override double PlaceInitialBet() => BetSize; + public override double MakeInsuranceBet() => 0; + public override bool ShouldHit(Hand hand) => false; + public override bool ShouldDouble(Hand hand) => false; + public override bool ShouldSplit(Hand hand) => false; +} diff --git a/BlackjackSim/BlackjackSim/Players/PlayerCardCountingSimple.cs b/BlackjackSim/BlackjackSim/Players/PlayerCardCountingSimple.cs new file mode 100644 index 0000000..bcb826e --- /dev/null +++ b/BlackjackSim/BlackjackSim/Players/PlayerCardCountingSimple.cs @@ -0,0 +1,28 @@ +using System; + +namespace BlackjackSim.Players; + +public class PlayerCardCountingSimple : PlayerTabular // Build on top of a good system. +{ + public PlayerCardCountingSimple() + { + Name = "Card Counting (Simple)"; + } + + private int counter; + private int decks; + + public override void OnShoeReset(int decks) + { + this.decks = decks; + counter = 0; + } + public override void OnSeenCard(Card card, bool yours) + { + int val = card.GetValue(true); + if (val <= 6) counter++; + else if (val >= 10) counter--; + } + + public override double PlaceInitialBet() => BetSize * Math.Pow(1.1, -counter); +} diff --git a/BlackjackSim/BlackjackSim/Players/PlayerPercentageBet.cs b/BlackjackSim/BlackjackSim/Players/PlayerPercentageBet.cs new file mode 100644 index 0000000..5e4b353 --- /dev/null +++ b/BlackjackSim/BlackjackSim/Players/PlayerPercentageBet.cs @@ -0,0 +1,17 @@ +namespace BlackjackSim.Players; + +public class PlayerPercentageBet : PlayerBase +{ + public double PercentageBet { get; set; } + + public PlayerPercentageBet(double percentage) : base($"Percentage Bet {percentage}%") + { + PercentageBet = percentage; + } + + public override double PlaceInitialBet() => Money * (PercentageBet * 0.01); + public override double MakeInsuranceBet() => 0; + public override bool ShouldHit(Hand hand) => hand.GetValue() <= 16; + public override bool ShouldDouble(Hand hand) => false; + public override bool ShouldSplit(Hand hand) => false; +} diff --git a/BlackjackSim/BlackjackSim/Players/PlayerProbabilistic.cs b/BlackjackSim/BlackjackSim/Players/PlayerProbabilistic.cs new file mode 100644 index 0000000..853e53d --- /dev/null +++ b/BlackjackSim/BlackjackSim/Players/PlayerProbabilistic.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; + +namespace BlackjackSim.Players; + +public class PlayerProbabilistic : PlayerBase +{ + public double BetValue { get; set; } = 100; + + public PlayerProbabilistic() : base("Probabilistic Choices") { } + + private readonly List seenCards = [], remainingCards = []; + private Card dealerCard; + + public override void OnShoeReset(int decks) + { + seenCards.Clear(); + remainingCards.Clear(); + for (int i = 0; i < decks; i++) remainingCards.AddRange(Card.GetDeck()); + } + public override void OnSeenCard(Card card, bool yours) + { + seenCards.Add(card); + remainingCards.Remove(card); + } + public override void InitialVisibleDealerCard(Card card) => dealerCard = card; + + public override double PlaceInitialBet() => BetValue; + public override double MakeInsuranceBet() + { + // Calculate odds that the dealer's hand equals 21. + Hand temp = new(this) + { + cards = [dealerCard] + }; + + int matches = 0, doesntMatch = 0; + for (int i = 0; i < remainingCards.Count; i++) + { + Card added = remainingCards[i]; + temp.cards.Add(added); + if (temp.GetValue() == 21) matches++; + else doesntMatch++; + temp.cards.Remove(added); + } + + double trueOdds = (double)matches / (matches + doesntMatch); + double weightedOdds = ApplyBias(trueOdds, 0.65); + return BetValue * 0.5 * weightedOdds; // Bet proportionally. + } + + public override bool ShouldDouble(Hand hand) => false; // TODO + public override bool ShouldSplit(Hand hand) => false; // TODO: How?? + public override bool ShouldHit(Hand hand) + { + // Determine odds that the next card will bust. + Hand temp = new(this) + { + cards = new(hand.cards), + }; + + int willBust = 0, wontBust = 0; + for (int i = 0; i < remainingCards.Count; i++) + { + Card added = remainingCards[i]; + temp.cards.Add(added); + if (temp.GetValue() > 21) willBust++; + else wontBust++; + temp.cards.Remove(added); + } + + if ((double)willBust / (willBust + wontBust) >= 0.5) return false; + else return true; + } + + private static double ApplyBias(double x, double bias) + { + double k = (1 - bias) * (1 - bias) * (1 - bias); // Better than pow. + return x * k / (x * k - x + 1); + } +} diff --git a/BlackjackSim/BlackjackSim/Players/PlayerRandomized.cs b/BlackjackSim/BlackjackSim/Players/PlayerRandomized.cs new file mode 100644 index 0000000..37e23de --- /dev/null +++ b/BlackjackSim/BlackjackSim/Players/PlayerRandomized.cs @@ -0,0 +1,30 @@ +using System; + +namespace BlackjackSim.Players; + +public class PlayerRandomized : PlayerBase +{ + public double MinBetValue { get; set; } = 100; + public double MaxBetValue { get; set; } = 100; + + public double BetInsuranceOdds { get; set; } = 0.5; + public double HitOdds { get; set; } = 0.5; + public double DoubleOdds { get; set; } = 0.5; + public double SplitOdds { get; set; } = 0.5; + + private readonly Random rand = new(); + private double betValue; + + public PlayerRandomized() : base("Randomized Player") { } + + public override double PlaceInitialBet() + { + betValue = rand.NextDouble() * (MaxBetValue - MinBetValue) + MinBetValue; + return betValue; + } + public override double MakeInsuranceBet() => rand.NextDouble() <= BetInsuranceOdds ? betValue / 2 : 0; + + public override bool ShouldHit(Hand hand) => rand.NextDouble() <= HitOdds; + public override bool ShouldDouble(Hand hand) => rand.NextDouble() <= DoubleOdds; + public override bool ShouldSplit(Hand hand) => rand.NextDouble() <= SplitOdds; +} diff --git a/BlackjackSim/BlackjackSim/Players/PlayerStandard.cs b/BlackjackSim/BlackjackSim/Players/PlayerStandard.cs new file mode 100644 index 0000000..33a02cf --- /dev/null +++ b/BlackjackSim/BlackjackSim/Players/PlayerStandard.cs @@ -0,0 +1,17 @@ +namespace BlackjackSim.Players; + +public class PlayerStandard : PlayerBase +{ + public double BetSize { get; set; } + + public PlayerStandard(double betSize) : base($"Standard Bet {betSize}") + { + BetSize = betSize; + } + + public override double PlaceInitialBet() => BetSize; + public override double MakeInsuranceBet() => 0; + public override bool ShouldHit(Hand hand) => hand.GetValue() <= 16; + public override bool ShouldDouble(Hand hand) => false; + public override bool ShouldSplit(Hand hand) => true; +} diff --git a/BlackjackSim/BlackjackSim/Players/PlayerTabular.cs b/BlackjackSim/BlackjackSim/Players/PlayerTabular.cs new file mode 100644 index 0000000..49a7fbf --- /dev/null +++ b/BlackjackSim/BlackjackSim/Players/PlayerTabular.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; + +namespace BlackjackSim.Players; + +public class PlayerTabular : PlayerBase +{ + public double BetSize { get; set; } = 100; + + public Dictionary HardTotals = new() + { + { 21, [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand ] }, + { 20, [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand ] }, + { 19, [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand ] }, + { 18, [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand ] }, + { 17, [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand ] }, + { 16, [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 15, [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 14, [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 13, [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 12, [Choice.Hit , Choice.Hit , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 11, [Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit ] }, + { 10, [Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.Hit , Choice.Hit ] }, + { 9, [Choice.Hit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 8, [Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 7, [Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 6, [Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 5, [Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 4, [Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 3, [Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { 2, [Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + }; + public Dictionary AceSoftTotals = new() + { + { ValueCombined.Ten , [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand ] }, + { ValueCombined.Nine , [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand ] }, + { ValueCombined.Eight, [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.DoubleStand, Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand ] }, + { ValueCombined.Seven, [Choice.DoubleStand, Choice.DoubleStand, Choice.DoubleStand, Choice.DoubleStand, Choice.DoubleStand, Choice.Stand , Choice.Stand , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { ValueCombined.Six , [Choice.Hit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { ValueCombined.Five , [Choice.Hit , Choice.Hit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { ValueCombined.Four , [Choice.Hit , Choice.Hit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { ValueCombined.Three, [Choice.Hit , Choice.Hit , Choice.Hit , Choice.DoubleHit , Choice.DoubleHit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { ValueCombined.Two , [Choice.Hit , Choice.Hit , Choice.Hit , Choice.DoubleHit , Choice.DoubleHit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + }; + public Dictionary Pairs = new() + { + { ValueCombined.Ace , [Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split ] }, + { ValueCombined.Ten , [Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand , Choice.Stand ] }, + { ValueCombined.Nine , [Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Stand , Choice.Split , Choice.Split , Choice.Stand , Choice.Stand ] }, + { ValueCombined.Eight, [Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split ] }, + { ValueCombined.Seven, [Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { ValueCombined.Six , [Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { ValueCombined.Five , [Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.DoubleHit , Choice.Hit , Choice.Hit ] }, + { ValueCombined.Four , [Choice.Hit , Choice.Hit , Choice.Hit , Choice.Split , Choice.Split , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { ValueCombined.Three, [Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + { ValueCombined.Two , [Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Split , Choice.Hit , Choice.Hit , Choice.Hit , Choice.Hit ] }, + }; + + public PlayerTabular() : base("Tabular Gameplay") { } + + private Card dealerCard; + + public override double PlaceInitialBet() => BetSize; + public override double MakeInsuranceBet() => 0; + public override void InitialVisibleDealerCard(Card card) => dealerCard = card; + + public override bool ShouldDouble(Hand hand) + { + Choice choice = GetChoice(hand); + return choice == Choice.DoubleHit || choice == Choice.DoubleStand; + } + public override bool ShouldSplit(Hand hand) => GetChoice(hand) == Choice.Split; + public override bool ShouldHit(Hand hand) + { + Choice choice = GetChoice(hand); + return choice == Choice.Hit || choice == Choice.DoubleHit; + } + + private Choice GetChoice(Hand hand) + { + ValueCombined dealerCard = CardToValue(this.dealerCard); + Choice[] row; + if (hand.cards.Count == 2) + { + // Check for pairs. + ValueCombined cardA = CardToValue(hand.cards[0]), + cardB = CardToValue(hand.cards[1]); + if (cardA == cardB) row = Pairs[cardA]; // Load pair table. + else if (cardA == ValueCombined.Ace) row = AceSoftTotals[cardB]; // Load soft totals. + else if (cardB == ValueCombined.Ace) row = AceSoftTotals[cardA]; // Load soft totals. + else row = HardTotals[hand.GetValue()]; // Not pair or ace, load hard totals. + } + else row = HardTotals[hand.GetValue()]; // Load hard totals. + return row[(int)dealerCard]; + } + + public enum Choice + { + Stand, + Hit, + DoubleHit, + DoubleStand, + Split + } + public enum ValueCombined + { + Two = 0, + Three = 1, + Four = 2, + Five = 3, + Six = 4, + Seven = 5, + Eight = 6, + Nine = 7, + Ten = 8, + Ace = 9 + } + public static ValueCombined CardToValue(Card c) => c.value switch + { + ValueKind.Two => ValueCombined.Two, + ValueKind.Three => ValueCombined.Three, + ValueKind.Four => ValueCombined.Four, + ValueKind.Five => ValueCombined.Five, + ValueKind.Six => ValueCombined.Six, + ValueKind.Seven => ValueCombined.Seven, + ValueKind.Eight => ValueCombined.Eight, + ValueKind.Nine => ValueCombined.Nine, + ValueKind.Ten => ValueCombined.Ten, + ValueKind.Jack => ValueCombined.Ten, + ValueKind.Queen => ValueCombined.Ten, + ValueKind.King => ValueCombined.Ten, + ValueKind.Ace => ValueCombined.Ace, + _ => throw new("invalid card?") // Shouldn't happen under correct usage. + }; +} diff --git a/BlackjackSim/BlackjackSim/Program.cs b/BlackjackSim/BlackjackSim/Program.cs new file mode 100644 index 0000000..4609218 --- /dev/null +++ b/BlackjackSim/BlackjackSim/Program.cs @@ -0,0 +1,275 @@ +/*****722871 + * Date: 9/18/2024 + * Programmer: Kyle + * Program Name: Blackjack + * Program Description: Simulates casino blackjack as accurately as possible. + */ + +using BlackjackSim.Dealers; +using BlackjackSim.Players; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BlackjackSim; + +public static class Program +{ + public static async Task Main() + { + Console.CursorVisible = false; + Console.OutputEncoding = Encoding.Unicode; + DealerBase dealer = new DealerStandard() + { + BlackjackPayment = 1.5f, + WinPayment = 1.0f, + DrawTo = 16, + }; + await SimulatePlayerGraph(dealer, new PlayerCardCountingSimple()); + Console.ResetColor(); + } + + public static async Task SimulatePlayerGraph(DealerBase dealer, PlayerBase player) + { + Game game = new(dealer, player); + List moneyGraph = []; + int rounds = 0; + while (player.Money > 1) + { + game.PlayRound(false); + moneyGraph.Add(new(player.Money, player.DeltaMoneyThisRound)); + RenderOnce(dealer, player, moneyGraph); + + rounds++; + await Task.Delay(20); + } + } + public static Task SimulatePlayerGraphFast(DealerBase dealer, PlayerBase player, int iters) + { + Game game = new(dealer, player); + List moneyGraph = []; + + for (int r = 0; r < iters; r++) + { + moneyGraph.Clear(); + player.Money = player.InitialMoney; + + int roundsThisTime = 0; + while (player.Money > 1 && roundsThisTime < 10_000 && r < iters) + { + game.PlayRound(false); + lock (moneyGraph) + { + moneyGraph.Add(new(player.Money, player.DeltaMoneyThisRound)); + } + r++; + roundsThisTime++; + } + RenderOnce(dealer, player, moneyGraph); + } + + return Task.CompletedTask; + } + + private static int lastHandsWon, lastHandsLost, lastHandsPushed, lastHandsDoubled, lastHandsSplit, lastHandsBlackjacked, lastInsuranceWins, lastInsuranceLosses, insuranceShowDirection; + private static void RenderOnce(DealerBase dealer, PlayerBase player, List segments) + { + Console.SetCursorPosition(0, 1); + Console.ResetColor(); + Console.WriteLine($"\x1b[37m Player Funds: \x1b[97m${player.Money:0.00} "); + + double moneyBefore = player.Money - player.DeltaMoneyThisRound; + double percent = (player.Money / moneyBefore - 1) * 100; + string color = player.DeltaMoneyThisRound > 0 ? "\x1b[32m" : (player.DeltaMoneyThisRound < 0 ? "\x1b[31m" : "\x1b[37m"); + Console.WriteLine($"\x1b[37m Delta: {color}${player.DeltaMoneyThisRound,8:0.00} ({percent,5:0.0}%) "); + + Console.WriteLine($"\n\x1b[37m Hands Won: {(player.HandsWon > lastHandsWon ? "\x1b[92m+" : "\x1b[90m ")}{player.HandsWon,4} ({100.0 * player.HandsWon / player.HandsPlayed,4:0.0}%) "); + Console.WriteLine($"\x1b[37m Hands Lost: {(player.HandsLost > lastHandsLost ? "\x1b[91m+" : "\x1b[90m ")}{player.HandsLost,4} ({100.0 * player.HandsLost / player.HandsPlayed,4:0.0}%) "); + Console.WriteLine($"\x1b[37m Hands Pushed: {(player.HandsPushed > lastHandsPushed ? "\x1b[97m+" : "\x1b[90m ")}{player.HandsPushed,4} ({100.0 * player.HandsPushed / player.HandsPlayed,4:0.0}%) "); + Console.WriteLine($"\x1b[37m Blackjacks: {(player.HandsBlackjacked > lastHandsBlackjacked ? "\x1b[33m+" : "\x1b[90m ")}{player.HandsBlackjacked,4} ({100.0 * player.HandsBlackjacked / player.HandsPlayed,4:0.0}%) "); + + Console.WriteLine($"\n\x1b[37m Hands Doubled: {(player.HandsDoubled > lastHandsDoubled ? "\x1b[97m+" : "\x1b[90m ")}{player.HandsDoubled,3} ({100.0 * player.HandsDoubled / player.HandsPlayed,4:0.0}%) "); + Console.WriteLine($"\x1b[37m Hands Split: {(player.HandsSplit > lastHandsSplit ? "\x1b[97m+" : "\x1b[90m ")}{player.HandsSplit,3} ({100.0 * player.HandsSplit / player.HandsPlayed,4:0.0}%) "); + + percent = 100 * dealer.HouseEdge - 100; + Console.WriteLine($"\n\x1b[37m House Edge: {(percent > 0 ? "\x1b[31m" : "\x1b[32m")}{percent,5:0.0}% "); + + int centerX = (int)(Console.WindowWidth * 0.5); + Console.SetCursorPosition(centerX, 4); + string winWrite = $"\x1b[37mInsurance Wins: {(player.InsuranceWon > lastInsuranceWins ? "\x1b[92m+" : "\x1b[90m ")}{player.InsuranceWon,4} ({100.0 * player.InsuranceWon / player.InsurancePlayed,4:0.0}%) "; + Console.Write(winWrite); + if (player.InsuranceWon > lastInsuranceWins) insuranceShowDirection = 1; + + Console.SetCursorPosition(centerX, 5); + string loseWrite = $"\x1b[37mInsurance Losses: {(player.InsuranceLost > lastInsuranceLosses ? "\x1b[91m+" : "\x1b[90m ")}{player.InsuranceLost,4} ({100.0 * player.InsuranceLost / player.InsurancePlayed,4:0.0}%) "; + Console.Write(loseWrite); + if (player.InsuranceLost > lastInsuranceLosses) insuranceShowDirection = -1; + + if (insuranceShowDirection > 0) + { + Console.Write(new string(' ', 10)); + Console.SetCursorPosition(centerX + 33, 4); + Console.Write($" ↑ ${player.InsuranceDelta:0.00}"); + } + else if (insuranceShowDirection < 0) + { + Console.Write($" ↓ ${-player.InsuranceDelta:0.00}"); + Console.SetCursorPosition(centerX + 33, 4); + Console.Write(new string(' ', 10)); + } + + lastHandsWon = player.HandsWon; + lastHandsLost = player.HandsLost; + lastHandsPushed = player.HandsPushed; + lastHandsDoubled = player.HandsDoubled; + lastHandsSplit = player.HandsSplit; + lastHandsBlackjacked = player.HandsBlackjacked; + lastInsuranceWins = player.InsuranceWon; + lastInsuranceLosses = player.InsuranceLost; + + StringBuilder[] total, local; + lock (segments) + { + total = GenerateGraph(segments.Count >= 10000 ? segments[^10000..] : segments, 40, 15); + local = GenerateGraph(segments.Count >= 40 ? segments[^40..] : segments, 40, 15); + } + RenderTwoGraphs(total, local, 1, 40, 40); + } + + public static StringBuilder[] GenerateGraph(IEnumerable segments, int width, + int height, bool render = false) + { + StringBuilder[] result = new StringBuilder[height]; + Dictionary> points = []; + + // Locate min and max of the graph. + double maxMoney = int.MinValue; + double minMoney = int.MaxValue; + int count = 0; + foreach (GraphSegment seg in segments) + { + if (seg.money > maxMoney) maxMoney = seg.money; + else if (seg.money < minMoney) minMoney = seg.money; + count++; + } + + // Converts the graph segments into list of X points by Y. + // Used for the stringbuilders, since they're grouped by Y + // rather than X. + int index = 0; + foreach (GraphSegment seg in segments) + { + // Change lerp scale. + int x = (int)((double)index / count * width); + int y = (int)((seg.money - minMoney) / (maxMoney - minMoney) * (height * 2)); + + y = height * 2 - y; + + if (points.TryGetValue(y, out List<(int x, int d)>? p)) p.Add((x, Math.Sign(seg.delta))); + else points.Add(y, [(x, Math.Sign(seg.delta))]); + index++; + } + + // Sort the lists. Slows it down a little but I got issues with negative spacing. + foreach (KeyValuePair> xVals in points) + xVals.Value.Sort((a, b) => a.x.CompareTo(b.x)); + + // Generate the lines. Two sets of y-values can fit on each + // line, so that's what we'll do. + for (int i = 0; i < height; i++) + { + StringBuilder line = new("\x1b[0m"); // Start with reset. + List<(int x, int d)> upper, lower; + if (!points.TryGetValue(i * 2, out upper!)) upper = []; + if (!points.TryGetValue(i * 2 + 1, out lower!)) lower = []; + + int curPos = 0; + + // The color only changes when we hit a point. + List<(int x, int d)> totalHits = [.. upper.Concat(lower).Distinct(DirectionComparer.Instance)]; + foreach ((int x, int d) tuple in totalHits) + { + if (curPos > tuple.x) continue; // Why??? + line.Append(new string(' ', tuple.x - curPos)); + bool top = upper.Contains(tuple), bot = lower.Contains(tuple); + + if (tuple.d > 0) line.Append("\x1b[92m"); + else if (tuple.d < 0) line.Append("\x1b[91m"); + else line.Append("\x1b[37m"); + + if (top && bot) line.Append('█'); + else if (top) line.Append('▀'); + else if (bot) line.Append('▄'); + else line.Append(' '); + curPos = tuple.x + 1; + } + line.Append(new string(' ', width - curPos)); + result[i] = line; + } + + if (render) RenderOneGraph(result, width, height); + return result; + } + + public static void RenderOneGraph(StringBuilder[] lines, int width, int height) + { + // Render the graph. + int top = Console.WindowHeight - height - 1; + int left = (Console.WindowWidth - width - 1) / 2; + for (int y = 0; y < height; y++) + { + Console.SetCursorPosition(left, top + y); + Console.Write(lines[y]); + } + } + public static void RenderTwoGraphs(StringBuilder[] linesA, StringBuilder[] linesB, + int spacing, int widthA, int widthB) + { + int height = Math.Max(linesA.Length, linesB.Length); + StringBuilder[] total = new StringBuilder[height]; + + int totalWidth = widthA + spacing + widthB; + + // Combine graphs. + for (int i = 0; i < height; i++) + { + StringBuilder combined; + if (i < linesA.Length) combined = linesA[i]; + else combined = new(new string(' ', widthA)); + combined.Append(new string(' ', spacing)); + if (i < linesB.Length) combined.Append(linesB[i]); + total[i] = combined; + } + // Render the graph. + int top = Console.WindowHeight - height - 1; + int left = (Console.WindowWidth - totalWidth - 1) / 2; + for (int y = 0; y < height; y++) + { + Console.SetCursorPosition(left, top + y); + Console.Write(total[y]); + } + } + + public class DirectionComparer : IEqualityComparer<(int x, int d)> + { + public static readonly DirectionComparer Instance = new(); + + public bool Equals((int x, int d) a, (int x, int d) b) => a.x == b.x; + public int GetHashCode((int x, int d) item) => + item.x.GetHashCode() ^ item.d.GetHashCode(); + } + + public readonly struct GraphSegment + { + public readonly double money; + public readonly double delta; + + public GraphSegment(double money, double delta) + { + this.money = money; + this.delta = delta; + } + } +} diff --git a/BlackjackSim/BlackjackSim/Shoe.cs b/BlackjackSim/BlackjackSim/Shoe.cs new file mode 100644 index 0000000..fa22294 --- /dev/null +++ b/BlackjackSim/BlackjackSim/Shoe.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace BlackjackSim; + +public class Shoe +{ + public int TotalDecks => decks; + public int TotalCards => 52 * decks; + public int CardsTaken => 52 * decks - cards.Count; + public int CardsRemaining => cards.Count; + + private readonly List cards; + private readonly int decks; + private readonly Random rand; + + public Shoe(int decks) + { + this.decks = decks; + cards = []; + rand = new(); + + for (int i = 0; i < decks; i++) cards.AddRange(Card.GetDeck()); + + // SHUFFLE: pick a random index out of the cards that haven't been + // shuffled and add it to the bottom of the list. + int notShuffled = cards.Count; + while (notShuffled > 0) + { + int index = rand.Next(notShuffled); + Card c = cards[index]; + cards.RemoveAt(index); + cards.Add(c); + notShuffled--; + } + } + + public Card Get() + { + Card card = cards[0]; + cards.RemoveAt(0); + return card; + } +} diff --git a/BlackjackSim/BlackjackSim/SuitKind.cs b/BlackjackSim/BlackjackSim/SuitKind.cs new file mode 100644 index 0000000..6bcce48 --- /dev/null +++ b/BlackjackSim/BlackjackSim/SuitKind.cs @@ -0,0 +1,9 @@ +namespace BlackjackSim; + +public enum SuitKind +{ + Diamonds, + Clubs, + Hearts, + Spades, +} diff --git a/BlackjackSim/BlackjackSim/ValueKind.cs b/BlackjackSim/BlackjackSim/ValueKind.cs new file mode 100644 index 0000000..0f856e0 --- /dev/null +++ b/BlackjackSim/BlackjackSim/ValueKind.cs @@ -0,0 +1,18 @@ +namespace BlackjackSim; + +public enum ValueKind +{ + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, + Ten, + Jack, + Queen, + King, + Ace +} diff --git a/README.md b/README.md index 92d6cb0..0b118a8 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,8 @@ I have about 1-2 weeks for each project. Check the Git commits for specific date - No additional libraries were used, only the built in TI libraries and the [TI CE toolchain](https://github.com/CE-Programming/toolchain). - Doesn't run great. It uses 16-bit color mode, so the graphics are somewhat slow to render. I attempted to find the way to switch to 4-bit color mode, but I didn't find enough useful info (the best I've found so far is to dissect the GraphX assembly code). Still runs decent though, I've made as many optimizations as I easily could with the renderer, and everything else is fast. - **WARNING**: This one can't be built without reconfiguring the `.clangd` file to include the path to the toolchain. +- Blackjack/ + - This program simulates casino blackjack rules. It allows for customizable player strategies. + - I made a few default strategies (draw until 17, simple card counting, simple probabilities). + - No additional libraries were used. + - It has two custom-rendered graphs on the console display. I haven't figured out how to use XTerm yet, so I'm generating individual characters.