From b199c7d52ee91c173371656d110df5876cb41d75 Mon Sep 17 00:00:00 2001 From: geemili Date: Sun, 14 Apr 2024 14:31:52 -0600 Subject: [PATCH] initial version of seizer-solitaire --- .gitignore | 2 + build.zig | 56 ++++ build.zig.zon | 36 +++ src/assets.zig | 394 +++++++++++++++++++++++++ src/cardsLarge_tilemap.png | Bin 0 -> 13185 bytes src/cardsMedium_tilemap.png | Bin 0 -> 3454 bytes src/cardsSmall_tilemap.png | Bin 0 -> 1504 bytes src/main.zig | 549 +++++++++++++++++++++++++++++++++++ src/solitaire-cards.aseprite | Bin 0 -> 2485 bytes src/solitaire-cards.png | Bin 0 -> 2501 bytes 10 files changed, 1037 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/assets.zig create mode 100644 src/cardsLarge_tilemap.png create mode 100644 src/cardsMedium_tilemap.png create mode 100644 src/cardsSmall_tilemap.png create mode 100644 src/main.zig create mode 100644 src/solitaire-cards.aseprite create mode 100644 src/solitaire-cards.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee7098f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-out/ +zig-cache/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..67c206f --- /dev/null +++ b/build.zig @@ -0,0 +1,56 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const seizer = b.dependency("seizer", .{ + .target = target, + .optimize = optimize, + }); + + const exe = b.addExecutable(.{ + .name = "seizer-solitaire", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + exe.root_module.addImport("seizer", seizer.module("seizer")); + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + exe_unit_tests.root_module.addImport("seizer", seizer.module("seizer")); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..562d684 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,36 @@ +.{ + .name = "seizer-solitaire", + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + .seizer = .{ + .url = "https://github.com/leroycep/seizer/archive/ec651ce18e423a8fdda3b1a3369d9121c45d96f0.tar.gz", + .hash = "122028487862f2462ba02191adbc6676b621b37fa05e0962f0a5990d4079ed34140e", + }, + }, + .paths = .{ + // This makes *all* files, recursively, included in this package. It is generally + // better to explicitly list the files and directories instead, to insure that + // fetching from tarballs, file system paths, and version control all result + // in the same contents hash. + "", + // For example... + //"build.zig", + //"build.zig.zon", + //"src", + //"LICENSE", + //"README.md", + }, +} diff --git a/src/assets.zig b/src/assets.zig new file mode 100644 index 0000000..5ebbcf5 --- /dev/null +++ b/src/assets.zig @@ -0,0 +1,394 @@ +pub const Suit = enum(u2) { + clubs = 0b00, + spades = 0b01, + hearts = 0b10, + diamonds = 0b11, + + pub fn color(this: @This()) u1 { + return @intCast((@intFromEnum(this) & 0b10) >> 1); + } +}; + +pub const Card = packed struct(u6) { + suit: Suit, + rank: Rank, + + pub const Rank = u4; +}; + +/// A texture with a regular grid of sprites +pub const TileSheet = struct { + texture: seizer.Texture, + tile_offset: [2]u32, + tile_size: [2]u32, + tile_stride: [2]u32, + + pub const InitOptions = struct { + allocator: std.mem.Allocator, + image_file_contents: []const u8, + tile_offset: [2]u32, + tile_size: [2]u32, + tile_stride: [2]u32, + texture_options: seizer.Texture.InitFromFileOptions = .{}, + }; + + pub fn init(options: InitOptions) !@This() { + const texture = try seizer.Texture.initFromFileContents( + options.allocator, + options.image_file_contents, + options.texture_options, + ); + return @This(){ + .texture = texture, + .tile_offset = options.tile_offset, + .tile_size = options.tile_size, + .tile_stride = options.tile_stride, + }; + } + + pub fn deinit(this: *@This()) void { + this.texture.deinit(); + } + + pub fn uvCoordinatesFromTileId(this: @This(), tile_id: u32) seizer.geometry.AABB(f32) { + const texture_sizef = [2]f32{ + @floatFromInt(this.texture.size[0]), + @floatFromInt(this.texture.size[1]), + }; + const tile_offsetf = [2]f32{ + @floatFromInt(this.tile_offset[0]), + @floatFromInt(this.tile_offset[1]), + }; + const tile_stridef = [2]f32{ + @floatFromInt(this.tile_stride[0]), + @floatFromInt(this.tile_stride[1]), + }; + const tile_sizef = [2]f32{ + @floatFromInt(this.tile_size[0]), + @floatFromInt(this.tile_size[1]), + }; + // add a half to round up + const size_in_tiles = [2]u32{ + (@as(u32, @intCast(this.texture.size[0])) + (this.tile_stride[0] / 2)) / this.tile_stride[0], + (@as(u32, @intCast(this.texture.size[1])) + (this.tile_stride[1] / 2)) / this.tile_stride[1], + }; + const pos_in_tiles = [2]u32{ + tile_id % size_in_tiles[0], + tile_id / size_in_tiles[0], + }; + const pos_in_tilesf = [2]f32{ + @floatFromInt(pos_in_tiles[0]), + @floatFromInt(pos_in_tiles[1]), + }; + + const pixel_pos = [2]f32{ + pos_in_tilesf[0] * tile_stridef[0] + tile_offsetf[0], + pos_in_tilesf[1] * tile_stridef[1] + tile_offsetf[1], + }; + return seizer.geometry.AABB(f32){ + .min = .{ + pixel_pos[0] / texture_sizef[0], + pixel_pos[1] / texture_sizef[1], + }, + .max = .{ + (pixel_pos[0] + tile_sizef[0]) / texture_sizef[0], + (pixel_pos[1] + tile_sizef[1]) / texture_sizef[1], + }, + }; + } + + pub fn renderTile(this: @This(), canvas: *seizer.Canvas, tile_id: u32, pos: [2]f32, options: struct { + size: ?[2]f32 = null, + color: [4]u8 = [4]u8{ 0xFF, 0xFF, 0xFF, 0xFF }, + }) void { + const uv = this.uvCoordinatesFromTileId(tile_id); + + canvas.rect(pos, options.size orelse [2]f32{ @floatFromInt(this.tile_size[0]), @floatFromInt(this.tile_size[1]) }, .{ + .texture = this.texture.glTexture, + .uv = uv, + .color = options.color, + }); + } +}; + +test "tilesheet math is exact" { + const tilesheet = TileSheet{ + .texture = .{ + .glTexture = undefined, + .size = .{ 494, 329 }, + }, + .tile_offset = .{ 6, 2 }, + .tile_size = .{ 20, 29 }, + .tile_stride = .{ 33, 33 }, + }; + + try std.testing.expectEqualDeep(seizer.geometry.AABB(f32){ + .min = .{ + 468.0 / 494.0, + 35.0 / 329.0, + }, + .max = .{ + (468.0 + 20.0) / 494.0, + (35.0 + 29.0) / 329.0, + }, + }, tilesheet.uvCoordinatesFromTileId(29)); +} + +/// A texture with a regular grid of sprites +pub const DeckSprites = struct { + tilesheet: TileSheet, + /// Return the tile index for a given card, + mapping: std.AutoHashMapUnmanaged(Card, u32), + blank: u32, + back: u32, + margin: u32, + hand_offset: [2]u32, + + pub fn deinit(this: *@This(), gpa: std.mem.Allocator) void { + this.tilesheet.deinit(); + this.mapping.deinit(gpa); + } + + pub fn getTileForCard(this: @This(), card: Card) u32 { + return this.mapping.get(card) orelse 0; + } +}; + +pub fn loadSmallCards(gpa: std.mem.Allocator) !DeckSprites { + var tilesheet = try TileSheet.init(.{ + .allocator = gpa, + .image_file_contents = @embedFile("./cardsSmall_tilemap.png"), + .tile_offset = .{ 0, 0 }, + .tile_size = .{ 16, 16 }, + .tile_stride = .{ 17, 17 }, + .texture_options = .{ + .min_filter = .nearest, + .mag_filter = .nearest, + }, + }); + errdefer tilesheet.deinit(); + + var mapping = std.AutoHashMap(Card, u32).init(gpa); + errdefer mapping.deinit(); + try mapping.ensureTotalCapacity(52); + + const hearts_start_index: u32 = 0; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .hearts, .rank = @intCast(rank + 1) }, + hearts_start_index + @as(u32, @intCast(rank)), + ); + } + + const diamonds_start_index: u32 = 14; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .diamonds, .rank = @intCast(rank + 1) }, + diamonds_start_index + @as(u32, @intCast(rank)), + ); + } + + const clubs_start_index: u32 = 28; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .clubs, .rank = @intCast(rank + 1) }, + clubs_start_index + @as(u32, @intCast(rank)), + ); + } + + const spades_start_index: u32 = 42; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .spades, .rank = @intCast(rank + 1) }, + spades_start_index + @as(u32, @intCast(rank)), + ); + } + + return DeckSprites{ + .tilesheet = tilesheet, + .mapping = mapping.unmanaged, + // TODO: add better graphic for blank card + .blank = 59, + .back = 59, + }; +} + +pub fn loadMediumCards(gpa: std.mem.Allocator) !DeckSprites { + var tilesheet = try TileSheet.init(.{ + .allocator = gpa, + .image_file_contents = @embedFile("./cardsMedium_tilemap.png"), + .tile_offset = .{ 6, 2 }, + .tile_size = .{ 20, 29 }, + .tile_stride = .{ 33, 33 }, + .texture_options = .{ + .min_filter = .nearest, + .mag_filter = .nearest, + }, + }); + errdefer tilesheet.deinit(); + + var mapping = std.AutoHashMap(Card, u32).init(gpa); + errdefer mapping.deinit(); + try mapping.ensureTotalCapacity(52); + + const hearts_start_index: u32 = 0; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .hearts, .rank = @intCast(rank + 1) }, + hearts_start_index + @as(u32, @intCast(rank)), + ); + } + + const diamonds_start_index: u32 = 15; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .diamonds, .rank = @intCast(rank + 1) }, + diamonds_start_index + @as(u32, @intCast(rank)), + ); + } + + const clubs_start_index: u32 = 30; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .clubs, .rank = @intCast(rank + 1) }, + clubs_start_index + @as(u32, @intCast(rank)), + ); + } + + const spades_start_index: u32 = 45; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .spades, .rank = @intCast(rank + 1) }, + spades_start_index + @as(u32, @intCast(rank)), + ); + } + + return DeckSprites{ + .tilesheet = tilesheet, + .mapping = mapping.unmanaged, + .blank = 14, + .back = 29, + }; +} + +pub fn loadLargeCards(gpa: std.mem.Allocator) !DeckSprites { + var tilesheet = try TileSheet.init(.{ + .allocator = gpa, + .image_file_contents = @embedFile("./cardsLarge_tilemap.png"), + .tile_offset = .{ 11, 2 }, + .tile_size = .{ 41, 60 }, + .tile_stride = .{ 65, 65 }, + .texture_options = .{ + .min_filter = .nearest, + .mag_filter = .nearest, + }, + }); + errdefer tilesheet.deinit(); + + var mapping = std.AutoHashMap(Card, u32).init(gpa); + errdefer mapping.deinit(); + try mapping.ensureTotalCapacity(52); + + const hearts_start_index: u32 = 0; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .hearts, .rank = @intCast(rank + 1) }, + hearts_start_index + @as(u32, @intCast(rank)), + ); + } + + const diamonds_start_index: u32 = 14; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .diamonds, .rank = @intCast(rank + 1) }, + diamonds_start_index + @as(u32, @intCast(rank)), + ); + } + + const clubs_start_index: u32 = 28; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .clubs, .rank = @intCast(rank + 1) }, + clubs_start_index + @as(u32, @intCast(rank)), + ); + } + + const spades_start_index: u32 = 42; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .spades, .rank = @intCast(rank + 1) }, + spades_start_index + @as(u32, @intCast(rank)), + ); + } + + return DeckSprites{ + .tilesheet = tilesheet, + .mapping = mapping.unmanaged, + .blank = 13, + .back = 27, + }; +} + +pub fn loadSolitaireCards(gpa: std.mem.Allocator) !DeckSprites { + var tilesheet = try TileSheet.init(.{ + .allocator = gpa, + .image_file_contents = @embedFile("./solitaire-cards.png"), + .tile_offset = .{ 0, 0 }, + .tile_size = .{ 32, 40 }, + .tile_stride = .{ 32, 40 }, + .texture_options = .{ + .min_filter = .nearest, + .mag_filter = .nearest, + }, + }); + errdefer tilesheet.deinit(); + + var mapping = std.AutoHashMap(Card, u32).init(gpa); + errdefer mapping.deinit(); + try mapping.ensureTotalCapacity(52); + + const hearts_start_index: u32 = 0; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .hearts, .rank = @intCast(rank + 1) }, + hearts_start_index + @as(u32, @intCast(rank)), + ); + } + + const diamonds_start_index: u32 = 14; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .diamonds, .rank = @intCast(rank + 1) }, + diamonds_start_index + @as(u32, @intCast(rank)), + ); + } + + const clubs_start_index: u32 = 28; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .clubs, .rank = @intCast(rank + 1) }, + clubs_start_index + @as(u32, @intCast(rank)), + ); + } + + const spades_start_index: u32 = 42; + for (0..13) |rank| { + mapping.putAssumeCapacityNoClobber( + Card{ .suit = .spades, .rank = @intCast(rank + 1) }, + spades_start_index + @as(u32, @intCast(rank)), + ); + } + + return DeckSprites{ + .tilesheet = tilesheet, + .mapping = mapping.unmanaged, + .blank = 13, + .back = 27, + .margin = 4, + .hand_offset = .{ 13, 12 }, + }; +} + +const seizer = @import("seizer"); +const gl = seizer.gl; +const std = @import("std"); diff --git a/src/cardsLarge_tilemap.png b/src/cardsLarge_tilemap.png new file mode 100644 index 0000000000000000000000000000000000000000..d8e0ad8090788dc6a397dcde6ac8e8d594703708 GIT binary patch literal 13185 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 selected_card; + const is_hovered = deck_is_hovered and j >= hovered_card; + const color = if (is_selected) + [4]u8{ 0xFF, 0xA0, 0xA0, 0xFF } + else if (is_hovered) + [4]u8{ 0xA0, 0xFF, 0xA0, 0xFF } + else + [4]u8{ 0xFF, 0xFF, 0xFF, 0xFF }; + + const tile_id = card_tilemap.?.getTileForCard(card); + card_tilemap.?.tilesheet.renderTile(&canvas, tile_id, pos, .{ + .size = tile_sizef, + .color = color, + }); + pos[1] += @floatFromInt(hand_offset[1]); + } + } + + 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{ + @floatFromInt(margin), + @floatFromInt(margin + ((margin + tile_size[1]) * (i + 1))), + }; + canvas.rect(pos, tile_sizef, .{ .color = color }); + + for (foundation.items) |card| { + const tile_id = card_tilemap.?.getTileForCard(card); + card_tilemap.?.tilesheet.renderTile(&canvas, tile_id, pos, .{ + .size = tile_sizef, + .color = color, + }); + } + } + + { + const deck_is_selected = selected_deck != null and selected_deck.? == &drawn_cards; + const deck_is_hovered = hovered_deck != null and hovered_deck.? == &drawn_cards; + + var pos = [2]f32{ + @floatFromInt(margin + margin + tile_size[0]), + @floatFromInt(margin), + }; + 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; + const color = if (is_selected) + [4]u8{ 0xFF, 0xA0, 0xA0, 0xFF } + else if (is_hovered) + [4]u8{ 0xA0, 0xFF, 0xA0, 0xFF } + else + [4]u8{ 0xFF, 0xFF, 0xFF, 0xFF }; + + const tile_id = card_tilemap.?.getTileForCard(card); + card_tilemap.?.tilesheet.renderTile(&canvas, tile_id, pos, .{ + .size = tile_sizef, + .color = color, + }); + pos[0] += @floatFromInt(hand_offset[0]); + } + } + + { + const is_hovered = hovered_deck != null and hovered_deck.? == &draw_pile; + 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) }; + 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, .{ + .size = tile_sizef, + .color = color, + }); + pos[1] -= 0.25 * scalef; + } + } + + var text_writer = canvas.textWriter(.{}); + for (1..@intFromEnum(seizer.backend.glfw.Joystick.Id.last)) |i| { + const joystick = seizer.backend.glfw.Joystick{ .jid = @enumFromInt(i) }; + if (joystick.getName()) |name| { + text_writer.writer().print("connected_controller[{}] = {s}\n", .{ i, name }) catch {}; + } + } + + canvas.end(); +} + +pub fn makeStandardDeck(allocator: std.mem.Allocator) ![]Card { + var deck = try allocator.alloc(Card, 52); + errdefer allocator.free(deck); + + 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; + } + } + + return deck; +} + +pub fn key_input_callback( + window: seizer.backend.glfw.Window, + key: seizer.backend.glfw.Key, + scancode: i32, + action: seizer.backend.glfw.Action, + mods: seizer.backend.glfw.Mods, +) void { + _ = window; + _ = scancode; + _ = mods; + switch (action) { + .press => switch (key) { + .z => doSelectOrPlace(), + .left => moveLeft(), + .right => moveRight(), + .down => moveDown(), + .up => moveUp(), + else => {}, + }, + else => {}, + } +} + +pub fn doSelectOrPlace() void { + if (selected_deck == null) { + if (hovered_deck == &draw_pile and !draw_pile_exhausted) { + drawn_cards.ensureUnusedCapacity(gpa, 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; + } + } + } else if (hovered_deck == selected_deck) { + const selected_substack = selected_deck.?.items[selected_card..]; + + // try to move all selected cards into the foundations + move_cards_to_foundations: for (0..selected_substack.len) |_| { + for (foundations[0..]) |*foundation| { + if (hovered_deck == foundation) break :move_cards_to_foundations; + } + + 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; + if ((empty and ace) or (suit_matches and is_next_rank)) { + foundation.ensureUnusedCapacity(gpa, 1) catch continue; + foundation.appendAssumeCapacity(selected_deck.?.pop()); + hovered_card = indexOfTopOfStack(hovered_deck.?.items); + break; + } + } else { + break :move_cards_to_foundations; + } + } + selected_deck = null; + } else { + 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; + + const hovered = hovered_deck orelse break :move_from_selected_to_hovered; + + if (hovered.items.len == 0) { + for (stacks[0..]) |*stack| { + if (hovered != stack) continue; + hovered.ensureUnusedCapacity(gpa, 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); + 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(gpa, 1) catch break :move_from_selected_to_hovered; + hovered.appendAssumeCapacity(selected_deck.?.pop()); + hovered_card = indexOfTopOfStack(hovered_deck.?.items); + 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; + if ((empty and ace) or (suit_matches and is_next_rank)) { + foundation.ensureUnusedCapacity(gpa, 1) catch continue; + foundation.appendAssumeCapacity(selected_deck.?.pop()); + hovered_card = indexOfTopOfStack(hovered_deck.?.items); + break :move_from_selected_to_hovered; + } + } + } + + 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 == selected_substack[0].suit) + { + break :move_from_selected_to_hovered; + } + hovered.ensureUnusedCapacity(gpa, 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); + } + selected_deck = null; + } +} + +pub fn moveLeft() void { + 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]; + } 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; + } + } + } +} + +pub fn moveRight() void { + 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]; + } 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; + } + } + } +} + +pub fn moveUp() void { + 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; + } 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; + } + } + } +} + +pub fn moveDown() void { + 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; + } + + 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; + } + } + } +} + +pub fn indexOfTopOfStack(cards: []Card) usize { + if (cards.len < 2) return 0; + + var index = cards.len - 1; + while (index > 0) : (index -= 1) { + const prev_card = cards[index]; + const card = cards[index - 1]; + if (card.suit.color() == prev_card.suit.color()) break; + if (card.rank != prev_card.rank + 1) break; + } + return index; +} + +const DeckSprites = assets.DeckSprites; +const Card = assets.Card; + +const assets = @import("./assets.zig"); +const seizer = @import("seizer"); +const gl = seizer.gl; +const ecs = seizer.flecs; +const std = @import("std"); diff --git a/src/solitaire-cards.aseprite b/src/solitaire-cards.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..8f85317b89293eccb663459869ae39412271499d GIT binary patch literal 2485 zcmb_ddr(tX8V@2wtCCu2(H03UMX;-Y#tO(IQmv2mfv#<-LcLmasnU`vXb6&fi_UH= zAZT%o80uw95vM#du8UZRxw^copyGn|Lt$)JM)|G z`_B2DGv_7vvi(V^Zq9!m2=sK1zk+oWE@$h0$-$$6E3~uR zQv!bpu`_;6d4AUHvY*THzEMQXU7xIH*)hk-%=+Nn=|TSG4VNdT!g#g8%NNeLN8f)o zjTcaTu|`9oa$CEot4Eafw%vuPZy7X0I&>$(;5sc*GVRjF`_1SfuT5X0s<$0b`9_ta zt2RxDH&ijTh1kulmao8CbO=C*G|l$D;ZG1?&)Q2kMHit71rOP962_2>Z&O zO3_1Gb`-R6)IjlGTx=54cQe@W3b?9Q_N3cf!Kg z-?OxgzMQMO8{=h9KJsqH1-_(Hh;R6o+MD9jqWjX`bY#UUk!9p8dZyu2*)O1w;n596 z%|KG4l&0T%+$+ZBnB3?Rbg_a+CEJlV>AktL_de#Uph-R7FVXT3 zbxvcrX_xI&@RSQnEwUHfZNlXHd@Y&277PDiZl+BQ5Ftbc+_8?_8GjbI&X$sK8Piqz z1y_YX+N{*M*Xf5vb?Q5jk?|g3q^LzUy%ww&W)g?kPUd|w#)#h7oc9rnsXouV*g(fb z3rEp>BCRh`0D6)MKA2d);1ST?5gr=<6S}QVsfH}a^OAG)E>%_d3n2~{pFX~W9iNs|EzGXGwB2#>EwO)=lA^X|BIcElQDnj_|GOR+ z^Z}{T!;;s3a*q5{ZQ&PkRZ~f#l@&Znuw~(tGN#>fdzh496tqU%y4)VbEi)UhN7E-( z!&c?3m)=6W_|C)><2UF@o+|_$KZl;|LDDkoV9l5clswN_Y-kvovhYls5IU#+JyK4 zC;c27C;5+sgmtgUEOUJ!U8Ba)k=b{HeqU+85fzx!G`t(+YfAb-tjC}OE#%qqsCS?x*e<4@#>`Hau20sd31VtT4=S2t zp9tm9ZDd=G^2!OnI?-qJTH7HB=o9{O3zuxFrxQ?MMK{|tvNDVkWbZ;f=#N#JhA-Y` zvs6MQkj#FA(bt{@BJ;#*WMZyehgBm`V!%ieOV2rKhdY(=%)L{T2@98;cm5mVIhU1}562vf~m z+Q^G&JHe}kzuT}AIsDaWuD?SYKr@r!AK*)g{5ObtTcdhu5w0Cen~to6d&#J_rsjU( zo{Y)B1-XfzRjN0)>4&MsUzgZXm*{irn`{lfxtoXI%D!rGarGYIbg!aw+>`Bm#hVTa z`9rX<#2bc}CT6+FAp+_SM9SkSYF56O@JYp+V&&XMIU$R_B)n2gz46e}A|Lu2eaF=n zKbR6X=ts8d*(>ou0T8Hx`I+Z|w+FvPW>#dw71+@pr`SQZJkz`f=0kTdr;WO9veZEP7_CykoD*IIXd1B3O454aJ49RCMXgH4 zSM+pm;0({R;NiJ8s^dh z6ADC~Nfw`8f*&NR#<-YVv1%Y_Kt`&ehyd$D z;MTpuc&ldF{-1YHja}~6e zPegu?7KGoK<2vOmma2ZtQH4*PX=ArZH{=SCdo;0PBk}&oQ({X*zB8I-wnQLf(<^`X z$`_;nhv}MlpvROP+hm(#rO*sq}I#9H_U z&5-^ED>c+wHA+OQ*81|tMX%8lXPS+&P&rZRD96@WDQT?x^G@$_-@g$z7eWT!SQ5m_ cWB=28EFrGO>}`*+RkMtjq>%L}3(nf|U$$g7e*gdg literal 0 HcmV?d00001 diff --git a/src/solitaire-cards.png b/src/solitaire-cards.png new file mode 100644 index 0000000000000000000000000000000000000000..7f77ca8893c947fd0d2bc782759aec5b1dc31019 GIT binary patch literal 2501 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 literal 0 HcmV?d00001