Merge pull request #770 from drewjcooper/csharp-71-poker

csharp 71 poker
This commit is contained in:
Jeff Atwood
2022-07-01 11:43:22 -07:00
committed by GitHub
24 changed files with 936 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
namespace Poker.Cards;
internal record struct Card (Rank Rank, Suit Suit)
{
public override string ToString() => $"{Rank} of {Suit}";
public static bool operator <(Card x, Card y) => x.Rank < y.Rank;
public static bool operator >(Card x, Card y) => x.Rank > y.Rank;
public static int operator -(Card x, Card y) => x.Rank - y.Rank;
}

View File

@@ -0,0 +1,28 @@
using static Poker.Cards.Rank;
namespace Poker.Cards;
internal class Deck
{
private readonly Card[] _cards;
private int _nextCard;
public Deck()
{
_cards = Ranks.SelectMany(r => Enum.GetValues<Suit>().Select(s => new Card(r, s))).ToArray();
}
public void Shuffle(IRandom _random)
{
for (int i = 0; i < _cards.Length; i++)
{
var j = _random.Next(_cards.Length);
(_cards[i], _cards[j]) = (_cards[j], _cards[i]);
}
_nextCard = 0;
}
public Card DealCard() => _cards[_nextCard++];
public Hand DealHand() => new Hand(Enumerable.Range(0, 5).Select(_ => DealCard()));
}

View File

@@ -0,0 +1,131 @@
using System.Text;
using static Poker.Cards.HandRank;
namespace Poker.Cards;
internal class Hand
{
public static readonly Hand Empty = new Hand();
private readonly Card[] _cards;
private Hand()
{
_cards = Array.Empty<Card>();
Rank = None;
}
public Hand(IEnumerable<Card> cards)
: this(cards, isAfterDraw: false)
{
}
private Hand(IEnumerable<Card> cards, bool isAfterDraw)
{
_cards = cards.ToArray();
(Rank, HighCard, KeepMask) = Analyze();
IsWeak = Rank < PartialStraight
|| Rank == PartialStraight && isAfterDraw
|| Rank <= TwoPair && HighCard.Rank <= 6;
}
public string Name => Rank.ToString(HighCard);
public HandRank Rank { get; }
public Card HighCard { get; }
public int KeepMask { get; set; }
public bool IsWeak { get; }
public Hand Replace(int cardNumber, Card newCard)
{
if (cardNumber < 1 || cardNumber > _cards.Length) { return this; }
_cards[cardNumber - 1] = newCard;
return new Hand(_cards, isAfterDraw: true);
}
private (HandRank, Card, int) Analyze()
{
var suitMatchCount = 0;
for (var i = 0; i < _cards.Length; i++)
{
if (i < _cards.Length-1 && _cards[i].Suit == _cards[i+1].Suit)
{
suitMatchCount++;
}
}
if (suitMatchCount == 4)
{
return (Flush, _cards[0], 0b11111);
}
var sortedCards = _cards.OrderBy(c => c.Rank).ToArray();
var handRank = Schmaltz;
var keepMask = 0;
Card highCard = default;
for (var i = 0; i < sortedCards.Length - 1; i++)
{
var matchesNextCard = sortedCards[i].Rank == sortedCards[i+1].Rank;
var matchesPreviousCard = i > 0 && sortedCards[i].Rank == sortedCards[i - 1].Rank;
if (matchesNextCard)
{
keepMask |= 0b11 << i;
highCard = sortedCards[i];
handRank = matchesPreviousCard switch
{
_ when handRank < Pair => Pair,
true when handRank == Pair => Three,
_ when handRank == Pair => TwoPair,
_ when handRank == TwoPair => FullHouse,
true => Four,
_ => FullHouse
};
}
}
if (keepMask == 0)
{
if (sortedCards[3] - sortedCards[0] == 3)
{
keepMask=0b1111;
handRank=PartialStraight;
}
if (sortedCards[4] - sortedCards[1] == 3)
{
if (handRank == PartialStraight)
{
return (Straight, sortedCards[4], 0b11111);
}
handRank=PartialStraight;
keepMask=0b11110;
}
}
return handRank < PartialStraight
? (Schmaltz, sortedCards[4], 0b11000)
: (handRank, highCard, keepMask);
}
public override string ToString()
{
var sb = new StringBuilder();
for (var i = 0; i < _cards.Length; i++)
{
var cardDisplay = $" {i+1} -- {_cards[i]}";
// Emulates the effect of the BASIC PRINT statement using the ',' to align text to 14-char print zones
sb.Append(cardDisplay.PadRight(cardDisplay.Length + 14 - cardDisplay.Length % 14));
if (i % 2 == 1)
{
sb.AppendLine();
}
}
sb.AppendLine();
return sb.ToString();
}
public static bool operator >(Hand x, Hand y) =>
x.Rank > y.Rank ||
x.Rank == y.Rank && x.HighCard > y.HighCard;
public static bool operator <(Hand x, Hand y) =>
x.Rank < y.Rank ||
x.Rank == y.Rank && x.HighCard < y.HighCard;
}

