Compare commits

...

5 Commits

3 changed files with 313 additions and 18 deletions

View File

@ -16,8 +16,8 @@
// internet connectivity.
.dependencies = .{
.seizer = .{
.url = "https://github.com/leroycep/seizer/archive/d425db26fbbf5a99faa70ea07fb7d2b028f690d1.tar.gz",
.hash = "1220250791e125cf4d342020eb3af65c36a8b971559ae9ffafc187c0cb116ee3d6e2",
.url = "https://github.com/leroycep/seizer/archive/2be76fb909fc487da4983a487d0695fc749f714a.tar.gz",
.hash = "12207072224809e8a8d2c1342b5e14db5b6fbb1fbbd7f5c6410d71d3708c9241261e",
},
},
.paths = .{

View File

@ -385,7 +385,7 @@ pub fn loadSolitaireCards(gpa: std.mem.Allocator) !DeckSprites {
.blank = 13,
.back = 27,
.margin = 4,
.hand_offset = .{ 13, 12 },
.hand_offset = .{ 10, 12 },
};
}

View File

@ -2,6 +2,7 @@ pub const main = seizer.main;
var gpa: std.mem.Allocator = undefined;
var prng: std.rand.DefaultPrng = undefined;
var window_global: *seizer.Window = undefined;
var canvas: seizer.Canvas = undefined;
var card_tilemap: ?DeckSprites = null;
@ -11,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;
@ -19,11 +22,24 @@ var hovered_card: usize = 0;
var selected_deck: ?*std.ArrayListUnmanaged(Card) = null;
var selected_card: usize = 0;
var show_menu: bool = false;
var menu_item_selected: u32 = 0;
const MENU_ITEMS = [_][]const u8{
"Continue",
"New Game",
"Quit",
};
var app_data_dir: std.fs.Dir = undefined;
var win_count_file: std.fs.File = undefined;
var win_count: u32 = 0;
var win_triggered: bool = false;
pub fn init(context: *seizer.Context) !void {
gpa = context.gpa;
prng = std.rand.DefaultPrng.init(@bitCast(std.time.timestamp()));
_ = try context.createWindow(.{
window_global = try context.createWindow(.{
.title = "Seizer Solitaire",
.on_render = render,
.on_destroy = destroy,
@ -34,15 +50,7 @@ pub fn init(context: *seizer.Context) !void {
card_tilemap = try assets.loadSolitaireCards(context.gpa);
draw_pile = std.ArrayListUnmanaged(Card).fromOwnedSlice(try makeStandardDeck(gpa));
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(gpa, drawn_card);
}
}
try resetGame();
try context.addButtonInput(.{
.title = "move_up",
@ -74,6 +82,71 @@ pub fn init(context: *seizer.Context) !void {
.on_event = deselect,
.default_bindings = &.{.b},
});
try context.addButtonInput(.{
.title = "toggle_menu",
.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);
app_data_dir = try std.fs.cwd().makeOpenPath(app_data_dir_path, .{});
win_count_file = try app_data_dir.createFile("win_count.bin", .{ .read = true, .truncate = false });
var win_count_buffer: [4]u8 = undefined;
const bytes_read = try win_count_file.preadAll(&win_count_buffer, 0);
if (bytes_read < win_count_buffer.len) {
win_count = 0;
std.mem.writeInt(u32, &win_count_buffer, win_count, .little);
try win_count_file.pwriteAll(&win_count_buffer, 0);
} else {
win_count = std.mem.readInt(u32, &win_count_buffer, .little);
}
}
fn resetGame() !void {
draw_pile.deinit(gpa);
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;
hovered_deck = null;
hovered_card = 0;
selected_deck = null;
selected_card = 0;
draw_pile = std.ArrayListUnmanaged(Card).fromOwnedSlice(try makeStandardDeck(gpa));
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(gpa, drawn_card);
}
}
win_triggered = false;
}
fn resetHistory() void {
for (history.items) |*snapshot| {
snapshot.deinit();
}
history.shrinkRetainingCapacity(0);
}
fn destroy(window: *seizer.Window) void {
@ -95,6 +168,17 @@ fn destroy(window: *seizer.Window) void {
}
fn render(window: *seizer.Window) !void {
if (haveWon() and !win_triggered) {
win_count += 1;
var win_count_buffer: [4]u8 = undefined;
std.mem.writeInt(u32, &win_count_buffer, win_count, .little);
try win_count_file.pwriteAll(&win_count_buffer, 0);
resetHistory();
win_triggered = true;
}
gl.clearColor(0.2, 0.4, 0.2, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
@ -110,12 +194,38 @@ fn render(window: *seizer.Window) !void {
card_tilemap.?.tilesheet.tile_size[1] * scale,
};
const margin = card_tilemap.?.margin * scale;
const marginf: f32 = @floatFromInt(card_tilemap.?.margin * scale);
const hand_offset = [2]u32{
card_tilemap.?.hand_offset[0] * scale,
card_tilemap.?.hand_offset[1] * scale,
};
const hand_offsetf = [2]f32{
@floatFromInt(hand_offset[0]),
@floatFromInt(hand_offset[1]),
};
const tile_sizef = [2]f32{ @floatFromInt(tile_size[0]), @floatFromInt(tile_size[1]) };
const space_taken_by_board = [2]f32{
@floatFromInt(2 * margin + 8 * (margin + tile_size[0])),
@floatFromInt(2 * margin + 5 * (margin + tile_size[1])),
};
const board_offset = [2]f32{
(canvas.window_size[0] - space_taken_by_board[0]) / 2,
(canvas.window_size[1] - space_taken_by_board[1]) / 2,
};
{
_ = canvas.printText(
.{
canvas.window_size[0] - board_offset[0] - marginf - hand_offsetf[0],
board_offset[1] + marginf + hand_offsetf[1],
},
"Win Count: {}",
.{win_count},
.{ .@"align" = .right, .scale = scalef },
);
}
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;
@ -127,9 +237,11 @@ fn render(window: *seizer.Window) !void {
[4]u8{ 0xFF, 0xFF, 0xFF, 0xFF };
var pos = [2]f32{
@floatFromInt(margin + ((tile_size[0] + margin) * (i + 1))),
@floatFromInt(margin + margin + tile_size[1]),
@floor(@as(f32, @floatFromInt(margin + ((tile_size[0] + margin) * (i + 1))))),
@floor(@as(f32, @floatFromInt(margin + margin + tile_size[1]))),
};
pos[0] += board_offset[0];
pos[1] += board_offset[1];
canvas.rect(pos, tile_sizef, .{ .color = deck_color });
for (stack.items, 0..) |card, j| {
@ -151,14 +263,36 @@ fn render(window: *seizer.Window) !void {
}
}
if (haveWon()) {
const offset =
[2]f32{
2 * marginf + tile_sizef[0],
2 * marginf + tile_sizef[1],
};
_ = canvas.writeText(
.{
board_offset[0] + offset[0] + (space_taken_by_board[0] - offset[0]) / 2,
board_offset[1] + offset[1] + (space_taken_by_board[1] - offset[0]) / 2,
},
"You Win!",
.{
.baseline = .middle,
.@"align" = .center,
.scale = 2 * scalef,
},
);
}
for (&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 };
const pos = [2]f32{
var pos = [2]f32{
@floatFromInt(margin),
@floatFromInt(margin + ((margin + tile_size[1]) * (i + 1))),
};
pos[0] += board_offset[0];
pos[1] += board_offset[1];
canvas.rect(pos, tile_sizef, .{ .color = color });
for (foundation.items) |card| {
@ -167,6 +301,7 @@ fn render(window: *seizer.Window) !void {
.size = tile_sizef,
.color = color,
});
pos[1] -= 0.25 * scalef;
}
}
@ -178,6 +313,8 @@ fn render(window: *seizer.Window) !void {
@floatFromInt(margin + margin + tile_size[0]),
@floatFromInt(margin),
};
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;
@ -202,6 +339,10 @@ fn render(window: *seizer.Window) !void {
const color = if (is_hovered) [4]u8{ 0xA0, 0xFF, 0xA0, 0xFF } else [4]u8{ 0xFF, 0xFF, 0xFF, 0xFF };
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);
card_tilemap.?.tilesheet.renderTile(&canvas, tile_id, pos, .{
@ -212,9 +353,37 @@ fn render(window: *seizer.Window) !void {
}
}
if (show_menu) {
canvas.rect(
.{ canvas.window_size[0] / 3, canvas.window_size[1] / 3 },
.{ canvas.window_size[0] / 3, canvas.window_size[1] / 3 },
.{ .color = .{ 0, 0, 0, 0xFF } },
);
const x: f32 = canvas.window_size[0] / 2;
var y: f32 = canvas.window_size[1] / 3 + (canvas.font.lineHeight * scalef);
for (MENU_ITEMS, 0..) |menu_item, i| {
if (i == menu_item_selected) {
const size = canvas.printText(.{ x, y }, "> {s} <", .{menu_item}, .{ .@"align" = .center, .scale = scalef });
y += size[1];
} else {
const size = canvas.writeText(.{ x, y }, menu_item, .{ .@"align" = .center, .scale = scalef });
y += size[1];
}
}
}
canvas.end();
}
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;
}
return true;
}
pub fn makeStandardDeck(allocator: std.mem.Allocator) ![]Card {
var deck = try allocator.alloc(Card, 52);
errdefer allocator.free(deck);
@ -235,14 +404,28 @@ pub fn makeStandardDeck(allocator: std.mem.Allocator) ![]Card {
pub fn deselect(pressed: bool) !void {
if (!pressed) return;
if (show_menu) return;
selected_deck = null;
selected_card = 0;
}
pub fn doSelectOrPlace(pressed: bool) !void {
if (!pressed) return;
if (show_menu) {
const menu_item = MENU_ITEMS[menu_item_selected];
if (std.mem.eql(u8, menu_item, "Continue")) {
show_menu = false;
} else if (std.mem.eql(u8, menu_item, "New Game")) {
try resetGame();
show_menu = false;
} else if (std.mem.eql(u8, menu_item, "Quit")) {
window_global.setShouldClose(true);
}
return;
}
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);
@ -265,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;
@ -282,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;
@ -303,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;
}
@ -311,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;
@ -322,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;
}
}
@ -339,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;
}
@ -346,6 +539,7 @@ pub fn doSelectOrPlace(pressed: bool) !void {
pub fn moveLeft(pressed: bool) !void {
if (!pressed) return;
if (show_menu) return;
if (hovered_deck == null) {
hovered_deck = &draw_pile;
hovered_card = 0;
@ -378,6 +572,7 @@ pub fn moveLeft(pressed: bool) !void {
pub fn moveRight(pressed: bool) !void {
if (!pressed) return;
if (show_menu) return;
if (hovered_deck == null) {
hovered_deck = &draw_pile;
hovered_card = 0;
@ -411,7 +606,10 @@ pub fn moveRight(pressed: bool) !void {
pub fn moveUp(pressed: bool) !void {
if (!pressed) return;
if (hovered_deck == null) {
if (show_menu) {
if (menu_item_selected == 0) menu_item_selected = MENU_ITEMS.len;
menu_item_selected -= 1;
} else if (hovered_deck == null) {
hovered_deck = &draw_pile;
hovered_card = 0;
} else if (hovered_deck == &draw_pile) {
@ -452,7 +650,10 @@ pub fn moveUp(pressed: bool) !void {
pub fn moveDown(pressed: bool) !void {
if (!pressed) return;
if (hovered_deck == null) {
if (show_menu) {
menu_item_selected += 1;
menu_item_selected %= MENU_ITEMS.len;
} else if (hovered_deck == null) {
hovered_deck = &draw_pile;
hovered_card = 0;
} else if (hovered_deck == &draw_pile) {
@ -491,6 +692,20 @@ 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;
}
pub fn indexOfTopOfStack(cards: []Card) usize {
if (cards.len < 2) return 0;
@ -504,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;