Compare commits

...

11 Commits

Author SHA1 Message Date
LeRoyce Pearson c5526ba229 feat: activating and undoing actions 2024-02-26 21:55:51 -07:00
LeRoyce Pearson 1134f0ee08 improve navigation with the arrow keys 2024-02-26 21:00:01 -07:00
LeRoyce Pearson 45bd514bd3 remove `element` from `Action`, rely only on `command` 2024-02-26 20:39:10 -07:00
LeRoyce Pearson a7bc7e87e0 replace `Spread` with `CardElements` and an `HBox` 2024-02-26 20:18:39 -07:00
LeRoyce Pearson 91f896c5ef implement hovering over elements 2024-02-26 19:50:26 -07:00
LeRoyce Pearson 54d04f4a2b refactor: handler that returns a tree of elements 2024-02-26 18:27:31 -07:00
LeRoyce Pearson d63a6d06fb feat: highlight selected card, change highlight card 2024-02-26 13:59:14 -07:00
LeRoyce Pearson 45bc09b612 render draw/discard/hand 2024-02-25 00:19:34 -07:00
LeRoyce Pearson 695c4d6280 feat: add small size graphics 2024-02-24 16:44:41 -07:00
LeRoyce Pearson 680f56a39d feat: render stack of cards
Added `tile_offset` and `tile_stride` to render exactly the card without
the blank space surrounding it on the tile.
2024-02-24 16:27:37 -07:00
LeRoyce Pearson 755518bb5d feat: use framebuffer size to choose sprite set
Using multiple sizes of sprites will hopefully make porting the game to
a wider range of devices easier.
2024-02-24 15:58:55 -07:00
2 changed files with 1142 additions and 85 deletions

301
src/assets.zig Normal file
View File

@ -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");

View File