View File

@@ -0,0 +1,34 @@
namespace Poker.Cards;
internal class HandRank
{
public static HandRank None = new(0, "");
public static HandRank Schmaltz = new(1, "schmaltz, ", c => $"{c.Rank} high");
public static HandRank PartialStraight = new(2, ""); // The original code does not assign a display string here
public static HandRank Pair = new(3, "a pair of ", c => $"{c.Rank}'s");
public static HandRank TwoPair = new(4, "two pair, ", c => $"{c.Rank}'s");
public static HandRank Three = new(5, "three ", c => $"{c.Rank}'s");
public static HandRank Straight = new(6, "straight", c => $"{c.Rank} high");
public static HandRank Flush = new(7, "a flush in ", c => c.Suit.ToString());
public static HandRank FullHouse = new(8, "full house, ", c => $"{c.Rank}'s");
public static HandRank Four = new(9, "four ", c => $"{c.Rank}'s");
// The original code does not detect a straight flush or royal flush
private readonly int _value;
private readonly string _displayName;
private readonly Func<Card, string> _suffixSelector;
private HandRank(int value, string displayName, Func<Card, string>? suffixSelector = null)
{
_value = value;
_displayName = displayName;
_suffixSelector = suffixSelector ?? (_ => "");
}
public string ToString(Card highCard) => $"{_displayName}{_suffixSelector.Invoke(highCard)}";
public static bool operator >(HandRank x, HandRank y) => x._value > y._value;
public static bool operator <(HandRank x, HandRank y) => x._value < y._value;
public static bool operator >=(HandRank x, HandRank y) => x._value >= y._value;
public static bool operator <=(HandRank x, HandRank y) => x._value <= y._value;
}

View File

@@ -0,0 +1,50 @@
namespace Poker.Cards;
internal struct Rank : IComparable<Rank>
{
public static IEnumerable<Rank> Ranks => new[]
{
Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King, Ace
};
public static Rank Two = new(2);
public static Rank Three = new(3);
public static Rank Four = new(4);
public static Rank Five = new(5);
public static Rank Six = new(6);
public static Rank Seven = new(7);
public static Rank Eight = new(8);
public static Rank Nine = new(9);
public static Rank Ten = new(10);
public static Rank Jack = new(11, "Jack");
public static Rank Queen = new(12, "Queen");
public static Rank King = new(13, "King");
public static Rank Ace = new(14, "Ace");
private readonly int _value;
private readonly string _name;
private Rank(int value, string? name = null)
{
_value = value;
_name = name ?? $" {value} ";
}
public override string ToString() => _name;
public int CompareTo(Rank other) => this - other;
public static bool operator <(Rank x, Rank y) => x._value < y._value;
public static bool operator >(Rank x, Rank y) => x._value > y._value;
public static bool operator ==(Rank x, Rank y) => x._value == y._value;
public static bool operator !=(Rank x, Rank y) => x._value != y._value;
public static int operator -(Rank x, Rank y) => x._value - y._value;
public static bool operator <=(Rank rank, int value) => rank._value <= value;
public static bool operator >=(Rank rank, int value) => rank._value >= value;
public override bool Equals(object? obj) => obj is Rank other && this == other;
public override int GetHashCode() => _value.GetHashCode();
}

View File

