From 2510ba3eb6835fe4a1bdfe64bf471ca5b1e62037 Mon Sep 17 00:00:00 2001 From: geemili Date: Mon, 8 Jul 2024 13:20:59 -0600 Subject: [PATCH] refactor: remove some global state and put it in Snapshot --- src/main.zig | 834 +++++++++++++++++++++++++++++---------------------- 1 file changed, 480 insertions(+), 354 deletions(-) diff --git a/src/main.zig b/src/main.zig index 9062eb6..59a243f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,21 +7,15 @@ var window_global: seizer.Window = undefined; var canvas: seizer.Canvas = undefined; var card_tilemap: ?DeckSprites = null; -var draw_pile: std.ArrayListUnmanaged(Card) = .{}; -var draw_pile_exhausted = false; -var drawn_cards: std.ArrayListUnmanaged(Card) = .{}; -var stacks: [7]std.ArrayListUnmanaged(Card) = [_]std.ArrayListUnmanaged(Card){.{}} ** 7; -var foundations: [4]std.ArrayListUnmanaged(Card) = [_]std.ArrayListUnmanaged(Card){.{}} ** 4; - +/// The last Snapshot is the current game state var history: std.ArrayListUnmanaged(Snapshot) = .{}; -var last_hovered_stack: usize = 0; -var last_hovered_foundation: usize = 0; -var hovered_deck: ?*std.ArrayListUnmanaged(Card) = null; -var hovered_card: usize = 0; +var last_hovered_stack: Deck = .stack1; +var last_hovered_foundation: Deck = .foundation1; -var selected_deck: ?*std.ArrayListUnmanaged(Card) = null; -var selected_card: usize = 0; +/// Which deck is currently hovered. Necessary because a deck can have no cards and be a valid target. +var hovered_deck: ?Deck = null; +var hovered_card: ?u6 = null; const MenuEvent = union(enum) { up, down, draw: struct { scalef: f32 }, select, deselect }; const MenuFn = *const fn (MenuEvent) anyerror!void; @@ -140,61 +134,28 @@ fn onWinCountWritten(userdata: ?*anyopaque, result: seizer.Platform.FileError!vo } fn resetGame() !void { + try history.append(seizer.platform.allocator(), Snapshot.init(prng.random())); + resetHistory(); - draw_pile.deinit(seizer.platform.allocator()); - draw_pile_exhausted = false; - drawn_cards.shrinkRetainingCapacity(0); - for (&stacks) |*stack| { - stack.shrinkRetainingCapacity(0); - } - for (&foundations) |*foundation| { - foundation.shrinkRetainingCapacity(0); - } - last_hovered_stack = 0; - last_hovered_foundation = 0; + last_hovered_stack = .stack1; + last_hovered_foundation = .foundation1; hovered_deck = null; - hovered_card = 0; - - selected_deck = null; - selected_card = 0; - - draw_pile = std.ArrayListUnmanaged(Card).fromOwnedSlice(try makeStandardDeck(seizer.platform.allocator())); - prng.random().shuffle(Card, draw_pile.items); - - for (&stacks, 0..) |*stack, i| { - for (0..i + 1) |_| { - const drawn_card = draw_pile.pop(); - try stack.append(seizer.platform.allocator(), drawn_card); - } - } + hovered_card = null; win_triggered = false; } fn resetHistory() void { - for (history.items) |*snapshot| { - snapshot.deinit(); - } + const current_state = history.pop(); history.shrinkRetainingCapacity(0); + history.appendAssumeCapacity(current_state); } fn destroy(window: seizer.Window) void { _ = window; - for (history.items) |*snapshot| { - snapshot.deinit(); - } history.deinit(seizer.platform.allocator()); - draw_pile.deinit(seizer.platform.allocator()); - drawn_cards.deinit(seizer.platform.allocator()); - for (&stacks) |*stack| { - stack.deinit(seizer.platform.allocator()); - } - for (&foundations) |*foundation| { - foundation.deinit(seizer.platform.allocator()); - } - if (card_tilemap) |*ct| { ct.deinit(seizer.platform.allocator()); card_tilemap = null; @@ -288,9 +249,11 @@ fn render(window: seizer.Window) !void { ); } - for (&stacks, 0..) |*stack, i| { - const deck_is_selected = selected_deck != null and selected_deck.? == stack; - const deck_is_hovered = hovered_deck != null and hovered_deck.? == stack; + const snapshot = history.items[history.items.len - 1]; + + for (Deck.STACKS[0..], 0..) |stack, i| { + const deck_is_selected = if (snapshot.selected_card) |s| snapshot.deckContainsIndex(stack, s) else false; + const deck_is_hovered = if (hovered_deck) |hovered| hovered == stack else false; const deck_color = if (deck_is_selected) [4]u8{ 0xFF, 0xA0, 0xA0, 0xFF } else if (deck_is_hovered) @@ -306,9 +269,12 @@ fn render(window: seizer.Window) !void { pos[1] += board_offset[1]; canvas.rect(pos, tile_sizef, .{ .color = deck_color }); - for (stack.items, 0..) |card, j| { - const is_selected = deck_is_selected and j >= selected_card; - const is_hovered = deck_is_hovered and j >= hovered_card; + const stack_index = snapshot.getDeckStartIndex(stack); + const stack_cards = snapshot.cards[stack_index..][0..snapshot.deck_card_count[@intFromEnum(stack)]]; + + for (stack_cards, stack_index..) |card, j| { + const is_selected = deck_is_selected and j >= snapshot.selected_card.?; + const is_hovered = deck_is_hovered and j >= snapshot.getDeckStartIndex(hovered_deck.?) + hovered_card.?; const color = if (is_selected) [4]u8{ 0xFF, 0xA0, 0xA0, 0xFF } else if (is_hovered) @@ -345,7 +311,7 @@ fn render(window: seizer.Window) !void { ); } - for (&foundations, 0..) |*foundation, i| { + for (&Deck.FOUNDATIONS, 0..) |foundation, i| { const is_hovered = hovered_deck != null and hovered_deck.? == foundation; const color = if (is_hovered) [4]u8{ 0xA0, 0xFF, 0xA0, 0xFF } else [4]u8{ 0xFF, 0xFF, 0xFF, 0xFF }; @@ -357,7 +323,8 @@ fn render(window: seizer.Window) !void { pos[1] += board_offset[1]; canvas.rect(pos, tile_sizef, .{ .color = color }); - for (foundation.items) |card| { + const foundation_cards = snapshot.getDeck(foundation); + for (foundation_cards) |card| { const tile_id = card_tilemap.?.getTileForCard(card); card_tilemap.?.tilesheet.renderTile(&canvas, tile_id, pos, .{ .size = tile_sizef, @@ -368,8 +335,11 @@ fn render(window: seizer.Window) !void { } { - const deck_is_selected = selected_deck != null and selected_deck.? == &drawn_cards; - const deck_is_hovered = hovered_deck != null and hovered_deck.? == &drawn_cards; + const deck_is_selected = snapshot.deckContainsIndex(.drawn_cards, snapshot.selected_card orelse 63); + const deck_is_hovered = hovered_deck != null and hovered_deck.? == .drawn_cards; + + const drawn_cards_index = snapshot.getDeckStartIndex(.drawn_cards); + const drawn_cards_cards = snapshot.cards[drawn_cards_index..][0..snapshot.deck_card_count[@intFromEnum(Deck.drawn_cards)]]; var pos = [2]f32{ @floatFromInt(margin + margin + tile_size[0]), @@ -377,9 +347,9 @@ fn render(window: seizer.Window) !void { }; pos[0] += board_offset[0]; pos[1] += board_offset[1]; - for (drawn_cards.items, 0..) |card, j| { - const is_selected = deck_is_selected and j >= selected_card; - const is_hovered = deck_is_hovered and j >= hovered_card; + for (drawn_cards_cards, drawn_cards_index..) |card, j| { + const is_selected = deck_is_selected and j >= snapshot.selected_card orelse 63; + const is_hovered = deck_is_hovered and j >= snapshot.getDeckStartIndex(hovered_deck.?) + hovered_card.?; const color = if (is_selected) [4]u8{ 0xFF, 0xA0, 0xA0, 0xFF } else if (is_hovered) @@ -397,8 +367,8 @@ fn render(window: seizer.Window) !void { } { - const is_selected = selected_deck != null and selected_deck.? == &draw_pile; - const is_hovered = hovered_deck != null and hovered_deck.? == &draw_pile; + const is_selected = snapshot.deckContainsIndex(.draw_pile, snapshot.selected_card orelse 63); + const is_hovered = hovered_deck != null and hovered_deck.? == .draw_pile; const color = if (is_selected) [4]u8{ 0xFF, 0xA0, 0xA0, 0xFF } else if (is_hovered) @@ -406,13 +376,16 @@ fn render(window: seizer.Window) !void { else [4]u8{ 0xFF, 0xFF, 0xFF, 0xFF }; + const draw_pile_index = snapshot.getDeckStartIndex(.draw_pile); + const draw_pile_cards = snapshot.cards[draw_pile_index..][0..snapshot.deck_card_count[@intFromEnum(Deck.draw_pile)]]; + var pos = [2]f32{ @floatFromInt(margin), @floatFromInt(margin) }; pos[0] += board_offset[0]; pos[1] += board_offset[1]; canvas.rect(pos, tile_sizef, .{ .color = color }); - for (draw_pile.items) |card| { - const tile_id = if (!draw_pile_exhausted) card_tilemap.?.back else card_tilemap.?.getTileForCard(card); + for (draw_pile_cards) |card| { + const tile_id = if (!snapshot.draw_pile_exhausted) card_tilemap.?.back else card_tilemap.?.getTileForCard(card); card_tilemap.?.tilesheet.renderTile(&canvas, tile_id, pos, .{ .size = tile_sizef, .color = color, @@ -431,10 +404,12 @@ fn render(window: seizer.Window) !void { } pub fn haveWon() bool { - if (draw_pile.items.len > 0) return false; - if (drawn_cards.items.len > 0) return false; - for (stacks) |stack| { - if (stack.items.len > 0) return false; + const snapshot = history.items[history.items.len - 1]; + + if (snapshot.deck_card_count[@intFromEnum(Deck.draw_pile)] > 0) return false; + if (snapshot.deck_card_count[@intFromEnum(Deck.drawn_cards)] > 0) return false; + for (Deck.STACKS) |stack| { + if (snapshot.deck_card_count[@intFromEnum(stack)] > 0) return false; } return true; } @@ -462,8 +437,9 @@ pub fn deselect(pressed: bool) !void { if (current_menu_fn) |menu_fn| { try menu_fn(.deselect); } - selected_deck = null; - selected_card = 0; + + const snapshot = &history.items[history.items.len - 1]; + snapshot.selected_card = null; } pub fn doSelectOrPlace(pressed: bool) !void { @@ -472,55 +448,58 @@ pub fn doSelectOrPlace(pressed: bool) !void { try menu_fn(.select); return; } - if (selected_deck == null) { - if (hovered_deck == &draw_pile and !draw_pile_exhausted) { - resetHistory(); - drawn_cards.ensureUnusedCapacity(seizer.platform.allocator(), 3) catch return; - for (0..3) |_| { - if (draw_pile.popOrNull()) |card| drawn_cards.appendAssumeCapacity(card); - } - if (draw_pile.items.len == 0) { - draw_pile_exhausted = true; - } - } else if (hovered_deck == &draw_pile and draw_pile_exhausted) { - selected_deck = hovered_deck; - selected_card = hovered_card; - } else if (hovered_deck == &drawn_cards) { - selected_deck = hovered_deck; - selected_card = hovered_card; - } else { - for (stacks[0..]) |*stack| { - if (hovered_deck != stack) continue; - selected_deck = hovered_deck; - selected_card = hovered_card; - break; - } + + const snapshot = &history.items[history.items.len - 1]; + if (snapshot.selected_card == null) { + const hovered = hovered_deck orelse return; + const card_count_in_hovered = snapshot.deck_card_count[@intFromEnum(hovered)]; + switch (hovered) { + .draw_pile => if (!snapshot.draw_pile_exhausted) { + snapshot.* = snapshot.moveCards(.drawn_cards, 3, .draw_pile); + if (snapshot.deck_card_count[@intFromEnum(Deck.draw_pile)] == 0) { + snapshot.draw_pile_exhausted = true; + } + resetHistory(); + } else if (card_count_in_hovered != 0) { + snapshot.selected_card = snapshot.getDeckStartIndex(hovered) + hovered_card.?; + }, + .drawn_cards => if (card_count_in_hovered != 0) { + snapshot.selected_card = snapshot.getDeckStartIndex(hovered) + hovered_card.?; + }, + .stack1, + .stack2, + .stack3, + .stack4, + .stack5, + .stack6, + .stack7, + => if (card_count_in_hovered != 0) { + snapshot.selected_card = snapshot.getDeckStartIndex(hovered) + hovered_card.?; + }, + else => {}, } - } else if (hovered_deck == selected_deck) { - var snapshot = try Snapshot.initFromCurrentGlobalState(); - const selected_substack = selected_deck.?.items[selected_card..]; + } else if (hovered_deck != null and snapshot.deckContainsIndex(hovered_deck.?, snapshot.selected_card.?) and hovered_deck.?.getDeckKind() != .foundation) { + // selected_deck shouldn't be null if we're in this branch, as the first branch checks for the null condition + const selected_deck_id = snapshot.selectedDeck().?; + const num_selected = snapshot.getDeck(selected_deck_id).len; // try to move all selected cards into the foundations var cards_moved = false; - move_cards_to_foundations: for (0..selected_substack.len) |_| { - for (foundations[0..]) |*foundation| { - if (hovered_deck == foundation) break :move_cards_to_foundations; - } + var new_snapshot = snapshot.*; + move_cards_to_foundations: for (0..num_selected) |_| { + const selected_deck = new_snapshot.getDeck(selected_deck_id); + if (selected_deck.len == 0) break; - for (foundations[0..]) |*foundation| { - const empty = foundation.items.len == 0; - const ace = selected_deck.?.items[selected_card].rank == 1; - const suit_matches = !empty and foundation.items[foundation.items.len - 1].suit == selected_deck.?.items[selected_deck.?.items.len - 1].suit; - const is_next_rank = !empty and foundation.items[foundation.items.len - 1].rank + 1 == selected_deck.?.items[selected_deck.?.items.len - 1].rank; + const top_card = selected_deck[selected_deck.len - 1]; + for (Deck.FOUNDATIONS) |foundation_id| { + const foundation = new_snapshot.getDeck(foundation_id); + + const empty = foundation.len == 0; + const ace = top_card.rank == 1; + const suit_matches = !empty and foundation[foundation.len - 1].suit == top_card.suit; + const is_next_rank = !empty and foundation[foundation.len - 1].rank + 1 == top_card.rank; if ((empty and ace) or (suit_matches and is_next_rank)) { - foundation.ensureUnusedCapacity(seizer.platform.allocator(), 1) catch continue; - foundation.appendAssumeCapacity(selected_deck.?.pop()); - - hovered_card = if (hovered_deck.? == &drawn_cards) - hovered_deck.?.items.len -| 1 - else - indexOfTopOfStack(hovered_deck.?.items); - + new_snapshot = new_snapshot.moveCards(foundation_id, 1, selected_deck_id); cards_moved = true; break; } @@ -530,75 +509,80 @@ pub fn doSelectOrPlace(pressed: bool) !void { } if (cards_moved) { - try history.append(seizer.platform.allocator(), snapshot); + new_snapshot.selected_card = null; + try history.append(seizer.platform.allocator(), new_snapshot); + hovered_card = @intCast(indexOfTopOfStack(new_snapshot.getDeck(hovered_deck.?))); } else { - snapshot.deinit(); + snapshot.selected_card = null; } - - selected_deck = null; } else { var cards_moved = false; - var snapshot = try Snapshot.initFromCurrentGlobalState(); - if (selected_deck) |selected| move_from_selected_to_hovered: { - const selected_substack = selected.items[selected_card..]; + var new_snapshot = snapshot.*; + + if (hovered_deck) |hovered| move_from_selected_to_hovered: { + // selected_deck shouldn't be null if we're in this branch, as the first branch checks for the null condition + const selected_deck_id = snapshot.selectedDeck().?; + const selected_substack = snapshot.getSelectedCards().?; if (selected_substack.len == 0) break :move_from_selected_to_hovered; - const hovered = hovered_deck orelse break :move_from_selected_to_hovered; + const hovered_cards = snapshot.getDeck(hovered); - if (hovered.items.len == 0) { - for (stacks[0..]) |*stack| { - if (hovered != stack) continue; - hovered.ensureUnusedCapacity(seizer.platform.allocator(), selected_substack.len) catch break :move_from_selected_to_hovered; - hovered.appendSliceAssumeCapacity(selected_substack); - selected.shrinkRetainingCapacity(selected.items.len - selected_substack.len); - hovered_card = indexOfTopOfStack(hovered_deck.?.items); - cards_moved = true; - break :move_from_selected_to_hovered; - } - - if (selected_substack.len == 1) { - if (draw_pile_exhausted and hovered == &draw_pile and draw_pile.items.len == 0) { - hovered.ensureUnusedCapacity(seizer.platform.allocator(), 1) catch break :move_from_selected_to_hovered; - hovered.appendAssumeCapacity(selected_deck.?.pop()); - hovered_card = indexOfTopOfStack(hovered_deck.?.items); + if (hovered_cards.len == 0) { + switch (hovered) { + .stack1, + .stack2, + .stack3, + .stack4, + .stack5, + .stack6, + .stack7, + => { + new_snapshot = snapshot.moveCards(hovered, @intCast(selected_substack.len), selected_deck_id); cards_moved = true; break :move_from_selected_to_hovered; - } else for (foundations[0..]) |*foundation| { - if (foundation != selected) continue; - const empty = foundation.items.len == 0; - const ace = selected_deck.?.items[selected_card].rank == 1; - const suit_matches = !empty and foundation.items[foundation.items.len - 1].suit == selected_deck.?.items[selected_deck.?.items.len - 1].suit; - const is_next_rank = !empty and foundation.items[foundation.items.len - 1].rank + 1 == selected_deck.?.items[selected_deck.?.items.len - 1].rank; + }, + .foundation1, + .foundation2, + .foundation3, + .foundation4, + => if (selected_substack.len == 1) { + const foundation = snapshot.getDeck(hovered); + const selected_card = selected_substack[0]; + + const empty = foundation.len == 0; + const ace = selected_card.rank == 1; + const suit_matches = !empty and foundation[foundation.len - 1].suit == selected_card.suit; + const is_next_rank = !empty and foundation[foundation.len - 1].rank + 1 == selected_card.rank; if ((empty and ace) or (suit_matches and is_next_rank)) { - foundation.ensureUnusedCapacity(seizer.platform.allocator(), 1) catch continue; - foundation.appendAssumeCapacity(selected_deck.?.pop()); - hovered_card = indexOfTopOfStack(hovered_deck.?.items); + new_snapshot = snapshot.moveCards(hovered, 1, selected_deck_id); cards_moved = true; break :move_from_selected_to_hovered; } - } + }, + .draw_pile => if (snapshot.draw_pile_exhausted and selected_substack.len == 1) { + new_snapshot = snapshot.moveCards(.draw_pile, 1, selected_deck_id); + cards_moved = true; + break :move_from_selected_to_hovered; + }, + .drawn_cards => {}, } - break :move_from_selected_to_hovered; } - if (hovered.items[hovered.items.len - 1].rank - 1 != selected_substack[0].rank or - hovered.items[hovered.items.len - 1].suit.color() == selected_substack[0].suit.color()) + const top_of_hovered = hovered_cards[hovered_cards.len - 1]; + if (top_of_hovered.rank - 1 != selected_substack[0].rank or + top_of_hovered.suit.color() == selected_substack[0].suit.color()) { break :move_from_selected_to_hovered; } - hovered.ensureUnusedCapacity(seizer.platform.allocator(), selected_substack.len) catch break :move_from_selected_to_hovered; - hovered.appendSliceAssumeCapacity(selected_substack); - selected.shrinkRetainingCapacity(selected.items.len - selected_substack.len); - hovered_card = indexOfTopOfStack(hovered_deck.?.items); + new_snapshot = snapshot.moveCards(hovered, @intCast(selected_substack.len), selected_deck_id); cards_moved = true; } if (cards_moved) { - try history.append(seizer.platform.allocator(), snapshot); - } else { - snapshot.deinit(); + new_snapshot.selected_card = null; + try history.append(seizer.platform.allocator(), new_snapshot); + hovered_card = @intCast(indexOfTopOfStack(new_snapshot.getDeck(hovered_deck.?))); } - selected_deck = null; } } @@ -608,32 +592,26 @@ pub fn moveLeft(pressed: bool) !void { _ = menu_fn; return; } - if (hovered_deck == null) { - hovered_deck = &draw_pile; - hovered_card = 0; - } else if (hovered_deck == &draw_pile) { - hovered_deck = &drawn_cards; - hovered_card = drawn_cards.items.len -| 1; - } else if (hovered_deck == &drawn_cards) { - hovered_deck = &draw_pile; - hovered_card = 0; - } else if (hovered_deck == &stacks[0]) { - hovered_deck = &foundations[last_hovered_foundation]; + + // move which pile is hovered + const snapshot = &history.items[history.items.len - 1]; + if (hovered_deck) |hovered| { + hovered_deck = hovered.getDeckLeftwards(last_hovered_foundation); } else { - for (stacks[1..], 1..) |*stack, i| { - if (hovered_deck == stack) { - hovered_deck = &stacks[i - 1]; - hovered_card = indexOfTopOfStack(hovered_deck.?.items); - last_hovered_stack = i - 1; - return; - } - } - for (&foundations) |*foundation| { - if (hovered_deck == foundation) { - hovered_deck = &stacks[stacks.len - 1]; - hovered_card = indexOfTopOfStack(hovered_deck.?.items); - return; - } + hovered_deck = .draw_pile; + } + + // find the top of the deck, depending on what kind of deck it is + if (hovered_deck) |hovered| { + hovered_card = switch (hovered.getDeckKind()) { + .draw, .foundation => snapshot.deck_card_count[@intFromEnum(hovered)] -| 1, + .stack => @intCast(indexOfTopOfStack(snapshot.getDeck(hovered))), + }; + + switch (hovered.getDeckKind()) { + .draw => {}, + .foundation => last_hovered_foundation = hovered, + .stack => last_hovered_stack = hovered, } } } @@ -644,32 +622,26 @@ pub fn moveRight(pressed: bool) !void { _ = menu_fn; return; } - if (hovered_deck == null) { - hovered_deck = &draw_pile; - hovered_card = 0; - } else if (hovered_deck == &draw_pile) { - hovered_deck = &drawn_cards; - hovered_card = drawn_cards.items.len -| 1; - } else if (hovered_deck == &drawn_cards) { - hovered_deck = &draw_pile; - hovered_card = 0; - } else if (hovered_deck == &stacks[stacks.len - 1]) { - hovered_deck = &foundations[last_hovered_foundation]; + + // move which pile is hovered + const snapshot = &history.items[history.items.len - 1]; + if (hovered_deck) |hovered| { + hovered_deck = hovered.getDeckRightwards(last_hovered_foundation); } else { - for (stacks[0 .. stacks.len - 1], 0..) |*stack, i| { - if (hovered_deck == stack) { - hovered_deck = &stacks[i + 1]; - hovered_card = indexOfTopOfStack(hovered_deck.?.items); - last_hovered_stack = i + 1; - return; - } - } - for (&foundations) |*foundation| { - if (hovered_deck == foundation) { - hovered_deck = &stacks[0]; - hovered_card = indexOfTopOfStack(hovered_deck.?.items); - return; - } + hovered_deck = .draw_pile; + } + + // find the top of the deck, depending on what kind of deck it is + if (hovered_deck) |hovered| { + hovered_card = switch (hovered.getDeckKind()) { + .draw, .foundation => snapshot.deck_card_count[@intFromEnum(hovered)] -| 1, + .stack => @intCast(indexOfTopOfStack(snapshot.getDeck(hovered))), + }; + + switch (hovered.getDeckKind()) { + .draw => {}, + .foundation => last_hovered_foundation = hovered, + .stack => last_hovered_stack = hovered, } } } @@ -678,42 +650,28 @@ pub fn moveUp(pressed: bool) !void { if (!pressed) return; if (current_menu_fn) |menu_fn| { - try menu_fn(.up); - } else if (hovered_deck == null) { - hovered_deck = &draw_pile; - hovered_card = 0; - } else if (hovered_deck == &draw_pile) { - hovered_deck = &foundations[foundations.len - 1]; - last_hovered_foundation = foundations.len - 1; - } else if (hovered_deck == &drawn_cards) { - hovered_deck = &stacks[last_hovered_stack]; - hovered_card = indexOfTopOfStack(hovered_deck.?.items); - } else if (hovered_deck == &foundations[0]) { - hovered_deck = &draw_pile; - hovered_card = 0; + return try menu_fn(.up); + } + + // move which pile is hovered + const snapshot = &history.items[history.items.len - 1]; + if (hovered_deck) |hovered| { + hovered_deck = hovered.getDeckUpwards(last_hovered_stack); } else { - for (stacks[0..]) |*stack| { - if (hovered_deck != stack) continue; - const top_of_stack = indexOfTopOfStack(stack.items); - if (hovered_card > top_of_stack) { - hovered_card -= 1; - return; - } - if (drawn_cards.items.len > 0) { - hovered_deck = &drawn_cards; - hovered_card = drawn_cards.items.len -| 1; - } else { - hovered_deck = &draw_pile; - hovered_card = 0; - } - return; - } - for (foundations[1..], 1..) |*foundation, i| { - if (hovered_deck == foundation) { - hovered_deck = &foundations[i - 1]; - last_hovered_foundation = i - 1; - return; - } + hovered_deck = .draw_pile; + } + + // find the top of the deck, depending on what kind of deck it is + if (hovered_deck) |hovered| { + hovered_card = switch (hovered.getDeckKind()) { + .draw, .foundation => snapshot.deck_card_count[@intFromEnum(hovered)] -| 1, + .stack => @intCast(indexOfTopOfStack(snapshot.getDeck(hovered))), + }; + + switch (hovered.getDeckKind()) { + .draw => {}, + .foundation => last_hovered_foundation = hovered, + .stack => last_hovered_stack = hovered, } } } @@ -721,52 +679,41 @@ pub fn moveUp(pressed: bool) !void { pub fn moveDown(pressed: bool) !void { if (!pressed) return; if (current_menu_fn) |menu_fn| { - try menu_fn(.down); - } else if (hovered_deck == null) { - hovered_deck = &draw_pile; - hovered_card = 0; - } else if (hovered_deck == &draw_pile) { - hovered_deck = &foundations[0]; - last_hovered_foundation = 0; - } else if (hovered_deck == &drawn_cards) { - hovered_deck = &stacks[last_hovered_stack]; - hovered_card = indexOfTopOfStack(hovered_deck.?.items); - } else if (hovered_deck == &foundations[foundations.len - 1]) { - hovered_deck = &draw_pile; - hovered_card = 0; - } else { - for (stacks[0..]) |*stack| { - if (hovered_deck != stack) continue; - if (hovered_card < stack.items.len -| 1) { - hovered_card += 1; - return; - } + return try menu_fn(.down); + } - if (drawn_cards.items.len > 0) { - hovered_deck = &drawn_cards; - hovered_card = drawn_cards.items.len -| 1; - } else { - hovered_deck = &draw_pile; - hovered_card = 0; - } - return; - } - for (foundations[0 .. foundations.len - 1], 0..) |*foundation, i| { - if (hovered_deck == foundation) { - hovered_deck = &foundations[i + 1]; - last_hovered_foundation = i + 1; - return; - } + // move which pile is hovered + const snapshot = &history.items[history.items.len - 1]; + if (hovered_deck) |hovered| { + hovered_deck = hovered.getDeckDownwards(last_hovered_stack); + } else { + hovered_deck = .draw_pile; + } + + // find the top of the deck, depending on what kind of deck it is + if (hovered_deck) |hovered| { + hovered_card = switch (hovered.getDeckKind()) { + .draw, .foundation => snapshot.deck_card_count[@intFromEnum(hovered)] -| 1, + .stack => @intCast(indexOfTopOfStack(snapshot.getDeck(hovered))), + }; + + switch (hovered.getDeckKind()) { + .draw => {}, + .foundation => last_hovered_foundation = hovered, + .stack => last_hovered_stack = hovered, } } } pub fn undo(pressed: bool) !void { if (!pressed) return; - if (history.popOrNull()) |snapshot_const| { - var snapshot = snapshot_const; - defer snapshot.deinit(); - try snapshot.restore(); + if (history.items.len > 1) { + _ = history.pop(); + + const snapshot = &history.items[history.items.len - 1]; + hovered_deck = snapshot.selectedDeck(); + hovered_card = if (snapshot.selected_card) |selected_card| selected_card - snapshot.getDeckStartIndex(hovered_deck.?) else null; + snapshot.selected_card = null; } } @@ -779,7 +726,7 @@ pub fn toggleMenu(pressed: bool) !void { } } -pub fn indexOfTopOfStack(cards: []Card) usize { +pub fn indexOfTopOfStack(cards: []const Card) usize { if (cards.len < 2) return 0; var index = cards.len - 1; @@ -792,89 +739,265 @@ pub fn indexOfTopOfStack(cards: []Card) usize { return index; } -const Snapshot = struct { - draw_pile: []Card, - draw_pile_exhausted: bool, - drawn_cards: []Card, - stacks: [7][]Card, - foundations: [4][]Card, - selected_deck: *std.ArrayListUnmanaged(Card), - selected_card: usize, +pub const Deck = enum(u4) { + draw_pile, + drawn_cards, + stack1, + stack2, + stack3, + stack4, + stack5, + stack6, + stack7, + foundation1, + foundation2, + foundation3, + foundation4, - pub fn initFromCurrentGlobalState() !Snapshot { - const snapshot_selected_deck = selected_deck orelse return error.InvalidSelectionForSnapshot; + /// The total number of decks in the game. + pub const COUNT = 13; - const snapshot_draw_pile = try seizer.platform.allocator().dupe(Card, draw_pile.items); - errdefer seizer.platform.allocator().free(snapshot_draw_pile); - const snapshot_drawn_cards = try seizer.platform.allocator().dupe(Card, drawn_cards.items); - errdefer seizer.platform.allocator().free(snapshot_drawn_cards); + pub const ALL = [13]Deck{ .draw_pile, .drawn_cards, .stack1, .stack2, .stack3, .stack4, .stack5, .stack6, .stack7, .foundation1, .foundation2, .foundation3, .foundation4 }; + pub const STACKS = [7]Deck{ .stack1, .stack2, .stack3, .stack4, .stack5, .stack6, .stack7 }; + pub const FOUNDATIONS = [4]Deck{ .foundation1, .foundation2, .foundation3, .foundation4 }; - var snapshot_stacks: [7][]Card = undefined; - for (&snapshot_stacks, &stacks) |*snapshot_stack, stack| { - snapshot_stack.* = try seizer.platform.allocator().dupe(Card, stack.items); - } + pub const Kind = enum { + draw, + stack, + foundation, + }; - var snapshot_foundations: [4][]Card = undefined; - for (&snapshot_foundations, &foundations) |*snapshot_foundation, foundation| { - snapshot_foundation.* = try seizer.platform.allocator().dupe(Card, foundation.items); - } - - return Snapshot{ - .draw_pile = snapshot_draw_pile, - .draw_pile_exhausted = draw_pile_exhausted, - .drawn_cards = snapshot_drawn_cards, - .stacks = snapshot_stacks, - .foundations = snapshot_foundations, - .selected_deck = snapshot_selected_deck, - .selected_card = selected_card, + pub fn getDeckKind(deck: Deck) Deck.Kind { + return switch (deck) { + .draw_pile, + .drawn_cards, + => .draw, + .stack1, + .stack2, + .stack3, + .stack4, + .stack5, + .stack6, + .stack7, + => .stack, + .foundation1, + .foundation2, + .foundation3, + .foundation4, + => .foundation, }; } - pub fn deinit(snapshot: *@This()) void { - seizer.platform.allocator().free(snapshot.draw_pile); - seizer.platform.allocator().free(snapshot.drawn_cards); - for (snapshot.stacks) |stack| { - seizer.platform.allocator().free(stack); - } - for (snapshot.foundations) |foundation| { - seizer.platform.allocator().free(foundation); - } + /// - `default_stack`: When moving from the `drawn_cards` deck, this deck will be returned. + /// This should be one of the 7 stack decks, or the user will be unable to access the stacks using up and down. + pub fn getDeckDownwards(deck: Deck, default_stack: Deck) Deck { + return switch (deck) { + .draw_pile => .foundation1, + .drawn_cards => default_stack, + .stack1, + .stack2, + .stack3, + .stack4, + .stack5, + .stack6, + .stack7, + => .drawn_cards, + .foundation1 => .foundation2, + .foundation2 => .foundation3, + .foundation3 => .foundation4, + .foundation4 => .draw_pile, + }; } - pub fn restore(snapshot: @This()) !void { - // ensure there is enough space for all the cards - try draw_pile.ensureTotalCapacity(seizer.platform.allocator(), snapshot.draw_pile.len); - try drawn_cards.ensureTotalCapacity(seizer.platform.allocator(), snapshot.drawn_cards.len); - for (&snapshot.stacks, &stacks) |snapshot_stack, *stack| { - try stack.ensureTotalCapacity(seizer.platform.allocator(), snapshot_stack.len); - } - for (&snapshot.foundations, &foundations) |snapshot_foundation, *foundation| { - try foundation.ensureTotalCapacity(seizer.platform.allocator(), snapshot_foundation.len); + /// - `default_stack`: When moving from the `drawn_cards` deck, this deck will be returned. + /// This should be one of the 7 stack decks, or the user will be unable to access the stacks using up and down. + pub fn getDeckUpwards(deck: Deck, default_stack: Deck) Deck { + return switch (deck) { + .draw_pile => .foundation4, + .foundation4 => .foundation3, + .foundation3 => .foundation2, + .foundation2 => .foundation1, + .foundation1 => .draw_pile, + .drawn_cards => default_stack, + .stack1, + .stack2, + .stack3, + .stack4, + .stack5, + .stack6, + .stack7, + => .drawn_cards, + }; + } + + /// - `default_foundation`: When moving from the stacks, this deck will be returned. + /// This should be one of the 4 foundation decks, or the user will be unable to access the foundations using left and/or right. + pub fn getDeckRightwards(deck: Deck, default_foundation: Deck) Deck { + return switch (deck) { + .draw_pile => .drawn_cards, + .drawn_cards => .draw_pile, + .stack1 => .stack2, + .stack2 => .stack3, + .stack3 => .stack4, + .stack4 => .stack5, + .stack5 => .stack6, + .stack6 => .stack7, + .stack7 => default_foundation, + .foundation1, + .foundation2, + .foundation3, + .foundation4, + => .stack1, + }; + } + + /// - `default_foundation`: When moving from the stacks, this deck will be returned. + /// This should be one of the 4 foundation decks, or the user will be unable to access the foundations using left and/or right. + pub fn getDeckLeftwards(deck: Deck, default_foundation: Deck) Deck { + return switch (deck) { + .draw_pile => .drawn_cards, + .drawn_cards => .draw_pile, + .stack1 => default_foundation, + .stack2 => .stack1, + .stack3 => .stack2, + .stack4 => .stack3, + .stack5 => .stack4, + .stack6 => .stack5, + .stack7 => .stack6, + .foundation1, + .foundation2, + .foundation3, + .foundation4, + => .stack7, + }; + } +}; + +const Snapshot = struct { + /// All 52 cards in the game. Each deck comes one after the other, with it's size determined by `deck_card_count`. + cards: [52]Card = makeStandardDeckArray(), + + /// How many cards are in each deck. + deck_card_count: [Deck.COUNT]u6 = [_]u6{52} ++ [_]u6{0} ** 12, + + /// Index of the card selected in `cards`. You can determine the deck by iterating over the decks and determining which + /// contains this index. + selected_card: ?u6 = null, + + /// whether or not the draw pile has been exhausted. It determines whether the draw_pile deck is a free slot or not. + draw_pile_exhausted: bool = false, + + fn makeStandardDeckArray() [52]Card { + var deck: [52]Card = undefined; + + var next_index: usize = 0; + for (0..4) |suit| { + for (1..14) |rank| { + deck[next_index] = Card{ + .suit = @enumFromInt(suit), + .rank = @intCast(rank), + }; + next_index += 1; + } } - // reset the state - draw_pile.shrinkRetainingCapacity(0); - draw_pile.appendSliceAssumeCapacity(snapshot.draw_pile); + return deck; + } - draw_pile_exhausted = snapshot.draw_pile_exhausted; + pub fn init(random: std.Random) Snapshot { + var snapshot = Snapshot{}; - drawn_cards.shrinkRetainingCapacity(0); - drawn_cards.appendSliceAssumeCapacity(snapshot.drawn_cards); + random.shuffle(Card, snapshot.getDeckMut(.draw_pile)); - for (&snapshot.stacks, &stacks) |snapshot_stack, *stack| { - stack.shrinkRetainingCapacity(0); - stack.appendSliceAssumeCapacity(snapshot_stack); + for (Deck.STACKS[0..], 0..) |stack, i| { + snapshot = snapshot.moveCards(stack, @intCast(i + 1), .draw_pile); } - for (&snapshot.foundations, &foundations) |snapshot_foundation, *foundation| { - foundation.shrinkRetainingCapacity(0); - foundation.appendSliceAssumeCapacity(snapshot_foundation); + return snapshot; + } + + pub fn moveCards(this: @This(), destination_deck: Deck, amount: u6, source_deck: Deck) @This() { + var result: Snapshot = this; + + var result_index: u6 = 0; + for (Deck.ALL) |deck| { + result.deck_card_count[@intFromEnum(deck)] = 0; + + const deck_cards = this.getDeck(deck); + const cards_to_move = if (deck == source_deck) deck_cards[0 .. deck_cards.len - amount] else deck_cards; + + @memcpy(result.cards[result_index..][0..cards_to_move.len], cards_to_move); + result_index += @intCast(cards_to_move.len); + result.deck_card_count[@intFromEnum(deck)] += @intCast(cards_to_move.len); + + if (deck == destination_deck) { + const source_cards = this.getDeck(source_deck); + const source_cards_to_move = source_cards[source_cards.len - amount ..]; + + @memcpy(result.cards[result_index..][0..source_cards_to_move.len], source_cards_to_move); + + result_index += @intCast(source_cards_to_move.len); + result.deck_card_count[@intFromEnum(deck)] += @intCast(source_cards_to_move.len); + } } - selected_deck = null; - selected_card = 0; - hovered_deck = snapshot.selected_deck; - hovered_card = snapshot.selected_card; + return result; + } + + pub fn getDeckStartIndex(this: @This(), deck: Deck) u6 { + var index: u6 = 0; + for (this.deck_card_count[0..@intFromEnum(deck)]) |card_count| { + index += card_count; + } + return index; + } + + pub fn getDeck(this: *const @This(), deck: Deck) []const Card { + const index = this.getDeckStartIndex(deck); + const card_count = this.deck_card_count[@intFromEnum(deck)]; + return this.cards[index..][0..card_count]; + } + + pub fn getDeckMut(this: *@This(), deck: Deck) []Card { + const index = this.getDeckStartIndex(deck); + const card_count = this.deck_card_count[@intFromEnum(deck)]; + return this.cards[index..][0..card_count]; + } + + pub fn deckContainsIndex(this: @This(), deck: Deck, index: u6) bool { + const deck_index = this.getDeckStartIndex(deck); + const card_count = this.deck_card_count[@intFromEnum(deck)]; + const deck_end_index = deck_index + card_count; + + return index >= deck_index and index < deck_end_index; + } + + pub fn selectedDeck(this: @This()) ?Deck { + if (this.selected_card == null) return null; + + var index: u6 = 0; + for (this.deck_card_count, 0..) |card_count, deck_index| { + index += card_count; + if (this.selected_card.? < index) { + return @enumFromInt(deck_index); + } + } + + return null; + } + + pub fn getSelectedCards(this: *const @This()) ?[]const Card { + const selected_index = this.selected_card orelse return null; + + var index: u6 = 0; + for (this.deck_card_count) |card_count| { + if (selected_index >= index and selected_index < index + card_count) { + return this.cards[selected_index .. index + card_count]; + } + index += card_count; + } + + return null; } }; @@ -1019,7 +1142,10 @@ fn menuAbout(event: MenuEvent) !void { @max(title_text_size[0], description_text_size[0]) + 2 * margin[0], title_text_size[1] + description_text_size[1] + 2 * margin[1], }; - const menu_offset = [2]f32{ @floor((canvas.window_size[0] - menu_size[0]) / 2), @floor((canvas.window_size[1] - menu_size[1]) / 2) }; + const menu_offset = [2]f32{ + @floor((canvas.window_size[0] - menu_size[0]) / 2), + @floor((canvas.window_size[1] - menu_size[1]) / 2), + }; canvas.rect( menu_offset,