WIP deck methods, added flake, and setup build deps

This commit is contained in:
2024-07-05 08:49:59 -06:00
commit 777e07035b
10 changed files with 716 additions and 0 deletions
+437
View File
@@ -0,0 +1,437 @@
const std = @import("std");
const assert = std.debug.assert;
pub const DeckError = error{
DuplicateDiscard,
Overflow,
};
pub const Card = struct { suite: Suit, faceValue: Face };
pub const Suit = enum(u4) { Diamonds = 0, Clubs = 1, Hearts = 2, Spades = 3 };
pub const Face = enum(u4) {
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6,
Seven = 7,
Eight = 8,
Nine = 9,
Ten = 10,
Jack = 11,
Queen = 12,
King = 13,
Ace = 14,
};
comptime {
if (@typeInfo(Suit).Enum.fields.len != 4) {
@compileError("Only four suites allowed");
}
if (@typeInfo(Face).Enum.fields.len != 13) {
@compileError("Only 13 face cards permitted");
}
}
const NUM_SUITES = @typeInfo(Suit).Enum.fields.len;
const NUM_CARDS_IN_SUITE = @typeInfo(Face).Enum.fields.len;
const MAX_CARDS = NUM_SUITES * NUM_CARDS_IN_SUITE;
const CARD_STRUCT_SIZE = @sizeOf(Card) / 2;
/// A Bounded Array with a fixed size to fit 52 `Card` structs inside it.
/// Requires no allocations because of the fixed max size known at compile time.
// pub fn printCards(cards: CardSlice, options: bufPrintCardOptions) void {
// std.debug.print("Deck with {d} cards\tBuf len: {d}\n", .{ cards.len, cards.buffer.len });
// std.debug.print("--- Bottom of Deck ---\n", .{});
// var buf: [18]u8 = undefined;
// for (0..cards.len) |cardIdx| {
// const card = cards.get(cardIdx);
// std.debug.print(" - {d}: {s}\n", .{ cardIdx, bufPrintCard(card, &buf, options) });
// }
// std.debug.print("--- Top of Deck ---\n", .{});
// }
pub fn printDeck(deck: Deck, options: bufPrintCardOptions) void {
std.debug.print("Deck with {d} cards\n", .{deck.num_cards});
std.debug.print("--- Bottom of Deck ---\n", .{});
var buf: [18]u8 = undefined;
for (0..deck.num_cards) |cardIdx| {
std.debug.print(" - {d}: {s}\n", .{ cardIdx, bufPrintCard(deck.cards[cardIdx], &buf, options) });
}
std.debug.print("--- Top of Deck ---\n", .{});
}
pub const bufPrintCardOptions = struct {
use_icon: bool = true,
};
/// Returns a string representation of the card in the `buffer`
pub fn bufPrintCard(card: Card, buffer: []u8, options: bufPrintCardOptions) []const u8 {
// std.debug.print("\n\t{any}", .{card});
const fv = @tagName(card.faceValue);
const icon = if (options.use_icon) suiteToIcon(card.suite) else @tagName(card.suite);
return std.fmt.bufPrint(buffer, "{s} of {s}", .{ fv, icon }) catch return "*err printing card";
}
fn suiteToIcon(s: Suit) []const u8 {
return switch (s) {
.Spades => "󰣑",
.Hearts => "",
.Clubs => "󰣎",
.Diamonds => "󰣏",
};
}
pub const Deck = struct {
num_cards: u8 = 0,
num_discards: u8 = 0,
cb1: [MAX_CARDS]Card = [_]Card{Card{ .suite = .Diamonds, .faceValue = .Two }} ** MAX_CARDS,
cb2: [MAX_CARDS]Card = [_]Card{Card{ .suite = .Diamonds, .faceValue = .Two }} ** MAX_CARDS,
cards: []Card = undefined,
discard_pile: []Card = undefined,
pub fn init(self: *Deck) DeckError!void {
// var cardBuf = [_]Card{Card{ .suite = .Diamonds, .faceValue = .Two }} ** MAX_CARDS;
self.cards = &self.cb1;
// var discardBuf = [_]Card{Card{ .suite = .Diamonds, .faceValue = .Two }} ** MAX_CARDS;
self.discard_pile = &self.cb2;
// Construct the deck
for (0..NUM_SUITES) |suiteIdx| {
const suite: Suit = @enumFromInt(suiteIdx);
for (0..NUM_CARDS_IN_SUITE) |f| {
const faceIdx = NUM_CARDS_IN_SUITE - f + 1;
const face: Face = @enumFromInt(faceIdx);
self.cards[self.num_cards] = Card{ .suite = suite, .faceValue = face };
self.num_cards += 1;
}
}
assert(self.num_cards == MAX_CARDS);
}
pub fn deal(self: *Deck) ?Card {
if (self.num_cards == 0) return null;
self.num_cards -= 1;
return self.cards[self.num_cards];
}
pub fn peak(self: *Deck) ?Card {
if (self.num_cards == 0) return null;
return self.cards[self.num_cards - 1];
}
pub fn discard(self: *Deck, card: Card) DeckError!void {
// Check a duplicate isnt introduced
for (0..self.num_discards) |discardIdx| {
if (std.meta.eql(self.discard_pile[discardIdx], card)) {
return DeckError.DuplicateDiscard;
}
}
self.discard_pile[self.num_discards] = card;
self.num_discards += 1;
}
pub fn rebuild(self: *Deck) DeckError!void {
const newTotal = self.num_cards + self.num_discards;
assert(newTotal <= MAX_CARDS);
for (0..self.num_discards) |disIdx| {
self.cards[self.num_cards + disIdx];
self.num_cards += 1;
}
self.num_discards = 0;
assert(self.num_cards == newTotal);
try sort(self.cards);
}
pub fn order_deck(self: *Deck) DeckError!void {
try sort(self.cards);
}
fn sort(cards: []Card, numCards: u8) DeckError!void {
if (cards.len <= 1) return;
var cb = [MAX_CARDS]?Card{null} ** MAX_CARDS;
var sortedGapsDeck: []?Card = cb[0..MAX_CARDS];
for (cards) |card| {
const cardIdx = faceValueIdx(card.faceValue) + suiteValueIdx(card.suite);
sortedGapsDeck[cardIdx] = card;
}
var cb2: [MAX_CARDS]Card = undefined;
var sortedGaplessDeck = cb2[0..numCards];
var glIdx: u8 = 0;
for (0..sortedGapsDeck.len) |gIdx| {
if (sortedGapsDeck[gIdx] != null) {
sortedGaplessDeck[glIdx] = sortedGapsDeck[gIdx];
glIdx += 1;
}
}
var newCards = []Card.init(0) catch return DeckError.Overflow;
for (sortedGaplessDeck) |card| {
newCards.append(card) catch return DeckError.Overflow;
}
}
fn cloneDeck(deck: *[]Card) DeckError![]Card {
var newDeck = []Card.init(0) catch return DeckError.Overflow;
for (deck.buffer) |card| {
newDeck.append(card) catch return DeckError.Overflow;
}
return newDeck;
}
test "Deck.cloneDeck" {
var deck = Deck{};
try deck.init();
const clonedCards = try Deck.cloneDeck(&deck.cards);
try std.testing.expectEqual(deck.cards.len, clonedCards.len);
for (deck.cards, 0..) |card, i| {
try std.testing.expectEqual(card, clonedCards[i]);
}
}
fn faceValueIdx(f: Face) u8 {
const negIdx: i16 = @intCast(@intFromEnum(f));
return @intCast(@abs(negIdx - 14));
}
test "Deck.faceValueIdx" {
try std.testing.expectEqual(12, faceValueIdx(Face.Two));
try std.testing.expectEqual(1, faceValueIdx(Face.King));
}
fn suiteValueIdx(s: Suit) u8 {
const cards: u8 = @intCast(NUM_CARDS_IN_SUITE);
return @intFromEnum(s) * cards;
}
// fn mergeSplit(a: *BoundedArrayOfCards, b: *BoundedArrayOfCards, iBegin: u8, iEnd: u8) void {
// if (iEnd - iBegin <= 1) return;
// const iMiddle: u8 = (iBegin + iEnd) / 2;
// mergeSplit(a, b, iBegin, iMiddle);
// mergeSplit(a, b, iMiddle, iEnd);
// merge(b, a, iBegin, iMiddle, iEnd);
// }
// /// Merges two arrays of cards, deck `a` is merged into deck `b`
// fn merge(a: *BoundedArrayOfCards, b: *BoundedArrayOfCards, iBegin: u8, iMiddle: u8, iEnd: u8) void {
// var i = iBegin;
// var j = iMiddle;
// for (iBegin..iEnd) |k| {
// if (i < iMiddle and (j >= iEnd or gt(a.get(j), a.get(i)))) {
// b.set(k, a.get(i));
// i = i + 1;
// } else {
// b.set(k, a.get(j));
// j = j + 1;
// }
// }
// }
/// Greater than comparison function for two cards
/// A card is considered greater, if it is found closer
/// to the top of the deck when set to the default sort order
///
/// Default Order from Top to Bottom is:
/// Suites: Spades > Hearts > Clubs > Diamonds
/// FaceValue: 2 > 10 > Jack > Queen > King > Ace
/// 2 of Spades is on top, Ace of Diamonds is the bottom card
fn gt(l: Card, r: Card) bool {
const lSuite: u4 = @intFromEnum(l.suite);
const lFace: u4 = @intFromEnum(l.faceValue);
const rSuite: u4 = @intFromEnum(r.suite);
const rFace: u4 = @intFromEnum(r.faceValue);
if (lSuite != rSuite) {
return lSuite > rSuite;
}
// reverse comparison for face value, because 3 (3) is > ace (14)
return lFace < rFace;
}
test "Deck.gt" {
const twoHearts = Card{ .suite = .Hearts, .faceValue = .Two };
const kingSpades = Card{ .suite = .Spades, .faceValue = .King };
const aceSpades = Card{ .suite = .Spades, .faceValue = .Ace };
try std.testing.expect(Deck.gt(kingSpades, twoHearts));
try std.testing.expect(Deck.gt(kingSpades, aceSpades));
try std.testing.expect(!Deck.gt(aceSpades, aceSpades));
}
};
test "Deck.init" {
var deck = Deck{};
try deck.init();
try std.testing.expectEqual(MAX_CARDS, deck.num_cards);
try std.testing.expectEqual(Card{ .suite = .Diamonds, .faceValue = .Ace }, deck.cards[0]);
try std.testing.expectEqual(Card{ .suite = .Spades, .faceValue = .Two }, deck.cards[51]);
}
test "Deck.deal" {
var deck = Deck{};
try deck.init();
try std.testing.expectEqual(deck.deal(), Card{ .suite = .Spades, .faceValue = .Two });
try std.testing.expectEqual(51, deck.num_cards);
try std.testing.expectEqual(deck.deal(), Card{ .suite = .Spades, .faceValue = .Three });
try std.testing.expectEqual(50, deck.num_cards);
for (0..50) |_| {
_ = deck.deal();
}
try std.testing.expectEqual(0, deck.num_cards);
try std.testing.expectEqual(null, deck.deal());
}
test "Deck.peak" {
var deck = Deck{};
try deck.init();
try std.testing.expectEqual(Card{ .suite = .Spades, .faceValue = .Two }, deck.peak());
try std.testing.expectEqual(MAX_CARDS, deck.num_cards);
// Deal out 20 cards
for (0..20) |_| {
_ = deck.deal();
}
try std.testing.expectEqual(deck.peak(), Card{ .suite = .Hearts, .faceValue = .Nine });
try std.testing.expectEqual(deck.num_cards, 32);
// Try to peak empty deck
for (0..32) |_| {
_ = deck.deal();
}
try std.testing.expectEqual(null, deck.peak());
}
test "Deck.discard" {
var deck = Deck{};
try deck.init();
const twoSpadesCard = deck.deal();
try std.testing.expect(twoSpadesCard != null);
try std.testing.expect(twoSpadesCard.?.suite == .Spades);
try std.testing.expect(twoSpadesCard.?.faceValue == .Two);
// Deal out 13 cards
for (0..12) |_| {
_ = deck.deal();
}
const twoHeartsCard = deck.deal();
try std.testing.expect(twoHeartsCard != null);
try std.testing.expect(twoHeartsCard.?.suite == .Hearts);
try std.testing.expect(twoHeartsCard.?.faceValue == .Two);
// Discard two cards
try deck.discard(twoSpadesCard.?);
try deck.discard(twoHeartsCard.?);
try std.testing.expectEqual(2, deck.num_discards);
try std.testing.expectEqual(twoSpadesCard.?, deck.discard_pile[0]);
// Fails to add duplicate card
try std.testing.expectError(DeckError.DuplicateDiscard, deck.discard(twoSpadesCard.?));
}
test "Deck.rebuild" {
var deck = Deck{};
try deck.init();
var buf1 = [_]?Card{null} ** 5;
var buf2 = [_]?Card{null} ** 5;
var buf3 = [_]?Card{null} ** 5;
var playerOneHand: []?Card = &(buf1);
var playerTwoHand: []?Card = &(buf2);
var playerThreeHand: []?Card = &(buf3);
// Alternate dealing player one and two hands
for (0..10) |i| {
if (i % 2 == 0) try playerOneHand.append(deck.deal().?) else try playerTwoHand.append(deck.deal().?);
}
try std.testing.expectEqual(5, playerOneHand.len);
try std.testing.expectEqual(5, playerTwoHand.len);
// Discard top 10 cards from deck
for (0..10) |_| {
try deck.discard(deck.deal().?);
}
for (0..5) |i| {
playerThreeHand[i] = deck.deal().?;
}
try std.testing.expectEqual(5, playerThreeHand.len);
try std.testing.expectEqual(10, deck.discard_pile.len);
try std.testing.expectEqual(27, deck.cards.len);
for (0..15) |i| {
switch (i % 3) {
0 => try deck.discard(playerThreeHand.pop()),
1 => try deck.discard(playerOneHand.pop()),
2 => try deck.discard(playerTwoHand.pop()),
else => unreachable,
}
}
try std.testing.expectEqual(25, deck.discard_pile.len);
try deck.rebuild();
try std.testing.expectEqual(0, deck.discard_pile.len);
try std.testing.expectEqual(52, deck.cards.len);
try std.testing.expectEqual(Card{ .suite = .Diamonds, .faceValue = .Ace }, deck.cards[0]);
try std.testing.expectEqual(Card{ .suite = .Spades, .faceValue = .Two }, deck.cards[51]);
}
// test "Deck.rebuild with missing cards" {
// var deck = Deck{};
// try deck.init();
// // var playerOneHand = []Card.init(0) catch unreachable;
// // var playerTwoHand = []Card.init(0) catch unreachable;
// // Alternate dealing player one discarding cards
// for (0..52) |i| {
// switch (i % 4) {
// 0 => try playerTwoHand.append(deck.deal().?),
// 1 => try deck.discard(deck.deal().?),
// 2 => try playerOneHand.append(deck.deal().?),
// 3 => {
// _ = deck.deal();
// },
// else => unreachable,
// }
// }
// try std.testing.expectEqual(13, playerOneHand.len);
// try std.testing.expectEqual(13, playerTwoHand.len);
// try std.testing.expectEqual(13, deck.discard_pile.len);
// try std.testing.expectEqual(0, deck.cards.len);
// for (0..26) |i| {
// switch (i % 2) {
// 0 => try deck.discard(playerOneHand.pop()),
// 1 => try deck.discard(playerTwoHand.pop()),
// else => unreachable,
// }
// }
// try std.testing.expectEqual(39, deck.discard_pile.len);
// try deck.rebuild();
// try std.testing.expectEqual(0, deck.discard_pile.len);
// try std.testing.expectEqual(39, deck.cards.len);
// try std.testing.expectEqual(Card{ .suite = .Diamonds, .faceValue = .Ace }, deck.cards[0]);
// try std.testing.expectEqual(Card{ .suite = .Spades, .faceValue = .Two }, deck.cards[38]);
// }
+6
View File
@@ -0,0 +1,6 @@
const std = @import("std");
const deck = @import("deck.zig");
pub fn main() !void {
std.debug.print("It works!\n", .{});
}