Added the blackjack simulator.

This commit is contained in:
That_One_Nerd 2024-09-18 11:28:04 -04:00
parent 622fe06e25
commit 884f51108f
22 changed files with 1238 additions and 0 deletions

3
.gitignore vendored
View File

@ -4,7 +4,10 @@
# Build files. # Build files.
*/bin/ */bin/
*/obj/ */obj/
*/*/bin/
*/*/obj/
convimg.out convimg.out
# Other stuff. # Other stuff.
.DS_Store .DS_Store
*/.vs/

View File

@ -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

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,72 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace BlackjackSim;
public readonly struct Card : IEquatable<Card>
{
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<SuitKind>();
ValueKind[] values = Enum.GetValues<ValueKind>();
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<SuitKind, ValueKind> tuple) =>
new(tuple.Item1, tuple.Item2);
public static implicit operator Card(ValueTuple<ValueKind, SuitKind> tuple) =>
new(tuple.Item1, tuple.Item2);
}

View File

@ -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();
}

View File

@ -0,0 +1,13 @@
namespace BlackjackSim.Dealers;
public class DealerStandard : DealerBase
{
public override void OnGameBegin(Game game)
{
}
public override bool ShouldResetShoe()
{
return false;
}
}

View File

@ -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<Type, int> 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, int> 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<PlayerBase, List<Hand>> playerHands = [];
List<Hand> 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<Hand>? hands)) hands.Add(hand);
else
{
List<Hand> 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<PlayerBase, List<Hand>> ph in playerHands)
{
PlayerBase player = ph.Key;
List<Hand> 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;
}
}
}

View File

@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
namespace BlackjackSim;
public class Hand
{
public IPerson player;
public List<Card> 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
}

View File

@ -0,0 +1,3 @@
namespace BlackjackSim;
public interface IPerson;

View File

@ -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<Hand> 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) { }
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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<Card> 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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,134 @@
using System.Collections.Generic;
namespace BlackjackSim.Players;
public class PlayerTabular : PlayerBase
{
public double BetSize { get; set; } = 100;
public Dictionary<int, Choice[]> 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<ValueCombined, Choice[]> 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<ValueCombined, Choice[]> 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.
};
}

View File

@ -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<GraphSegment> 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<GraphSegment> 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<GraphSegment> 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<GraphSegment> segments, int width,
int height, bool render = false)
{
StringBuilder[] result = new StringBuilder[height];
Dictionary<int, List<(int x, int d)>> 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<int, List<(int x, int d)>> 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;
}
}
}

View File

@ -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<Card> 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;
}
}

View File

@ -0,0 +1,9 @@
namespace BlackjackSim;
public enum SuitKind
{
Diamonds,
Clubs,
Hearts,
Spades,
}

View File

@ -0,0 +1,18 @@
namespace BlackjackSim;
public enum ValueKind
{
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Jack,
Queen,
King,
Ace
}

View File

@ -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). - 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. - 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. - **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.