@@ -0,0 +1,9 @@
namespace Poker.Cards;
internal enum Suit
{
Clubs,
Diamonds,
Hearts,
Spades
}

33
71_Poker/csharp/Game.cs Normal file
View File

@@ -0,0 +1,33 @@
using Poker.Cards;
using Poker.Players;
using Poker.Resources;
namespace Poker;
internal class Game
{
private readonly IReadWrite _io;
private readonly IRandom _random;
public Game(IReadWrite io, IRandom random)
{
_io = io;
_random = random;
}
internal void Play()
{
_io.Write(Resource.Streams.Title);
_io.Write(Resource.Streams.Instructions);
var deck = new Deck();
var human = new Human(200, _io);
var computer = new Computer(200, _io, _random);
var table = new Table(_io, _random, deck, human, computer);
do
{
table.PlayHand();
} while (table.ShouldPlayAnotherHand());
}
}

View File

@@ -0,0 +1,48 @@
using Poker.Strategies;
using static System.StringComparison;
namespace Poker;
internal static class IReadWriteExtensions
{
internal static bool ReadYesNo(this IReadWrite io, string prompt)
{
while (true)
{
var response = io.ReadString(prompt);
if (response.Equals("YES", InvariantCultureIgnoreCase)) { return true; }
if (response.Equals("NO", InvariantCultureIgnoreCase)) { return false; }
io.WriteLine("Answer Yes or No, please.");
}
}
internal static float ReadNumber(this IReadWrite io) => io.ReadNumber("");
internal static int ReadNumber(this IReadWrite io, string prompt, int max, string maxPrompt)
{
io.Write(prompt);
while (true)
{
var response = io.ReadNumber();
if (response <= max) { return (int)response; }
io.WriteLine(maxPrompt);
}
}
internal static Strategy ReadHumanStrategy(this IReadWrite io, bool noCurrentBets)
{
while(true)
{
io.WriteLine();
var bet = io.ReadNumber("What is your bet");
if (bet != (int)bet)
{
if (noCurrentBets && bet == .5) { return Strategy.Check; }
io.WriteLine("No small change, please.");
continue;
}
if (bet == 0) { return Strategy.Fold; }
return Strategy.Bet(bet);
}
}
}

View File

@@ -0,0 +1,130 @@
using Poker.Cards;
using Poker.Strategies;
using static System.StringComparison;
namespace Poker.Players;
internal class Computer : Player
{
private readonly IReadWrite _io;
private readonly IRandom _random;
public Computer(int bank, IReadWrite io, IRandom random)
: base(bank)
{
_io = io;
_random = random;
Strategy = Strategy.None;
}
public Strategy Strategy { get; set; }
public override void NewHand()
{
base.NewHand();
Strategy = (Hand.IsWeak, Hand.Rank < HandRank.Three, Hand.Rank < HandRank.FullHouse) switch
{
(true, _, _) when _random.Next(10) < 2 => Strategy.Bluff(23, 0b11100),
(true, _, _) when _random.Next(10) < 2 => Strategy.Bluff(23, 0b11110),
(true, _, _) when _random.Next(10) < 1 => Strategy.Bluff(23, 0b11111),
(true, _, _) => Strategy.Fold,
(false, true, _) => _random.Next(10) < 2 ? Strategy.Bluff(23) : Strategy.Check,
(false, false, true) => Strategy.Bet(35),
(false, false, false) => _random.Next(10) < 1 ? Strategy.Bet(35) : Strategy.Raise
};
}
protected override void DrawCards(Deck deck)
{
var keepMask = Strategy.KeepMask ?? Hand.KeepMask;
var count = 0;
for (var i = 1; i <= 5; i++)
{
if ((keepMask & (1 << (i - 1))) == 0)
{
Hand = Hand.Replace(i, deck.DealCard());
count++;
}
}
_io.WriteLine();
_io.Write($"I am taking {count} card");
if (count != 1)
{
_io.WriteLine("s");
}
Strategy = (Hand.IsWeak, Hand.Rank < HandRank.Three, Hand.Rank < HandRank.FullHouse) switch
{
_ when Strategy is Bluff => Strategy.Bluff(28),
(true, _, _) => Strategy.Fold,
(false, true, _) => _random.Next(10) == 0 ? Strategy.Bet(19) : Strategy.Raise,
(false, false, true) => _random.Next(10) == 0 ? Strategy.Bet(11) : Strategy.Bet(19),
(false, false, false) => Strategy.Raise
};
}
public int GetWager(int wager)
{
wager += _random.Next(10);
if (Balance < Table.Human.Bet + wager)
{
if (Table.Human.Bet == 0) { return Balance; }
if (Balance >= Table.Human.Bet)
{
_io.WriteLine("I'll see you.");
Bet = Table.Human.Bet;
Table.CollectBets();
}
else
{
RaiseFunds();
}
}
return wager;
}
public bool TryBuyWatch()
{
if (!Table.Human.HasWatch) { return false; }
var response = _io.ReadString("Would you like to sell your watch");
if (response.StartsWith("N", InvariantCultureIgnoreCase)) { return false; }
var (value, message) = (_random.Next(10) < 7) switch
{
true => (75, "I'll give you $75 for it."),
false => (25, "That's a pretty crummy watch - I'll give you $25.")
};
_io.WriteLine(message);
Table.Human.SellWatch(value);
// The original code does not have the computer part with any money
return true;
}
public void RaiseFunds()
{
if (Table.Human.HasWatch) { return; }
var response = _io.ReadString("Would you like to buy back your watch for $50");
if (response.StartsWith("N", InvariantCultureIgnoreCase)) { return; }
// The original code does not deduct $50 from the player
Balance += 50;
Table.Human.ReceiveWatch();
IsBroke = true;
}
public void CheckFunds() { IsBroke = Balance <= Table.Ante; }
public override void TakeWinnings()
{
_io.WriteLine("I win.");
base.TakeWinnings();
}
}

