diff --git a/build.zig b/build.zig index 6132fb8..0bd5a44 100644 --- a/build.zig +++ b/build.zig @@ -1,16 +1,11 @@ const std = @import("std"); pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{ - .whitelist = &[_]std.Target.Query{ - std.Target.Query{ .cpu_arch = .aarch64, .os_tag = .macos }, - std.Target.Query{ .cpu_arch = .x86_64, .os_tag = .linux }, - }, - }); + const target = b.standardTargetOptions(.{}); // TODO // Prefer small size binaries for optimization - const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .Debug }); + const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseSmall }); const exe = b.addExecutable(.{ .name = "genius_deck", @@ -25,11 +20,12 @@ pub fn build(b: *std.Build) void { .openssl = false, }); exe.root_module.addImport("zap", zap.module("zap")); + b.installArtifact(exe); // Create Check step for zls const exe_check = b.addExecutable(.{ - .name = "zerver", + .name = "genius_deck", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, diff --git a/src/deck.zig b/src/deck.zig index 70c8394..1b38deb 100644 --- a/src/deck.zig +++ b/src/deck.zig @@ -2,11 +2,12 @@ const std = @import("std"); const assert = std.debug.assert; pub const DeckError = error{ - DuplicateDiscard, - Overflow, + DuplicateInDiscard, + DuplicateInDeck, + OutOfRangeCut, }; -pub const Card = struct { suite: Suit, faceValue: Face }; +pub const Card = struct { suit: Suit, face_value: Face }; pub const Suit = enum(u4) { Diamonds = 0, Clubs = 1, Hearts = 2, Spades = 3 }; @@ -26,40 +27,36 @@ pub const Face = enum(u4) { Ace = 14, }; +pub const ShuffleOptions = struct { + seed: ?u64 = null, +}; + comptime { if (@typeInfo(Suit).Enum.fields.len != 4) { - @compileError("Only four suites allowed"); + @compileError("Only four suits 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; +const NUM_SUITS: u8 = @typeInfo(Suit).Enum.fields.len; +const NUM_CARDS_IN_SUIT: u8 = @typeInfo(Face).Enum.fields.len; +const MAX_CARDS: u8 = NUM_SUITS * NUM_CARDS_IN_SUIT; +const CARD_STRUCT_SIZE: u8 = @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 { +pub fn printDeck(deck: *Deck, options: bufPrintCardOptions) void { std.debug.print("Deck with {d} cards\n", .{deck.num_cards}); + printCards(deck.cards, deck.num_cards, options); + std.debug.print("\nDiscard pile with {d} cards\n", .{deck.num_discards}); + printCards(deck.discard_pile, deck.num_discards, options); +} + +pub fn printCards(cards: []?Card, num_cards: u8, options: bufPrintCardOptions) void { 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) }); + for (0..num_cards) |card_idx| { + std.debug.print(" - {d}: {s}\n", .{ card_idx, bufPrintCard(cards[card_idx].?, &buf, options) }); } std.debug.print("--- Top of Deck ---\n", .{}); } @@ -70,12 +67,12 @@ pub const bufPrintCardOptions = struct { /// 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); + const fv = @tagName(card.face_value); + const icon = if (options.use_icon) suitToIcon(card.suit) else @tagName(card.suit); return std.fmt.bufPrint(buffer, "{s} of {s}", .{ fv, icon }) catch return "*err printing card"; } -fn suiteToIcon(s: Suit) []const u8 { +pub fn suitToIcon(s: Suit) []const u8 { return switch (s) { .Spades => "󰣑", .Hearts => "", @@ -87,117 +84,146 @@ fn suiteToIcon(s: Suit) []const u8 { 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, + _cb1: [MAX_CARDS]?Card = [_]?Card{null} ** MAX_CARDS, + _cb2: [MAX_CARDS]?Card = [_]?Card{null} ** 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; + /// Populates the `Deck.cards` with the default sort order of face cards. + pub fn init(self: *Deck) void { + self.cards = &self._cb1; + 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 }; + for (0..NUM_SUITS) |suit_idx| { + const suit: Suit = @enumFromInt(suit_idx); + for (0..NUM_CARDS_IN_SUIT) |f| { + const face_idx = NUM_CARDS_IN_SUIT - f + 1; + const face: Face = @enumFromInt(face_idx); + self.cards[self.num_cards] = Card{ .suit = suit, .face_value = face }; self.num_cards += 1; } } assert(self.num_cards == MAX_CARDS); } + /// Pops and returns the top card off the deck, `null` if none are left. pub fn deal(self: *Deck) ?Card { if (self.num_cards == 0) return null; self.num_cards -= 1; - return self.cards[self.num_cards]; + const card = self.cards[self.num_cards]; + self.cards[self.num_cards] = null; + return card; } + /// Returns the top card off the deck without affecting the deck, + /// `null` if none are left. pub fn peak(self: *Deck) ?Card { if (self.num_cards == 0) return null; return self.cards[self.num_cards - 1]; } + /// Places the `card` into the `Deck.discard_pile` slice of cards. + /// Returns an error if the card already exists in the `Deck.cards` or + /// `Deck.discard_pile`. 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; + for (0..self.num_discards) |discard_idx| { + if (std.meta.eql(self.discard_pile[discard_idx], card)) { + return DeckError.DuplicateInDiscard; + } + } + for (0..self.num_cards) |card_idx| { + if (std.meta.eql(self.cards[card_idx], card)) { + return DeckError.DuplicateInDeck; } } 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); + pub fn cut(self: *Deck, index: u8) DeckError!void { + if (index >= self.num_cards or index < 0) return DeckError.OutOfRangeCut; + if (index == 0) return; - for (0..self.num_discards) |disIdx| { - self.cards[self.num_cards + disIdx]; + const index_from_top: u8 = self.num_cards - index; + var cb = [_]?Card{null} ** MAX_CARDS; + var top_cards: []?Card = cb[index_from_top..MAX_CARDS]; + for (0..index) |top_idx| { + top_cards[top_idx] = self.cards[index_from_top + top_idx]; + } + + for (0..index_from_top) |bot_idx| { + self.cards[self.num_cards - bot_idx - 1] = self.cards[index_from_top - bot_idx - 1]; + } + + for (0..index) |top_idx| { + self.cards[top_idx] = top_cards[top_idx]; + } + } + + /// Empties the `Deck.discard_pile` into the `Deck.cards` and + /// sorts the resulting `[]Card` slice. + pub fn rebuild(self: *Deck) void { + const new_total = self.num_cards + self.num_discards; + assert(new_total <= MAX_CARDS); + + const back_of_cards_idx = self.num_cards; + for (0..self.num_discards) |dis_idx| { + const card_idx = back_of_cards_idx + dis_idx; + self.cards[card_idx] = self.discard_pile[dis_idx]; self.num_cards += 1; } self.num_discards = 0; - assert(self.num_cards == newTotal); + assert(self.num_cards == new_total); - try sort(self.cards); + self.sort(); } - pub fn order_deck(self: *Deck) DeckError!void { - try sort(self.cards); + /// Sorts the `Deck.cards` slice in place + pub fn order_deck(self: *Deck) void { + self.sort(); } - 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]; + /// Sorts a `Deck.cards` in place + /// We can sort the deck quickly because we + /// know the index of each card by default, + /// the sorting takes N time, and 2N space + /// + /// Default Order from Top to Bottom is: + /// Suits: 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 sort(self: *Deck) void { + if (self.num_cards <= 1) return; + var cb = [_]?Card{null} ** MAX_CARDS; + var sorted_gaps_deck: []?Card = cb[0..MAX_CARDS]; + var idx_used = [_]bool{false} ** 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 cards_added: u8 = 0; + for (self.cards) |maybe_card| { + if (maybe_card) |card| { + const card_idx = faceValueIdx(card.face_value) + suitValueIdx(card.suit); + assert(!idx_used[card_idx]); + idx_used[card_idx] = true; + sorted_gaps_deck[card_idx] = card; + cards_added += 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]); + assert(cards_added == self.num_cards); + self.num_cards = 0; + for (0..MAX_CARDS) |g_idx| { + if (sorted_gaps_deck[g_idx] != null) { + self.cards[self.num_cards] = sorted_gaps_deck[g_idx].?; + self.num_cards += 1; + } } + assert(self.num_cards == cards_added); } + /// Gives the index within the suit for a given `Face` fn faceValueIdx(f: Face) u8 { - const negIdx: i16 = @intCast(@intFromEnum(f)); - return @intCast(@abs(negIdx - 14)); + const neg_idx: i16 = @intCast(@intFromEnum(f)); + return @intCast(@abs(neg_idx - 14)); } test "Deck.faceValueIdx" { @@ -205,83 +231,40 @@ pub const Deck = struct { try std.testing.expectEqual(1, faceValueIdx(Face.King)); } - fn suiteValueIdx(s: Suit) u8 { - const cards: u8 = @intCast(NUM_CARDS_IN_SUITE); + fn suitValueIdx(s: Suit) u8 { + const cards: u8 = @intCast(NUM_CARDS_IN_SUIT); 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; + pub fn shuffle(self: *Deck, shuffle_options: ShuffleOptions) void { + const seed: u64 = if (shuffle_options.seed) |s| s else @intCast(std.time.milliTimestamp()); + var prng = std.Random.DefaultPrng.init(seed); + const random = prng.random(); + var i: u8 = 0; + while (i < self.num_cards - 1) : (i += 1) { + const j = random.intRangeLessThan(u8, 0, self.num_cards); + std.mem.swap(Card, &self.cards[i].?, &self.cards[j].?); } - // 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(); + 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]); + try std.testing.expectEqual(Card{ .suit = .Diamonds, .face_value = .Ace }, deck.cards[0]); + try std.testing.expectEqual(Card{ .suit = .Spades, .face_value = .Two }, deck.cards[51]); } test "Deck.deal" { var deck = Deck{}; - try deck.init(); + deck.init(); - try std.testing.expectEqual(deck.deal(), Card{ .suite = .Spades, .faceValue = .Two }); + try std.testing.expectEqual(deck.deal(), Card{ .suit = .Spades, .face_value = .Two }); try std.testing.expectEqual(51, deck.num_cards); - try std.testing.expectEqual(deck.deal(), Card{ .suite = .Spades, .faceValue = .Three }); + try std.testing.expectEqual(deck.deal(), Card{ .suit = .Spades, .face_value = .Three }); try std.testing.expectEqual(50, deck.num_cards); for (0..50) |_| { @@ -294,9 +277,9 @@ test "Deck.deal" { test "Deck.peak" { var deck = Deck{}; - try deck.init(); + deck.init(); - try std.testing.expectEqual(Card{ .suite = .Spades, .faceValue = .Two }, deck.peak()); + try std.testing.expectEqual(Card{ .suit = .Spades, .face_value = .Two }, deck.peak()); try std.testing.expectEqual(MAX_CARDS, deck.num_cards); // Deal out 20 cards @@ -304,7 +287,7 @@ test "Deck.peak" { _ = deck.deal(); } - try std.testing.expectEqual(deck.peak(), Card{ .suite = .Hearts, .faceValue = .Nine }); + try std.testing.expectEqual(deck.peak(), Card{ .suit = .Hearts, .face_value = .Nine }); try std.testing.expectEqual(deck.num_cards, 32); // Try to peak empty deck @@ -316,12 +299,12 @@ test "Deck.peak" { test "Deck.discard" { var deck = Deck{}; - try deck.init(); + 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); + try std.testing.expect(twoSpadesCard.?.suit == .Spades); + try std.testing.expect(twoSpadesCard.?.face_value == .Two); // Deal out 13 cards for (0..12) |_| { @@ -329,8 +312,8 @@ test "Deck.discard" { } const twoHeartsCard = deck.deal(); try std.testing.expect(twoHeartsCard != null); - try std.testing.expect(twoHeartsCard.?.suite == .Hearts); - try std.testing.expect(twoHeartsCard.?.faceValue == .Two); + try std.testing.expect(twoHeartsCard.?.suit == .Hearts); + try std.testing.expect(twoHeartsCard.?.face_value == .Two); // Discard two cards try deck.discard(twoSpadesCard.?); @@ -340,12 +323,12 @@ test "Deck.discard" { try std.testing.expectEqual(twoSpadesCard.?, deck.discard_pile[0]); // Fails to add duplicate card - try std.testing.expectError(DeckError.DuplicateDiscard, deck.discard(twoSpadesCard.?)); + try std.testing.expectError(DeckError.DuplicateInDiscard, deck.discard(twoSpadesCard.?)); } test "Deck.rebuild" { var deck = Deck{}; - try deck.init(); + deck.init(); var buf1 = [_]?Card{null} ** 5; var buf2 = [_]?Card{null} ** 5; var buf3 = [_]?Card{null} ** 5; @@ -355,7 +338,7 @@ test "Deck.rebuild" { // 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().?); + if (i % 2 == 0) playerOneHand[i / 2] = deck.deal().? else playerTwoHand[i / 2] = deck.deal().?; } try std.testing.expectEqual(5, playerOneHand.len); @@ -372,66 +355,131 @@ test "Deck.rebuild" { try std.testing.expectEqual(5, playerThreeHand.len); - try std.testing.expectEqual(10, deck.discard_pile.len); - try std.testing.expectEqual(27, deck.cards.len); + try std.testing.expectEqual(10, deck.num_discards); + try std.testing.expectEqual(27, deck.num_cards); 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()), + 0 => try deck.discard(playerThreeHand[i / 3].?), + 1 => try deck.discard(playerOneHand[i / 3].?), + 2 => try deck.discard(playerTwoHand[i / 3].?), else => unreachable, } } - try std.testing.expectEqual(25, deck.discard_pile.len); + try std.testing.expectEqual(25, deck.num_discards); - try deck.rebuild(); + deck.rebuild(); - try std.testing.expectEqual(0, deck.discard_pile.len); - try std.testing.expectEqual(52, deck.cards.len); + try std.testing.expectEqual(0, deck.num_discards); + try std.testing.expectEqual(52, 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]); + try std.testing.expectEqual(Card{ .suit = .Diamonds, .face_value = .Ace }, deck.cards[0]); + try std.testing.expectEqual(Card{ .suit = .Spades, .face_value = .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; +test "Deck.rebuild with missing cards" { + var deck = Deck{}; + deck.init(); + var buf1 = [_]?Card{null} ** 13; + var buf2 = [_]?Card{null} ** 13; + var player_one_hand: []?Card = &(buf1); + var player_two_hand: []?Card = &(buf2); -// // 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, -// } -// } + // Mix up the cards, throw some away + for (0..MAX_CARDS) |i| { + switch (i % 4) { + 0 => player_two_hand[i / 4] = deck.deal().?, + 1 => try deck.discard(deck.deal().?), + 2 => player_one_hand[i / 4] = 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); + try std.testing.expectEqual(13, deck.num_discards); + try std.testing.expectEqual(0, deck.num_cards); -// 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); + for (0..26) |i| { + switch (i % 2) { + 0 => try deck.discard(player_one_hand[i / 2].?), + 1 => try deck.discard(player_two_hand[i / 2].?), + else => unreachable, + } + } + try std.testing.expectEqual(39, deck.num_discards); -// try deck.rebuild(); + deck.rebuild(); -// try std.testing.expectEqual(0, deck.discard_pile.len); -// try std.testing.expectEqual(39, deck.cards.len); + try std.testing.expectEqual(0, deck.num_discards); + try std.testing.expectEqual(39, 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[38]); -// } + try std.testing.expectEqual(Card{ .suit = .Diamonds, .face_value = .King }, deck.cards[0]); + try std.testing.expectEqual(Card{ .suit = .Spades, .face_value = .Two }, deck.cards[38]); +} + +test "Deck.cut" { + var deck = Deck{}; + deck.init(); + + try deck.cut(1); + try std.testing.expectEqual(Card{ .suit = .Spades, .face_value = .Three }, deck.peak()); + + try deck.cut(0); + try std.testing.expectEqual(Card{ .suit = .Spades, .face_value = .Three }, deck.peak()); + + try deck.cut(21); + try std.testing.expectEqual(Card{ .suit = .Hearts, .face_value = .Jack }, deck.peak()); + + // lots of cuts to get back to sorted + try deck.cut(5); + try deck.cut(0); + try deck.cut(2); + try deck.cut(8); + try deck.cut(12); + try deck.cut(3); + try std.testing.expectEqual(Card{ .suit = .Spades, .face_value = .Two }, deck.peak()); + + var deck2 = Deck{}; + deck2.init(); + try std.testing.expectEqual(deck2.num_cards, deck.num_cards); + + // Should be just like a new deck at this point + for (0..deck2.num_cards) |idx| { + try std.testing.expectEqual(deck2.cards[idx], deck.cards[idx]); + } + + // Invalid cuts + for (0..10) |_| { + _ = deck.deal(); + } + try std.testing.expectEqual(42, deck.num_cards); + try std.testing.expectError(DeckError.OutOfRangeCut, deck.cut(50)); + try std.testing.expectError(DeckError.OutOfRangeCut, deck.cut(42)); + + try deck.cut(41); + try std.testing.expectEqual(Card{ .suit = .Diamonds, .face_value = .Ace }, deck.peak()); +} + +test "Deck.shuffle" { + var deck = Deck{}; + deck.init(); + + deck.shuffle(.{ .seed = 0 }); + + try std.testing.expectEqual(Card{ .suit = .Diamonds, .face_value = .Five }, deck.peak()); + + for (0..25) |_| { + _ = deck.deal(); + } + + deck.shuffle(.{ .seed = 0 }); + + try std.testing.expectEqual(Card{ .suit = .Diamonds, .face_value = .Ace }, deck.peak()); + + deck.sort(); + deck.shuffle(.{ .seed = 0 }); + + try std.testing.expectEqual(Card{ .suit = .Hearts, .face_value = .Nine }, deck.peak()); +} diff --git a/src/main.zig b/src/main.zig index 8fd460e..414a5a5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,333 @@ const std = @import("std"); -const deck = @import("deck.zig"); +const zap = @import("zap"); -pub fn main() !void { - std.debug.print("It works!\n", .{}); +const d = @import("deck.zig"); +const DeckHttpType = enum { GET, POST }; +const DeckHttpRoute = union(DeckHttpType) { + GET: *const fn (*DeckContext) void, + POST: *const fn (*DeckContext) void, +}; +const DeckContext = struct { + deck: *d.Deck, + request: zap.Request, +}; + +var routes: std.StringHashMap(DeckHttpRoute) = undefined; +var deck: d.Deck = undefined; +var verbose = false; +var very_verbose = false; + +pub fn main() void { + // Setup stack allocator + var buf: [1024]u8 = undefined; + var buf_allocator = std.heap.FixedBufferAllocator.init(&buf); + var arena = std.heap.ArenaAllocator.init(buf_allocator.allocator()); + const allocator = arena.allocator(); + defer arena.deinit(); + + // Parse Args + var args_iter = std.process.argsWithAllocator(allocator) catch { + std.debug.print("ERR: Unable to initialize args parser"); + }; + var port_or_err: error{ BadPort, MissingPort }!u16 = 8081; + + _ = args_iter.skip(); + while (args_iter.next()) |arg| { + if (std.mem.eql(u8, arg, "-h")) { + printHelp(); + return; + } else if (std.mem.eql(u8, arg, "-p")) { + const port_slice = args_iter.next(); + if (port_slice) |ps| { + port_or_err = std.fmt.parseInt(u16, ps, 10) catch error.BadPort; + } else { + port_or_err = error.MissingPort; + } + } else if (std.mem.eql(u8, arg, "-v")) { + verbose = true; + } else if (std.mem.eql(u8, arg, "-vv")) { + verbose = true; + very_verbose = true; + } + } + if (port_or_err == error.BadPort) { + std.debug.print("The port provided is not a valid number.", .{}); + return; + } else if (port_or_err == error.MissingPort) { + std.debug.print("A port was not provided after the cli -p option, try `-p 8000`\n", .{}); + return; + } + const port: usize = @intCast(port_or_err catch 8081); + std.debug.print("Starting deck server on port {d}\n", .{port}); + deck = d.Deck{}; + deck.init(); + + routes = std.StringHashMap(DeckHttpRoute).init(allocator); + + routes.put("/deal", DeckHttpRoute{ .GET = deal }) catch return; + routes.put("/cheat", DeckHttpRoute{ .GET = cheat }) catch return; + routes.put("/discard", DeckHttpRoute{ .POST = discard }) catch return; + routes.put("/cut", DeckHttpRoute{ .POST = cut }) catch return; + routes.put("/rebuild", DeckHttpRoute{ .POST = rebuild }) catch return; + routes.put("/sort", DeckHttpRoute{ .POST = sort }) catch return; + routes.put("/shuffle", DeckHttpRoute{ .POST = shuffle }) catch return; + + var listener = zap.HttpListener.init(.{ .port = port, .on_request = onRequest, .log = verbose }); + + std.debug.print("Listening\n", .{}); + listener.listen() catch |err| { + std.debug.print("Unable to start http server: {any}\n", .{err}); + }; + + zap.start(.{ .threads = 1, .workers = 1 }); +} + +fn onRequest(request: zap.Request) void { + if (request.path == null) { + return; + } + var found_route = false; + if (routes.get(request.path.?)) |route| { + switch (route) { + .GET => { + if (request.method == null) return; + if (!std.mem.eql(u8, request.method.?, "GET")) return; + var ctx = DeckContext{ .deck = &deck, .request = request }; + found_route = true; + route.GET(&ctx); + if (very_verbose) { + d.printDeck(&deck, .{}); + } + }, + .POST => { + if (request.method == null) return; + if (!std.mem.eql(u8, request.method.?, "POST")) return; + var ctx = DeckContext{ .deck = &deck, .request = request }; + found_route = true; + route.POST(&ctx); + if (very_verbose) { + d.printDeck(&deck, .{}); + } + }, + } + } + if (!found_route) { + sendErrorMessage(request, "Unknown route", .not_found); + } +} + +pub const DealGetRequest = struct { + cards_in_deck: u8, + face_value: []const u8, + suit: []const u8, + suit_unicode: []const u8, +}; + +fn deal(ctx: *DeckContext) void { + const card_or_null = ctx.deck.deal(); + if (card_or_null) |card| { + var buf: [200]u8 = undefined; + const card_json = zap.stringifyBuf(&buf, DealGetRequest{ + .cards_in_deck = ctx.deck.num_cards, + .face_value = @tagName(card.face_value), + .suit = @tagName(card.suit), + .suit_unicode = d.suitToIcon(card.suit), + }, .{}); + if (card_json == null) { + ctx.request.sendError(error.BadJsonStringify, if (@errorReturnTrace()) |t| t.* else null, 500); + return; + } + ctx.request.sendJson(card_json.?) catch |err| { + std.debug.print("{any}", .{err}); + return; + }; + } else { + ctx.request.sendBody("No cards left") catch return; + } +} + +fn cheat(ctx: *DeckContext) void { + const card_or_null = ctx.deck.peak(); + if (card_or_null) |card| { + var buf: [200]u8 = undefined; + const card_json = zap.stringifyBuf(&buf, DealGetRequest{ + .cards_in_deck = ctx.deck.num_cards, + .face_value = @tagName(card.face_value), + .suit = @tagName(card.suit), + .suit_unicode = d.suitToIcon(card.suit), + }, .{}); + if (card_json == null) { + ctx.request.sendError(error.BadJsonStringify, if (@errorReturnTrace()) |t| t.* else null, 500); + return; + } + ctx.request.sendJson(card_json.?) catch |err| { + std.debug.print("{any}", .{err}); + return; + }; + } else { + ctx.request.sendBody("No cards left") catch return; + } +} + +fn discard(ctx: *DeckContext) void { + ctx.request.parseBody() catch return; + if (ctx.request.body == null) { + ctx.request.setStatus(.bad_request); + ctx.request.sendJson("{\"success\" = false, \"error\": \"Missing card data\"}") catch return; + return; + } + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + const json_body = std.json.parseFromSlice(d.Card, allocator, ctx.request.body.?, .{}) catch |err| { + if (err == error.InvalidEnumTag) { + ctx.request.setStatus(.bad_request); + ctx.request.sendJson("{\"success\" = false, \"error\": \"Invalid card value provided\"}") catch return; + return; + } + ctx.request.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 500); + return; + }; + defer json_body.deinit(); + ctx.deck.discard(json_body.value) catch |err| { + switch (err) { + d.DeckError.DuplicateInDiscard => { + ctx.request.setStatus(.bad_request); + ctx.request.sendJson("{\"success\": false, \"error\": \"Unable to add to discard, already exists.\"}") catch return; + return; + }, + d.DeckError.DuplicateInDeck => { + ctx.request.setStatus(.bad_request); + ctx.request.sendJson("{\"success\": false, \"error\": \"Unable to add to discard, card exists in deck.\"}") catch return; + return; + }, + else => unreachable, + } + }; + ctx.request.setStatus(.ok); + var buf: [100]u8 = undefined; + const json = zap.stringifyBuf(&buf, .{ .success = true }, .{}); + if (json == null) { + return; + } + ctx.request.sendJson(json.?) catch return; +} + +pub const CutPostRequest = struct { + index: u8, +}; + +fn cut(ctx: *DeckContext) void { + if (ctx.request.body == null and ctx.request.query == null) { + ctx.request.setStatus(.bad_request); + ctx.request.sendJson("{\"success\" = false, \"error\": \"Missing cut index\"}") catch return; + return; + } + var index: u8 = undefined; + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + // use json body + if (ctx.request.body != null) { + ctx.request.parseBody() catch return; + const json_body = std.json.parseFromSlice(CutPostRequest, allocator, ctx.request.body.?, .{}) catch |err| { + if (err == error.Overflow) { + ctx.request.setStatus(.bad_request); + ctx.request.sendJson("{\"success\": false, \"error\": \"Expecting unsigned 8 bit integer as index\"}") catch return; + return; + } + ctx.request.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 500); + return; + }; + defer json_body.deinit(); + index = json_body.value.index; + } + // use query params + else { + ctx.request.parseQuery(); + var params = ctx.request.parametersToOwnedList(allocator, false) catch |err| { + ctx.request.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 500); + return; + }; + defer params.deinit(); + if (params.items.len != 1 or !std.mem.eql(u8, "index", params.items[0].key.str)) { + sendErrorMessage(ctx.request, "Only key 'index' allowed in query params", .bad_request); + return; + } + index = @intCast(params.items[0].value.?.Int); + } + ctx.deck.cut(index) catch |err| { + switch (err) { + d.DeckError.OutOfRangeCut => { + ctx.request.setStatus(.bad_request); + ctx.request.sendJson("{\"success\": false, \"error\": \"Index provided is out of range\"}") catch return; + return; + }, + else => unreachable, + } + }; + ctx.request.setStatus(.ok); + var buf: [100]u8 = undefined; + const json = zap.stringifyBuf(&buf, .{ .success = true }, .{}); + if (json == null) { + return; + } + ctx.request.sendJson(json.?) catch return; +} + +fn rebuild(ctx: *DeckContext) void { + ctx.deck.rebuild(); + ctx.request.setStatus(.ok); + var buf: [100]u8 = undefined; + const json = zap.stringifyBuf(&buf, .{ .success = true }, .{}); + if (json == null) { + return; + } + ctx.request.sendJson(json.?) catch return; +} + +fn sort(ctx: *DeckContext) void { + ctx.deck.order_deck(); + ctx.request.setStatus(.ok); + var buf: [100]u8 = undefined; + const json = zap.stringifyBuf(&buf, .{ .success = true }, .{}); + if (json == null) { + return; + } + ctx.request.sendJson(json.?) catch return; +} + +fn shuffle(ctx: *DeckContext) void { + ctx.deck.shuffle(.{}); + ctx.request.setStatus(.ok); + var buf: [100]u8 = undefined; + const json = zap.stringifyBuf(&buf, .{ .success = true }, .{}); + if (json == null) { + return; + } + ctx.request.sendJson(json.?) catch return; +} + +const DeckErrorMessage = struct { + success: bool, + message: []const u8, +}; + +fn sendErrorMessage(request: zap.Request, msg: []const u8, status: zap.StatusCode) void { + request.setStatus(status); + var buf: [300]u8 = undefined; + const json = zap.stringifyBuf(&buf, DeckErrorMessage{ .success = false, .message = msg }, .{}); + if (json == null) { + return; + } + request.sendJson(json.?) catch return; +} + +fn printHelp() void { + const menu_text = + \\Help Menu + \\ -v Verbose logging + \\ -vv Very verbose logging + \\ -p Specify a port other than the default 8081 + \\ -h Prints this help menu + ; + std.debug.print("{s}\n", .{menu_text}); }