diff --git a/src/main.zig b/src/main.zig index 03c0dd0..57d403b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -12,6 +12,8 @@ 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; +var history: std.ArrayListUnmanaged(Snapshot) = .{}; + var last_hovered_stack: usize = 0; var last_hovered_foundation: usize = 0; var hovered_deck: ?*std.ArrayListUnmanaged(Card) = null; @@ -85,6 +87,11 @@ pub fn init(context: *seizer.Context) !void { .on_event = toggleMenu, .default_bindings = &.{.start}, }); + try context.addButtonInput(.{ + .title = "undo", + .on_event = undo, + .default_bindings = &.{.x}, + }); const app_data_dir_path = try std.fs.getAppDataDir(gpa, "seizer-solitaire"); defer gpa.free(app_data_dir_path); @@ -135,6 +142,13 @@ fn resetGame() !void { win_triggered = false; } +fn resetHistory() void { + for (history.items) |*snapshot| { + snapshot.deinit(); + } + history.shrinkRetainingCapacity(0); +} + fn destroy(window: *seizer.Window) void { _ = window; draw_pile.deinit(gpa); @@ -161,6 +175,7 @@ fn render(window: *seizer.Window) !void { std.mem.writeInt(u32, &win_count_buffer, win_count, .little); try win_count_file.pwriteAll(&win_count_buffer, 0); + resetHistory(); win_triggered = true; } @@ -410,6 +425,7 @@ pub fn doSelectOrPlace(pressed: bool) !void { } if (selected_deck == null) { if (hovered_deck == &draw_pile and !draw_pile_exhausted) { + resetHistory(); drawn_cards.ensureUnusedCapacity(gpa, 3) catch return; for (0..3) |_| { if (draw_pile.popOrNull()) |card| drawn_cards.appendAssumeCapacity(card); @@ -432,9 +448,11 @@ pub fn doSelectOrPlace(pressed: bool) !void { } } } else if (hovered_deck == selected_deck) { + const snapshot = try Snapshot.initFromCurrentGlobalState(); const selected_substack = selected_deck.?.items[selected_card..]; // 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; @@ -449,14 +467,18 @@ pub fn doSelectOrPlace(pressed: bool) !void { foundation.ensureUnusedCapacity(gpa, 1) catch continue; foundation.appendAssumeCapacity(selected_deck.?.pop()); hovered_card = indexOfTopOfStack(hovered_deck.?.items); + cards_moved = true; break; } } else { break :move_cards_to_foundations; } } + + try history.append(gpa, snapshot); selected_deck = null; } else { + const snapshot = try Snapshot.initFromCurrentGlobalState(); if (selected_deck) |selected| move_from_selected_to_hovered: { const selected_substack = selected.items[selected_card..]; if (selected_substack.len == 0) break :move_from_selected_to_hovered; @@ -470,6 +492,7 @@ pub fn doSelectOrPlace(pressed: bool) !void { hovered.appendSliceAssumeCapacity(selected_substack); selected.shrinkRetainingCapacity(selected.items.len - selected_substack.len); hovered_card = indexOfTopOfStack(hovered_deck.?.items); + try history.append(gpa, snapshot); break :move_from_selected_to_hovered; } @@ -478,6 +501,7 @@ pub fn doSelectOrPlace(pressed: bool) !void { hovered.ensureUnusedCapacity(gpa, 1) catch break :move_from_selected_to_hovered; hovered.appendAssumeCapacity(selected_deck.?.pop()); hovered_card = indexOfTopOfStack(hovered_deck.?.items); + try history.append(gpa, snapshot); break :move_from_selected_to_hovered; } else for (foundations[0..]) |*foundation| { if (foundation != selected) continue; @@ -489,6 +513,7 @@ pub fn doSelectOrPlace(pressed: bool) !void { foundation.ensureUnusedCapacity(gpa, 1) catch continue; foundation.appendAssumeCapacity(selected_deck.?.pop()); hovered_card = indexOfTopOfStack(hovered_deck.?.items); + try history.append(gpa, snapshot); break :move_from_selected_to_hovered; } } @@ -506,6 +531,7 @@ pub fn doSelectOrPlace(pressed: bool) !void { hovered.appendSliceAssumeCapacity(selected_substack); selected.shrinkRetainingCapacity(selected.items.len - selected_substack.len); hovered_card = indexOfTopOfStack(hovered_deck.?.items); + try history.append(gpa, snapshot); } selected_deck = null; } @@ -666,6 +692,15 @@ pub fn moveDown(pressed: bool) !void { } } +pub fn undo(pressed: bool) !void { + if (!pressed) return; + if (history.popOrNull()) |snapshot_const| { + var snapshot = snapshot_const; + defer snapshot.deinit(); + try snapshot.restore(); + } +} + pub fn toggleMenu(pressed: bool) !void { if (!pressed) return; show_menu = !show_menu; @@ -684,6 +719,86 @@ 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 fn initFromCurrentGlobalState() !Snapshot { + const snapshot_selected_deck = selected_deck orelse return error.InvalidSelectionForSnapshot; + + const snapshot_draw_pile = try gpa.dupe(Card, draw_pile.items); + errdefer gpa.free(snapshot_draw_pile); + const snapshot_drawn_cards = try gpa.dupe(Card, drawn_cards.items); + errdefer gpa.free(snapshot_drawn_cards); + + var snapshot_stacks: [7][]Card = undefined; + for (&snapshot_stacks, &stacks) |*snapshot_stack, stack| { + snapshot_stack.* = try gpa.dupe(Card, stack.items); + } + + var snapshot_foundations: [4][]Card = undefined; + for (&snapshot_foundations, &foundations) |*snapshot_foundation, foundation| { + snapshot_foundation.* = try gpa.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 deinit(snapshot: *@This()) void { + gpa.free(snapshot.draw_pile); + gpa.free(snapshot.drawn_cards); + } + + pub fn restore(snapshot: @This()) !void { + // ensure there is enough space for all the cards + try draw_pile.ensureTotalCapacity(gpa, snapshot.draw_pile.len); + try drawn_cards.ensureTotalCapacity(gpa, snapshot.drawn_cards.len); + for (&snapshot.stacks, &stacks) |snapshot_stack, *stack| { + try stack.ensureTotalCapacity(gpa, snapshot_stack.len); + } + for (&snapshot.foundations, &foundations) |snapshot_foundation, *foundation| { + try foundation.ensureTotalCapacity(gpa, snapshot_foundation.len); + } + + // reset the state + draw_pile.shrinkRetainingCapacity(0); + draw_pile.appendSliceAssumeCapacity(snapshot.draw_pile); + + draw_pile_exhausted = snapshot.draw_pile_exhausted; + + drawn_cards.shrinkRetainingCapacity(0); + drawn_cards.appendSliceAssumeCapacity(snapshot.drawn_cards); + + for (&snapshot.stacks, &stacks) |snapshot_stack, *stack| { + stack.shrinkRetainingCapacity(0); + stack.appendSliceAssumeCapacity(snapshot_stack); + } + + for (&snapshot.foundations, &foundations) |snapshot_foundation, *foundation| { + foundation.shrinkRetainingCapacity(0); + foundation.appendSliceAssumeCapacity(snapshot_foundation); + } + + selected_deck = null; + selected_card = 0; + hovered_deck = snapshot.selected_deck; + hovered_card = snapshot.selected_card; + } +}; + const DeckSprites = assets.DeckSprites; const Card = assets.Card;