View File

@@ -0,0 +1,90 @@
using Poker.Cards;
using Poker.Strategies;
namespace Poker.Players;
internal class Human : Player
{
private readonly IReadWrite _io;
public Human(int bank, IReadWrite io)
: base(bank)
{
HasWatch = true;
_io = io;
}
public bool HasWatch { get; set; }
protected override void DrawCards(Deck deck)
{
var count = _io.ReadNumber("How many cards do you want", 3, "You can't draw more than three cards.");
if (count == 0) { return; }
_io.WriteLine("What are their numbers:");
for (var i = 1; i <= count; i++)
{
Hand = Hand.Replace((int)_io.ReadNumber(), deck.DealCard());
}
_io.WriteLine("Your new hand:");
_io.Write(Hand);
}
internal bool SetWager()
{
var strategy = _io.ReadHumanStrategy(Table.Computer.Bet == 0 && Bet == 0);
if (strategy is Strategies.Bet or Check)
{
if (Bet + strategy.Value < Table.Computer.Bet)
{
_io.WriteLine("If you can't see my bet, then fold.");
return false;
}
if (Balance - Bet - strategy.Value >= 0)
{
HasBet = true;
Bet += strategy.Value;
return true;
}
RaiseFunds();
}
else
{
Fold();
Table.CollectBets();
}
return false;
}
public void RaiseFunds()
{
_io.WriteLine();
_io.WriteLine("You can't bet with what you haven't got.");
if (Table.Computer.TryBuyWatch()) { return; }
// The original program had some code about selling a tie tack, but due to a fault
// in the logic the code was unreachable. I've omitted it in this port.
IsBroke = true;
}
public void ReceiveWatch()
{
// In the original code the player does not pay any money to receive the watch back.
HasWatch = true;
}
public void SellWatch(int amount)
{
HasWatch = false;
Balance += amount;
}
public override void TakeWinnings()
{
_io.WriteLine("You win.");
base.TakeWinnings();
}
}

View File

