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.