@ -1,89 +1,110 @@
var gl_binding: gl.Binding = undefined; var gl_binding: gl.Binding = undefined;
const Suit = enum(u2) { const HandlerError = error{OutOfMemory};
clubs = 0b00, const Handler = *const fn (std.mem.Allocator, Request) HandlerError!Response;
spades = 0b01,
hearts = 0b10,
diamonds = 0b11,
pub fn color(this: @This()) u1 { const Request = struct {
return (@intFromEnum(this) & 0b10) >> 1; game_state: GameState,
} command: ?[]const u8,
}; };
const Card = packed struct(u6) { const Response = union(enum) {
suit: Suit, page: Element,
rank: u4, transition: struct {
game_state: GameState,
can_undo: bool,
},
}; };
/// A texture with a regular grid of sprites const GameState = struct {
const TileSheet = struct { allocator: std.mem.Allocator,
texture: seizer.Texture, prng: std.rand.DefaultPrng,
tile_size: [2]u32, handler: Handler,
draw_pile: std.ArrayListUnmanaged(Card),
discard_pile: std.ArrayListUnmanaged(Card),
hands: []std.ArrayListUnmanaged(Card),
pub const InitOptions = struct { pub fn init(allocator: std.mem.Allocator, seed: u64, num_players: usize) !@This() {
allocator: std.mem.Allocator, var draw_pile = std.ArrayListUnmanaged(Card).fromOwnedSlice(try makeStandardDeck(allocator));
image_file_contents: []const u8, errdefer draw_pile.deinit(allocator);
tile_size: [2]u32,
texture_options: seizer.Texture.InitFromFileOptions = .{}, var prng = std.rand.DefaultPrng.init(seed);
};
prng.random().shuffle(Card, draw_pile.items);
const cards_per_player: usize = switch (num_players) {
1, 2 => 7,
3, 4 => 6,
else => unreachable,
};
const hands = try allocator.alloc(std.ArrayListUnmanaged(Card), num_players);
errdefer allocator.free(hands);
for (hands) |*hand| {
hand.* = try std.ArrayListUnmanaged(Card).initCapacity(allocator, cards_per_player);
for (0..cards_per_player) |_| {
hand.appendAssumeCapacity(draw_pile.pop());
}
}
var discard_pile = try std.ArrayListUnmanaged(Card).initCapacity(allocator, 52);
errdefer discard_pile.deinit(allocator);
// start game with one card in the discard pile
discard_pile.appendAssumeCapacity(draw_pile.pop());
pub fn init(options: InitOptions) !@This() {
const texture = try seizer.Texture.initFromFileContents(
options.allocator,
options.image_file_contents,
options.texture_options,
);
return @This(){ return @This(){
.texture = texture, .allocator = allocator,
.tile_size = options.tile_size, .prng = prng,
.handler = &drawCardHandler,
.draw_pile = draw_pile,
.discard_pile = discard_pile,
.hands = hands,
}; };
} }
pub fn deinit(this: *@This()) void { pub fn deinit(this: *@This()) void {
this.texture.deinit(); this.draw_pile.deinit(this.allocator);
this.discard_pile.deinit(this.allocator);
for (this.hands) |*hand| {
hand.deinit(this.allocator);
}
this.allocator.free(this.hands);
} }
pub fn renderTile(this: @This(), canvas: *seizer.Canvas, tile_id: u32, pos: [2]f32, options: struct { pub fn clone(this: @This(), allocator: std.mem.Allocator) !@This() {
size: ?[2]f32 = null, const hands = try allocator.alloc(std.ArrayListUnmanaged(Card), this.hands.len);
}) void { for (hands, this.hands) |*hand_clone, hand| {
const texture_sizef = [2]f32{ hand_clone.* = try hand.clone(allocator);
@floatFromInt(this.texture.size[0]), }
@floatFromInt(this.texture.size[1]),
return @This(){
.allocator = allocator,
.prng = this.prng,
.handler = this.handler,
.draw_pile = try this.draw_pile.clone(allocator),
.discard_pile = try this.discard_pile.clone(allocator),
.hands = hands,
}; };
const tile_sizef = [2]f32{
@floatFromInt(this.tile_size[0]),
@floatFromInt(this.tile_size[1]),
};
const size_in_tiles = [2]u32{
@as(u32, @intCast(this.texture.size[0])) / this.tile_size[0],
@as(u32, @intCast(this.texture.size[1])) / this.tile_size[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 uv = seizer.geometry.AABB(f32){
.min = .{
(pos_in_tilesf[0] * tile_sizef[0]) / texture_sizef[0],
(pos_in_tilesf[1] * tile_sizef[1]) / texture_sizef[1],
},
.max = .{
((pos_in_tilesf[0] + 1) * tile_sizef[0]) / texture_sizef[0],
((pos_in_tilesf[1] + 1) * tile_sizef[1]) / texture_sizef[1],
},
};
_ = canvas.printText(.{ 0, 0 }, "uv = {}", .{uv}, .{});
canvas.rect(pos, options.size orelse tile_sizef, .{
.texture = this.texture.glTexture,
.uv = uv,
});
} }
}; };
// const cards_medium_spritesheet =
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 main() !void { pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
@ -116,25 +137,135 @@ pub fn main() !void {
gl.makeBindingCurrent(&gl_binding); gl.makeBindingCurrent(&gl_binding);
// Set up input callbacks // Set up input callbacks
var input_state: InputState = undefined;
_ = seizer.backend.glfw.c.glfwSetWindowUserPointer(window, &input_state);
_ = seizer.backend.glfw.c.glfwSetKeyCallback(window, &glfw_key_callback);
_ = seizer.backend.glfw.c.glfwSetFramebufferSizeCallback(window, &glfw_framebuffer_size_callback); _ = seizer.backend.glfw.c.glfwSetFramebufferSizeCallback(window, &glfw_framebuffer_size_callback);
var card_tilemap = try TileSheet.init(.{ var card_tilemap_small: ?DeckSprites = null;
.allocator = gpa.allocator(), defer if (card_tilemap_small) |*tilemap| tilemap.deinit(gpa.allocator());
.image_file_contents = @embedFile("./cardsMedium_tilemap.png"),
.tile_size = .{ 32, 32 }, var card_tilemap_medium: ?DeckSprites = null;
.texture_options = .{ defer if (card_tilemap_medium) |*tilemap| tilemap.deinit(gpa.allocator());
.min_filter = .nearest,
.mag_filter = .nearest, var card_tilemap_large: ?DeckSprites = null;
}, defer if (card_tilemap_large) |*tilemap| tilemap.deinit(gpa.allocator());
});
defer card_tilemap.deinit();
var canvas = try seizer.Canvas.init(gpa.allocator(), .{}); var canvas = try seizer.Canvas.init(gpa.allocator(), .{});
defer canvas.deinit(gpa.allocator()); defer canvas.deinit(gpa.allocator());
// game state
var history = std.ArrayList(GameState).init(gpa.allocator());
defer {
for (history.items) |*state| {
state.deinit();
}
history.deinit();
}
try history.append(try GameState.init(gpa.allocator(), std.crypto.random.int(u64), 1));
var root_element: ?Element = null;
var response_arena = std.heap.ArenaAllocator.init(gpa.allocator());
defer response_arena.deinit();
var actions = std.ArrayList(Action).init(gpa.allocator());
defer actions.deinit();
// TODO: Restore hovered_action when undoing
var hovered_action: ?Action = null;
while (seizer.backend.glfw.c.glfwWindowShouldClose(window) != seizer.backend.glfw.c.GLFW_TRUE) { while (seizer.backend.glfw.c.glfwWindowShouldClose(window) != seizer.backend.glfw.c.GLFW_TRUE) {
input_state = .{
.left = false,
.right = false,
.up = false,
.down = false,
.action = false,
.undo = false,
};
seizer.backend.glfw.c.glfwPollEvents(); seizer.backend.glfw.c.glfwPollEvents();
var request_command: ?[]const u8 = null;
defer if (request_command) |command| gpa.allocator().free(command);
if (hovered_action) |hovered| {
if (input_state.action) {
request_command = try gpa.allocator().dupe(u8, hovered.command);
root_element = null;
hovered_action = null;
}
} else if (actions.items.len > 0) {
hovered_action = actions.items[0];
}
if (input_state.undo and history.items.len > 1) {
var discarded_state = history.pop();
discarded_state.deinit();
root_element = null;
hovered_action = null;
}
if (hovered_action) |hovered| {
var direction = [2]f32{ 0, 0 };
if (input_state.left) {
direction[0] -= 1;
}
if (input_state.right) {
direction[0] += 1;
}
if (input_state.up) {
direction[1] -= 1;
}
if (input_state.down) {
direction[1] += 1;
}
var new_distance: ?f32 = null;
var new_action = hovered;
for (actions.items) |action| {
if (std.mem.eql(u8, action.command, hovered.command)) {
continue;
}
if (distanceToAction(hovered.center, direction, action.center)) |distance| {
if (new_distance == null or (new_distance != null and distance < new_distance.?)) {
new_action = Action{
.center = .{
if (input_state.left or input_state.right) action.center[0] else hovered.center[0],
if (input_state.up or input_state.down) action.center[1] else hovered.center[1],
},
.command = action.command,
};
new_distance = distance;
}
}
}
hovered_action = new_action;
}
while (root_element == null) {
_ = response_arena.reset(.retain_capacity);
const current_state = history.items[history.items.len - 1];
const response = try current_state.handler(response_arena.allocator(), Request{
.game_state = current_state,
.command = request_command,
});
switch (response) {
.page => |page_root_element| root_element = page_root_element,
.transition => |transition| {
if (!transition.can_undo) {
for (history.items) |*state| {
state.deinit();
}
history.clearRetainingCapacity();
}
try history.append(try transition.game_state.clone(gpa.allocator()));
},
}
}
gl.clearColor(0.7, 0.5, 0.5, 1.0); gl.clearColor(0.7, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT); gl.clear(gl.COLOR_BUFFER_BIT);
@ -154,19 +285,613 @@ pub fn main() !void {
@floatFromInt(framebuffer_size[1]), @floatFromInt(framebuffer_size[1]),
}, },
}); });
card_tilemap.renderTile(&canvas, 1, .{ 10, 10 }, .{});
// canvas.rect(.{ 10, 10 }, .{ @as(f32, @floatFromInt(card_tilemap.size[0])) / 15.0, @as(f32, @floatFromInt(card_tilemap.size[1])) / 10.0 }, .{ _ = canvas.printText(.{ 0, 0 }, "History len: {}", .{history.items.len}, .{});
// .texture = card_tilemap.glTexture,
// .uv = .{ .min = .{ 0, 0 }, .max = .{ 1.0 / 15.0, 1.0 / 10.0 } }, switch (framebuffer_size[1]) {
// }); 0...300 => if (card_tilemap_small == null) {
_ = canvas.writeText(.{ 50, 50 }, "Hello, world!", .{}); card_tilemap_small = try assets.loadSmallCards(gpa.allocator());
_ = canvas.printText(.{ 50, 100 }, "window_size = {}, {}\nframebuffer_size = {}, {}", .{ window_size[0], window_size[1], framebuffer_size[0], framebuffer_size[1] }, .{}); },
301...1000 => if (card_tilemap_medium == null) {
card_tilemap_medium = try assets.loadMediumCards(gpa.allocator());
},
1001...std.math.maxInt(c_int) => if (card_tilemap_large == null) {
card_tilemap_large = try assets.loadLargeCards(gpa.allocator());
},
else => unreachable,
}
const deck_sprites = switch (framebuffer_size[1]) {
0...300 => card_tilemap_small.?,
301...1000 => card_tilemap_medium.?,
1001...std.math.maxInt(c_int) => card_tilemap_large.?,
else => unreachable,
};
if (root_element) |root| {
root.interface.render(
root.pointer,
&canvas,
.{
.hovered = if (hovered_action) |ha| ha.command else null,
.deck = deck_sprites,
},
.{ 0, 0 },
.{
@floatFromInt(window_size[0]),
@floatFromInt(window_size[1]),
},
);
}
canvas.end(); canvas.end();
seizer.backend.glfw.c.glfwSwapBuffers(window); seizer.backend.glfw.c.glfwSwapBuffers(window);
if (root_element) |root| {
actions.clearRetainingCapacity();
try root.interface.get_actions(
root.pointer,
&actions,
.{
.hovered = if (hovered_action) |ha| ha.command else null,
.deck = deck_sprites,
},
.{ 0, 0 },
.{
@floatFromInt(window_size[0]),
@floatFromInt(window_size[1]),
},
);
}
} }
} }
fn distanceToAction(origin: [2]f32, direction: [2]f32, other_pos: [2]f32) ?f32 {
const off_axis_dir = [2]f32{ direction[1], -direction[0] };
const offset = [2]f32{
other_pos[0] - origin[0],
other_pos[1] - origin[1],
};
// use dot product to check how in line with each direction the point of interest is
const in_axis_distance = offset[0] * direction[0] + offset[1] * direction[1];
const off_axis_distance = offset[0] * off_axis_dir[0] + offset[1] * off_axis_dir[1];
if (in_axis_distance > 0) {
return in_axis_distance + off_axis_distance * off_axis_distance;
} else {
return null;
}
}
const Action = struct {
center: [2]f32,
/// A string representing what should occur if this action is taken
command: []const u8,
};
const RenderResources = struct {
hovered: ?[]const u8,
deck: DeckSprites,
};
pub const Element = struct {
pointer: ?*anyopaque,
interface: *const Interface,
pub const Error = error{
OutOfMemory,
};
pub const Interface = struct {
minimum_size: *const fn (?*anyopaque, RenderResources) [2]f32,
render: *const fn (?*anyopaque, *seizer.Canvas, RenderResources, [2]f32, [2]f32) void,
get_actions: *const fn (?*anyopaque, *std.ArrayList(Action), RenderResources, [2]f32, [2]f32) Error!void,
};
};
pub const Page = struct {
allocator: std.mem.Allocator,
children: std.ArrayListUnmanaged(Child),
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,
};
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(), 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,
});
}
pub fn element(this: *@This()) Element {
return Element{
.pointer = this,
.interface = &Element.Interface{
.minimum_size = &element_minimum_size,
.render = &element_render,
.get_actions = &element_get_actions,
},
};
}
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;
}
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{
@max(min[0], pos_in_parent[0] - pos_in_child[0]),
@max(min[1], pos_in_parent[1] - pos_in_child[1]),
};
const child_max = [2]f32{
@min(max[0], pos_in_parent[0] + (child_size[0] - pos_in_child[0])),
@min(max[1], 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 fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Action), render_resources: RenderResources, min: [2]f32, max: [2]f32) Element.Error!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]),
};
try child.element.interface.get_actions(child.element.pointer, actions, 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,
.get_actions = &element_get_actions,
},
};
}
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 num_spaces = if (empty_space > 0) this.children.items.len + 1 else this.children.items.len - 1;
const space_around = empty_space / @as(f32, @floatFromInt(num_spaces));
var x: f32 = min[0] + @max(space_around, 0);
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 fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Action), render_resources: RenderResources, min: [2]f32, max: [2]f32) Element.Error!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] };
try child.interface.get_actions(child.pointer, actions, 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,
command: ?[]const u8 = null,
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,
.get_actions = &element_get_actions,
},
};
}
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,
}, .{});
}
}
if (render_resources.hovered != null and this.command != null and std.mem.eql(u8, render_resources.hovered.?, this.command.?)) {
const oy = -@as(f32, @floatFromInt(this.cards.len)) * (@as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])) / (52.0 * 4));
canvas.rect(
.{ min[0], start_y + oy },
.{ @floatFromInt(render_resources.deck.tilesheet.tile_size[0]), @floatFromInt(render_resources.deck.tilesheet.tile_size[1]) },
.{ .color = .{ 0xAA, 0xFF, 0xAA, 0x60 } },
);
}
}
pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Action), render_resources: RenderResources, min: [2]f32, max: [2]f32) Element.Error!void {
const this: *@This() = @ptrCast(@alignCast(pointer));
if (this.command) |command| {
const center = [2]f32{
min[0] + @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[0])) / 2,
max[1] - @as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[1])) * (2.0 - @as(f32, @floatFromInt(this.cards.len)) / (52.0 * 4)),
};
try actions.append(.{
.center = center,
.command = command,
});
}
}
};
pub const CardElement = struct {
allocator: std.mem.Allocator,
visual: Visual,
command: ?[]const u8,
const Visual = union(enum) {
back,
card: Card,
};
pub fn create(allocator: std.mem.Allocator, options: struct { visual: Visual, command: ?[]const u8 }) !*@This() {
const this = try allocator.create(@This());
errdefer allocator.destroy(this);
this.* = .{
.allocator = allocator,
.visual = options.visual,
.command = options.command,
};
return this;
}
pub fn element(this: *@This()) Element {
return Element{
.pointer = this,
.interface = &Element.Interface{
.minimum_size = &element_minimum_size,
.render = &element_render,
.get_actions = &element_get_actions,
},
};
}
pub fn element_minimum_size(pointer: ?*anyopaque, render_resources: RenderResources) [2]f32 {
const this: *@This() = @ptrCast(@alignCast(pointer));
_ = this;
return .{
@as(f32, @floatFromInt(render_resources.deck.tilesheet.tile_size[0])),
@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));
switch (this.visual) {
.back => render_resources.deck.tilesheet.renderTile(
canvas,
render_resources.deck.back,
.{ min[0], min[1] },
.{},
),
.card => |card| render_resources.deck.tilesheet.renderTile(
canvas,
render_resources.deck.getTileForCard(card),
.{ min[0], min[1] },
.{},
),
}
if (render_resources.hovered != null and this.command != null and std.mem.eql(u8, render_resources.hovered.?, this.command.?)) {
canvas.rect(
min,
.{ max[0] - min[0], max[1] - min[1] },
.{ .color = .{ 0xAA, 0xFF, 0xAA, 0x60 } },
);
}
}
pub fn element_get_actions(pointer: ?*anyopaque, actions: *std.ArrayList(Action), render_resources: RenderResources, min: [2]f32, max: [2]f32) Element.Error!void {
const this: *@This() = @ptrCast(@alignCast(pointer));
_ = render_resources;
if (this.command) |command| {
try actions.append(.{
.center = [2]f32{
(min[0] + max[0]) / 2,
(min[1] + max[1]) / 2,
},
.command = command,
});
}
}
};
/// Handler at the start of a turn, while the player is drawing a card.
fn drawCardHandler(arena: std.mem.Allocator, request: Request) HandlerError!Response {
if (request.command) |command| {
if (std.mem.eql(u8, command, "draw draw_pile")) {
var new_game_state = try request.game_state.clone(arena);
try new_game_state.hands[0].append(arena, new_game_state.draw_pile.pop());
new_game_state.handler = &playerTurnHandler;
return Response{ .transition = .{
.game_state = new_game_state,
.can_undo = false,
} };
} else if (std.mem.eql(u8, command, "draw discard_pile")) {
var new_game_state = try request.game_state.clone(arena);
try new_game_state.hands[0].append(arena, new_game_state.discard_pile.pop());
new_game_state.handler = &playerTurnHandler;
return Response{ .transition = .{
.game_state = new_game_state,
.can_undo = true,
} };
}
}
var draw_pile = try Pile.create(arena, request.game_state.draw_pile.items);
draw_pile.hidden = true;
draw_pile.command = "draw draw_pile";
var discard_pile = try Pile.create(arena, request.game_state.discard_pile.items);
discard_pile.command = "draw discard_pile";
var hand = try HBox.create(arena);
for (request.game_state.hands[0].items) |card| {
var card_element = try CardElement.create(arena, .{
.visual = .{ .card = card },
.command = null,
});
try hand.addElement(card_element.element());
}
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 playerTurnHandler(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 HBox.create(arena);
for (request.game_state.hands[0].items) |card| {
var card_element = try CardElement.create(arena, .{
.visual = .{ .card = card },
.command = "mark",
});
try hand.addElement(card_element.element());
}
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 { fn glfw_framebuffer_size_callback(window: ?*seizer.backend.glfw.c.GLFWwindow, width: c_int, height: c_int) callconv(.C) void {
_ = window; _ = window;
gl.viewport( gl.viewport(
@ -177,6 +902,37 @@ fn glfw_framebuffer_size_callback(window: ?*seizer.backend.glfw.c.GLFWwindow, wi
); );
} }
const InputState = struct {
left: bool,
right: bool,
up: bool,
down: bool,
action: bool,
undo: bool,
};
fn glfw_key_callback(window: ?*seizer.backend.glfw.c.GLFWwindow, key: c_int, scancode: c_int, action: c_int, mods: c_int) callconv(.C) void {
const input_state = @as(*InputState, @alignCast(@ptrCast(seizer.backend.glfw.c.glfwGetWindowUserPointer(window))));
_ = scancode;
_ = mods;
if (action == seizer.backend.glfw.c.GLFW_PRESS) {
switch (key) {
seizer.backend.glfw.c.GLFW_KEY_LEFT => input_state.left = true,
seizer.backend.glfw.c.GLFW_KEY_RIGHT => input_state.right = true,
seizer.backend.glfw.c.GLFW_KEY_UP => input_state.up = true,
seizer.backend.glfw.c.GLFW_KEY_DOWN => input_state.down = true,
seizer.backend.glfw.c.GLFW_KEY_Z => input_state.action = true,
seizer.backend.glfw.c.GLFW_KEY_BACKSPACE => input_state.undo = true,
else => {},
}
}
}
const DeckSprites = assets.DeckSprites;
const Card = assets.Card;
const assets = @import("./assets.zig");
const seizer = @import("seizer"); const seizer = @import("seizer");
const gl = seizer.gl; const gl = seizer.gl;
const std = @import("std"); const std = @import("std");