@@ -0,0 +1,59 @@
using Poker.Cards;
namespace Poker.Players;
internal abstract class Player
{
private Table? _table;
private bool _hasFolded;
protected Player(int bank)
{
Hand = Hand.Empty;
Balance = bank;
}
public Hand Hand { get; set; }
public int Balance { get; set; }
public bool HasBet { get; set; }
public int Bet { get; set; }
public bool HasFolded => _hasFolded;
public bool IsBroke { get; protected set; }
protected Table Table =>
_table ?? throw new InvalidOperationException("The player must be sitting at the table.");
public void Sit(Table table) => _table = table;
public virtual void NewHand()
{
Bet = 0;
Hand = Table.Deck.DealHand();
_hasFolded = false;
}
public int AnteUp()
{
Balance -= Table.Ante;
return Table.Ante;
}
public void DrawCards()
{
Bet = 0;
DrawCards(Table.Deck);
}
protected abstract void DrawCards(Deck deck);
public virtual void TakeWinnings()
{
Balance += Table.Pot;
Table.Pot = 0;
}
public void Fold()
{
_hasFolded = true;
}
}

View File

@@ -6,4 +6,12 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/*.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\00_Common\dotnet\Games.Common\Games.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,5 @@
global using Games.Common.IO;
global using Games.Common.Randomness;
global using Poker;
new Game(new ConsoleIO(), new RandomNumberGenerator()).Play();

View File

@@ -0,0 +1,5 @@
Welcome to the casino. We each have $200.
I will open the betting before the draw; you open after.
To fold bet 0; to check bet .5.
Enough talk -- Let's get down to business.

View File

@@ -0,0 +1,17 @@
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Poker.Resources;
internal static class Resource
{
internal static class Streams
{
public static Stream Instructions => GetStream();
public static Stream Title => GetStream();
}
private static Stream GetStream([CallerMemberName] string? name = null)
=> Assembly.GetExecutingAssembly().GetManifestResourceStream($"Poker.Resources.{name}.txt")
?? throw new ArgumentException($"Resource stream {name} does not exist", nameof(name));
}

View File

@@ -0,0 +1,5 @@
Poker
Creative Computing Morristown, New Jersey

View File

@@ -0,0 +1,8 @@
namespace Poker.Strategies;
internal class Bet : Strategy
{
public Bet(int amount) => Value = amount;
public override int Value { get; }
}

View File

@@ -0,0 +1,12 @@
namespace Poker.Strategies;
internal class Bluff : Bet
{
public Bluff(int amount, int? keepMask)
: base(amount)
{
KeepMask = keepMask;
}
public override int? KeepMask { get; }
}

View File

@@ -0,0 +1,6 @@
namespace Poker.Strategies;
internal class Check : Strategy
{
public override int Value => 0;
}

View File

@@ -0,0 +1,6 @@
namespace Poker.Strategies;
internal class Fold : Strategy
{
public override int Value => -1;
}

View File

@@ -0,0 +1,6 @@
namespace Poker.Strategies;
internal class None : Strategy
{
public override int Value => -1;
}

View File

@@ -0,0 +1,6 @@
namespace Poker.Strategies;
internal class Raise : Bet
{
public Raise() : base(2) { }
}

View File

@@ -0,0 +1,15 @@
namespace Poker.Strategies;
internal abstract class Strategy
{
public static Strategy None = new None();
public static Strategy Fold = new Fold();
public static Strategy Check = new Check();
public static Strategy Raise = new Raise();
public static Strategy Bet(float amount) => new Bet((int)amount);
public static Strategy Bet(int amount) => new Bet(amount);
public static Strategy Bluff(int amount, int? keepMask = null) => new Bluff(amount, keepMask);
public abstract int Value { get; }
public virtual int? KeepMask { get; }
}

214
71_Poker/csharp/Table.cs Normal file
View File

@@ -0,0 +1,214 @@
using Poker.Cards;
using Poker.Players;
using Poker.Strategies;
namespace Poker;
internal class Table
{
private readonly IReadWrite _io;
private readonly IRandom _random;
public int Pot;
public Table(IReadWrite io, IRandom random, Deck deck, Human human, Computer computer)
{
_io = io;
_random = random;
Deck = deck;
Human = human;
Computer = computer;
human.Sit(this);
computer.Sit(this);
}
public int Ante { get; } = 5;
public Deck Deck { get; }
public Human Human { get; }
public Computer Computer { get; }
internal void PlayHand()
{
while (true)
{
_io.WriteLine();
Computer.CheckFunds();
if (Computer.IsBroke) { return; }
_io.WriteLine($"The ante is ${Ante}. I will deal:");
_io.WriteLine();
if (Human.Balance <= Ante)
{
Human.RaiseFunds();
if (Human.IsBroke) { return; }
}
Deal(_random);
_io.WriteLine();
GetWagers("I'll open with ${0}", "I check.", allowRaiseAfterCheck: true);
if (SomeoneIsBroke() || SomeoneHasFolded()) { return; }
Draw();
GetWagers();
if (SomeoneIsBroke()) { return; }
if (!Human.HasBet)
{
GetWagers("I'll bet ${0}", "I'll check");
}
if (SomeoneIsBroke() || SomeoneHasFolded()) { return; }
if (GetWinner() is { } winner)
{
winner.TakeWinnings();
return;
}
}
}
private void Deal(IRandom random)
{
Deck.Shuffle(random);
Pot = Human.AnteUp() + Computer.AnteUp();
Human.NewHand();
Computer.NewHand();
_io.WriteLine("Your hand:");
_io.Write(Human.Hand);
}
private void Draw()
{
_io.WriteLine();
_io.Write("Now we draw -- ");
Human.DrawCards();
Computer.DrawCards();
_io.WriteLine();
}
private void GetWagers(string betFormat, string checkMessage, bool allowRaiseAfterCheck = false)
{
if (Computer.Strategy is Bet)
{
Computer.Bet = Computer.GetWager(Computer.Strategy.Value);
if (Computer.IsBroke) { return; }
_io.WriteLine(betFormat, Computer.Bet);
}
else
{
_io.WriteLine(checkMessage);
if (!allowRaiseAfterCheck) { return; }
}
GetWagers();
}
private void GetWagers()
{
while (true)
{
Human.HasBet = false;
while (true)
{
if (Human.SetWager()) { break; }
if (Human.IsBroke || Human.HasFolded) { return; }
}
if (Human.Bet == Computer.Bet)
{
CollectBets();
return;
}
if (Computer.Strategy is Fold)
{
if (Human.Bet > 5)
{
Computer.Fold();
_io.WriteLine("I fold.");
return;
}
}
if (Human.Bet > 3 * Computer.Strategy.Value)
{
if (Computer.Strategy is not Raise)
{
_io.WriteLine("I'll see you.");
Computer.Bet = Human.Bet;
CollectBets();
return;
}
}
var raise = Computer.GetWager(Human.Bet - Computer.Bet);
if (Computer.IsBroke) { return; }
_io.WriteLine($"I'll see you, and raise you {raise}");
Computer.Bet = Human.Bet + raise;
}
}
internal void CollectBets()
{
Human.Balance -= Human.Bet;
Computer.Balance -= Computer.Bet;
Pot += Human.Bet + Computer.Bet;
}
private bool SomeoneHasFolded()
{
if (Human.HasFolded)
{
_io.WriteLine();
Computer.TakeWinnings();
}
else if (Computer.HasFolded)
{
_io.WriteLine();
Human.TakeWinnings();
}
else
{
return false;
}
Pot = 0;
return true;
}
private bool SomeoneIsBroke() => Human.IsBroke || Computer.IsBroke;
private Player? GetWinner()
{
_io.WriteLine();
_io.WriteLine("Now we compare hands:");
_io.WriteLine("My hand:");
_io.Write(Computer.Hand);
_io.WriteLine();
_io.WriteLine($"You have {Human.Hand.Name}");
_io.WriteLine($"and I have {Computer.Hand.Name}");
if (Computer.Hand > Human.Hand) { return Computer; }
if (Human.Hand > Computer.Hand) { return Human; }
_io.WriteLine("The hand is drawn.");
_io.WriteLine($"All $ {Pot} remains in the pot.");
return null;
}
internal bool ShouldPlayAnotherHand()
{
if (Computer.IsBroke)
{
_io.WriteLine("I'm busted. Congratulations!");
return true;
}
if (Human.IsBroke)
{
_io.WriteLine("Your wad is shot. So long, sucker!");
return true;
}
_io.WriteLine($"Now I have $ {Computer.Balance} and you have $ {Human.Balance}");
return _io.ReadYesNo("Do you wish to continue");
}
}