From 54d04f4a2bf2fdcdd2a44645254dfcb0e3e85fe0 Mon Sep 17 00:00:00 2001 From: geemili Date: Mon, 26 Feb 2024 18:27:31 -0700 Subject: [PATCH] refactor: handler that returns a tree of elements --- src/assets.zig | 301 ++++++++++++++++++++++ src/main.zig | 688 ++++++++++++++++++++++++++----------------------- 2 files changed, 666 insertions(+), 323 deletions(-) create mode 100644 src/assets.zig diff --git a/src/assets.zig b/src/assets.zig new file mode 100644 index 0000000..c5b3a4a --- /dev/null +++ b/src/assets.zig @@ -0,0 +1,301 @@ +pub const Suit = enum(u2) { + clubs = 0b00, + spades = 0b01, + hearts = 0b10, + diamonds = 0b11, + + pub fn color(this: @This()) u1 { + return (@intFromEnum(this) & 0b10) >> 1; + } +}; + +pub const Card = packed struct(u6) { + suit: Suit, + 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 renderTile(this: @This(), canvas: *seizer.Canvas, tile_id: u32, pos: [2]f32, options: struct { + size: ?[2]f32 = null, + }) void { + 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], + }; + const uv = 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], + }, + }; + + canvas.rect(pos, options.size orelse tile_sizef, .{ + .texture = this.texture.glTexture, + .uv = uv, + }); + } +}; + +/// 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, + + 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, + }; +} + +const seizer = @import("seizer"); +const gl = seizer.gl; +const std = @import("std"); diff --git a/src/main.zig b/src/main.zig index 1590afa..07cd67b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,20 +1,5 @@ var gl_binding: gl.Binding = undefined; -const Suit = enum(u2) { - clubs = 0b00, - spades = 0b01, - hearts = 0b10, - diamonds = 0b11, - - pub fn color(this: @This()) u1 { - return (@intFromEnum(this) & 0b10) >> 1; - } -}; -const Card = packed struct(u6) { - suit: Suit, - rank: u4, -}; - const GameState = struct { allocator: std.mem.Allocator, prng: std.rand.DefaultPrng, @@ -88,113 +73,6 @@ pub fn makeStandardDeck(allocator: std.mem.Allocator) ![]Card { return deck; } -/// A texture with a regular grid of sprites -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 renderTile(this: @This(), canvas: *seizer.Canvas, tile_id: u32, pos: [2]f32, options: struct { - size: ?[2]f32 = null, - }) void { - 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], - }; - const uv = 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], - }, - }; - - canvas.rect(pos, options.size orelse tile_sizef, .{ - .texture = this.texture.glTexture, - .uv = uv, - }); - } -}; - -/// A texture with a regular grid of sprites -const DeckSprites = struct { - tilesheet: TileSheet, - /// Return the tile index for a given card, - mapping: std.AutoHashMapUnmanaged(Card, u32), - blank: u32, - back: 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 main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); @@ -248,6 +126,12 @@ pub fn main() !void { var game_state = try GameState.init(gpa.allocator(), std.crypto.random.int(u64), 1); defer game_state.deinit(); + var root_element: ?Element = null; + var response_arena = std.heap.ArenaAllocator.init(gpa.allocator()); + defer response_arena.deinit(); + + const handler = &drawCardHandler; + var selection: usize = 0; while (seizer.backend.glfw.c.glfwWindowShouldClose(window) != seizer.backend.glfw.c.GLFW_TRUE) { @@ -259,10 +143,24 @@ pub fn main() !void { }; seizer.backend.glfw.c.glfwPollEvents(); - if (input_state.right) { + if (input_state.left) { + if (selection == 0) selection = game_state.hands[0].items.len; + selection = selection - 1; + } else if (input_state.right) { selection = (selection + 1) % game_state.hands[0].items.len; } + if (root_element == null) { + _ = response_arena.reset(.retain_capacity); + const response = try handler(response_arena.allocator(), Request{ + .game_state = game_state, + }); + + switch (response) { + .page => |page_root_element| root_element = page_root_element, + } + } + gl.clearColor(0.7, 0.5, 0.5, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); @@ -285,13 +183,13 @@ pub fn main() !void { switch (framebuffer_size[1]) { 0...300 => if (card_tilemap_small == null) { - card_tilemap_small = try loadSmallCards(gpa.allocator()); + card_tilemap_small = try assets.loadSmallCards(gpa.allocator()); }, 301...1000 => if (card_tilemap_medium == null) { - card_tilemap_medium = try loadMediumCards(gpa.allocator()); + card_tilemap_medium = try assets.loadMediumCards(gpa.allocator()); }, 1001...std.math.maxInt(c_int) => if (card_tilemap_large == null) { - card_tilemap_large = try loadLargeCards(gpa.allocator()); + card_tilemap_large = try assets.loadLargeCards(gpa.allocator()); }, else => unreachable, } @@ -303,47 +201,19 @@ pub fn main() !void { else => unreachable, }; - const draw_pile_pos = [2]f32{ - canvas.window_size[0] / 2 - @as(f32, @floatFromInt(deck_sprites.tilesheet.tile_size[0])), - canvas.window_size[1] / 2, - }; - const discard_pile_pos = [2]f32{ - canvas.window_size[0] / 2 + @as(f32, @floatFromInt(deck_sprites.tilesheet.tile_size[0])), - canvas.window_size[1] / 2, - }; - const hand_pos = [2]f32{ - (canvas.window_size[0] - @as(f32, @floatFromInt(game_state.hands[0].items.len * deck_sprites.tilesheet.tile_size[0]))) / 2, - canvas.window_size[1] - @as(f32, @floatFromInt(deck_sprites.tilesheet.tile_size[1])), - }; - - for (game_state.draw_pile.items, 0..) |_, i| { - const oy = -@as(f32, @floatFromInt(i)) * (@as(f32, @floatFromInt(deck_sprites.tilesheet.tile_size[1])) / (52.0 * 4)); - deck_sprites.tilesheet.renderTile(&canvas, deck_sprites.back, .{ - draw_pile_pos[0], - draw_pile_pos[1] + oy, - }, .{}); - } - - for (game_state.discard_pile.items, 0..) |card, i| { - const oy = -@as(f32, @floatFromInt(i)) * (@as(f32, @floatFromInt(deck_sprites.tilesheet.tile_size[1])) / (52.0 * 4)); - deck_sprites.tilesheet.renderTile(&canvas, deck_sprites.getTileForCard(card), .{ - discard_pile_pos[0], - discard_pile_pos[1] + oy, - }, .{}); - } - - for (game_state.hands[0].items, 0..) |card, i| { - const pos = [2]f32{ - hand_pos[0] + @as(f32, @floatFromInt(i)) * @as(f32, @floatFromInt(deck_sprites.tilesheet.tile_size[0])), - hand_pos[1], - }; - deck_sprites.tilesheet.renderTile(&canvas, deck_sprites.getTileForCard(card), pos, .{}); - if (i == selection) { - canvas.rect(pos, [2]f32{ - @floatFromInt(deck_sprites.tilesheet.tile_size[0]), - @floatFromInt(deck_sprites.tilesheet.tile_size[1]), - }, .{ .color = .{ 0xAA, 0xFF, 0xAA, 0x60 } }); - } + if (root_element) |root| { + root.interface.render( + root.pointer, + &canvas, + .{ + .deck = deck_sprites, + }, + .{ 0, 0 }, + .{ + @floatFromInt(window_size[0]), + @floatFromInt(window_size[1]), + }, + ); } canvas.end(); @@ -352,179 +222,347 @@ pub fn main() !void { } } -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(); +const RenderResources = struct { + deck: DeckSprites, +}; - var mapping = std.AutoHashMap(Card, u32).init(gpa); - errdefer mapping.deinit(); - try mapping.ensureTotalCapacity(52); +const HandlerError = error{OutOfMemory}; +const Handler = *const fn (std.mem.Allocator, Request) HandlerError!Response; - 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 Request = struct { + game_state: GameState, +}; +const Response = union(enum) { + page: Element, +}; - 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)), - ); - } +pub const Element = struct { + pointer: ?*anyopaque, + interface: *const Interface, - 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 const Interface = struct { + minimum_size: *const fn (?*anyopaque, RenderResources) [2]f32, + render: *const fn (?*anyopaque, *seizer.Canvas, RenderResources, [2]f32, [2]f32) void, }; -} +}; -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(); +pub const Page = struct { + allocator: std.mem.Allocator, + children: std.ArrayListUnmanaged(Child), - 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 const Child = struct { + /// Where is the child attached on the parent? Imagine it as a pin going through + /// both the parent and child element. This defines where on the parent that pin + /// passes through. + /// + /// In a virtual coordinate where <0,0> = top-left, <1,1> = bottom-right, unless + /// the numbers are negative. + anchor_in_parent: [2]f32, + /// Where is the child attached on the parent? Imagine it as a pin going through + /// both the parent and child element. This defines where on the child that pin + /// passes through. + /// + /// In a virtual coordinate where <0,0> = top-left, <1,1> = bottom-right, unless + /// the numbers are negative. + anchor_in_child: [2]f32, + element: Element, }; -} -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)), - ); + pub fn create(allocator: std.mem.Allocator) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + this.* = .{ + .allocator = allocator, + .children = .{}, + }; + return this; } - 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)), - ); + pub fn addElement(this: *@This(), anchor_in_parent: [2]f32, anchor_in_child: [2]f32, child_element: Element) !void { + try this.children.append(this.allocator, Child{ + .anchor_in_parent = anchor_in_parent, + .anchor_in_child = anchor_in_child, + .element = child_element, + }); } - 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)), - ); + pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .minimum_size = &element_minimum_size, + .render = &element_render, + }, + }; } - 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)), - ); + pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + var minimum_size = [2]f32{ 0, 0 }; + for (this.children.items) |child| { + const child_size = child.element.interface.minimum_size(child.element.pointer, render_resources); + + minimum_size = .{ + @max(minimum_size[0], child_size[0]), + @max(minimum_size[1], child_size[1]), + }; + } + + return minimum_size; } - return DeckSprites{ - .tilesheet = tilesheet, - .mapping = mapping.unmanaged, - .blank = 13, - .back = 27, - }; + pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + const parent_size = [2]f32{ + max[0] - min[0], + max[1] - min[1], + }; + + for (this.children.items) |child| { + const pos_in_parent = [2]f32{ + child.anchor_in_parent[0] * parent_size[0], + child.anchor_in_parent[1] * parent_size[1], + }; + + const child_size = child.element.interface.minimum_size(child.element.pointer, render_resources); + + const pos_in_child = [2]f32{ + child.anchor_in_child[0] * child_size[0], + child.anchor_in_child[1] * child_size[1], + }; + + const child_min = [2]f32{ + pos_in_parent[0] - pos_in_child[0], + pos_in_parent[1] - pos_in_child[1], + }; + const child_max = [2]f32{ + pos_in_parent[0] + (child_size[0] - pos_in_child[0]), + pos_in_parent[1] + (child_size[1] - pos_in_child[1]), + }; + + child.element.interface.render(child.element.pointer, canvas, render_resources, child_min, child_max); + } + } +}; + +pub const HBox = struct { + allocator: std.mem.Allocator, + children: std.ArrayListUnmanaged(Element), + + pub fn create(allocator: std.mem.Allocator) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + this.* = .{ + .allocator = allocator, + .children = .{}, + }; + return this; + } + + pub fn addElement(this: *@This(), child_element: Element) !void { + try this.children.append(this.allocator, child_element); + } + + pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .minimum_size = &element_minimum_size, + .render = &element_render, + }, + }; + } + + pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + var minimum_size = [2]f32{ 0, 0 }; + for (this.children.items) |child| { + const child_size = child.interface.minimum_size(child.pointer, render_resources); + + minimum_size = .{ + minimum_size[0] + child_size[0], + @max(minimum_size[1], child_size[1]), + }; + } + + return minimum_size; + } + + pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + if (this.children.items.len == 0) return; + + const parent_size = [2]f32{ + max[0] - min[0], + max[1] - min[1], + }; + + var filled_space: f32 = 0; + for (this.children.items) |child| { + const child_size = child.interface.minimum_size(child.pointer, render_resources); + + filled_space += child_size[0]; + } + + const empty_space = parent_size[0] - filled_space; + + const space_around = empty_space / @as(f32, @floatFromInt((this.children.items.len + 1))); + + var x: f32 = min[0] + space_around; + for (this.children.items) |child| { + const child_size = child.interface.minimum_size(child.pointer, render_resources); + + const child_min = [2]f32{ x, min[1] }; + const child_max = [2]f32{ x + child_size[0], max[1] }; + + child.interface.render(child.pointer, canvas, render_resources, child_min, child_max); + + x += child_size[0] + space_around; + } + } +}; + +pub const Pile = struct { + allocator: std.mem.Allocator, + cards: []Card, + hidden: bool = false, + + pub fn create(allocator: std.mem.Allocator, cards: []const Card) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + + const cards_owned = try allocator.dupe(Card, cards); + errdefer allocator.free(cards_owned); + + this.* = .{ + .allocator = allocator, + .cards = cards_owned, + }; + return this; + } + + pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .minimum_size = &element_minimum_size, + .render = &element_render, + }, + }; + } + + pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + return .{ + @floatFromInt(render_resources.deck.tilesheet.tile_size[0]), + @as(f32, (@floatFromInt(render_resources.deck.tilesheet.tile_size[1]))) + @as(f32, (@floatFromInt(render_resources.deck.tilesheet.tile_size[1]))) * @as(f32, @floatFromInt(this.cards.len)) / 52.0 * 0.25, + }; + } + + pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + const start_y: f32 = max[1] - @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])); + + for (this.cards, 0..) |card, i| { + const oy = -@as(f32, @floatFromInt(i)) * (@as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])) / (52.0 * 4)); + if (this.hidden) { + render_resources.deck.tilesheet.renderTile(canvas, render_resources.deck.back, .{ + min[0], + start_y + oy, + }, .{}); + } else { + render_resources.deck.tilesheet.renderTile(canvas, render_resources.deck.getTileForCard(card), .{ + min[0], + start_y + oy, + }, .{}); + } + } + } +}; + +pub const Spread = struct { + allocator: std.mem.Allocator, + cards: []Card, + hidden: bool = false, + + pub fn create(allocator: std.mem.Allocator, cards: []const Card) !*@This() { + const this = try allocator.create(@This()); + errdefer allocator.destroy(this); + + const cards_owned = try allocator.dupe(Card, cards); + errdefer allocator.free(cards_owned); + + this.* = .{ + .allocator = allocator, + .cards = cards_owned, + }; + return this; + } + + pub fn element(this: *@This()) Element { + return Element{ + .pointer = this, + .interface = &Element.Interface{ + .minimum_size = &element_minimum_size, + .render = &element_render, + }, + }; + } + + pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: RenderResources) [2]f32 { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + return .{ + @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[0])) * @as(f32, @floatFromInt(this.cards.len)), + @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])), + }; + } + + pub fn element_render(pointer: ?*anyopaque, canvas: *seizer.Canvas, render_resources: RenderResources, min: [2]f32, max: [2]f32) void { + const this: *@This() = @ptrCast(@alignCast(pointer)); + + const start_y = max[1] - @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])); + + for (this.cards, 0..) |card, i| { + const ox = @as(f32, @floatFromInt(i)) * @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[0])); + if (this.hidden) { + render_resources.deck.tilesheet.renderTile(canvas, render_resources.deck.back, .{ + min[0] + ox, + start_y, + }, .{}); + } else { + render_resources.deck.tilesheet.renderTile(canvas, render_resources.deck.getTileForCard(card), .{ + min[0] + ox, + start_y, + }, .{}); + } + } + } +}; + +/// Handler at the start of a turn, while the player is drawing a card. +fn drawCardHandler(arena: std.mem.Allocator, request: Request) HandlerError!Response { + var draw_pile = try Pile.create(arena, request.game_state.draw_pile.items); + draw_pile.hidden = true; + + var discard_pile = try Pile.create(arena, request.game_state.discard_pile.items); + var hand = try Spread.create(arena, request.game_state.hands[0].items); + + var draw_discard_hbox = try HBox.create(arena); + try draw_discard_hbox.addElement(draw_pile.element()); + try draw_discard_hbox.addElement(discard_pile.element()); + + var page = try Page.create(arena); + try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, draw_discard_hbox.element()); + try page.addElement(.{ 0.5, 1 }, .{ 0.5, 1 }, hand.element()); + + return Response{ .page = page.element() }; } fn glfw_framebuffer_size_callback(window: ?*seizer.backend.glfw.c.GLFWwindow, width: c_int, height: c_int) callconv(.C) void { @@ -560,6 +598,10 @@ fn glfw_key_callback(window: ?*seizer.backend.glfw.c.GLFWwindow, key: c_int, sca } } +const DeckSprites = assets.DeckSprites; +const Card = assets.Card; + +const assets = @import("./assets.zig"); const seizer = @import("seizer"); const gl = seizer.gl; const std = @import("std");