Compare commits

...

5 Commits

Author SHA1 Message Date
LeRoyce Pearson e2a52dad0b feat: discard a card to end the turn 2024-02-28 15:26:13 -07:00
LeRoyce Pearson 52a247bceb don't reset position of selection when selecting cards
This makes it easier to select the next card.
2024-02-28 15:00:50 -07:00
LeRoyce Pearson 807712af79 fix: remove slight offset when cards were rendered
Fixed by updating version of seizer to one that doesn't offset the canvas
projection.
2024-02-28 14:47:35 -07:00
LeRoyce Pearson 84d8766124 send one move input per joystick button press
This makes it easier to control on the RG351M, as inputting a single
move no longer requires a frame perfect press and release.
2024-02-28 13:46:14 -07:00
LeRoyce Pearson 980d8cb6a1 initial support for RG351M controller input 2024-02-28 13:39:39 -07:00
4 changed files with 221 additions and 9 deletions

View File

@ -44,6 +44,7 @@ pub fn build(b: *std.Build) void {
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
exe_unit_tests.root_module.addImport("seizer", seizer.module("seizer"));
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);

View File

@ -16,8 +16,8 @@
// internet connectivity. // internet connectivity.
.dependencies = .{ .dependencies = .{
.seizer = .{ .seizer = .{
.url = "https://github.com/leroycep/seizer/archive/dd1f9f6c94e91edfd96f0075c5f5d33aa30758f4.tar.gz", .url = "https://github.com/leroycep/seizer/archive/8fdc6335641614c1cd844d1ecd5ad937584a443e.tar.gz",
.hash = "122009caed9e40d713c847b24c2abf8540299e32fb5e77f73ab2acdf5c27bc3e4c90", .hash = "1220b6f9d0aba788b55a3e26a34ca9793495fbdb062c463cef1433ff937b96aa77d6",
}, },
}, },
.paths = .{ .paths = .{

View File

@ -48,9 +48,7 @@ pub const TileSheet = struct {
this.texture.deinit(); this.texture.deinit();
} }
pub fn renderTile(this: @This(), canvas: *seizer.Canvas, tile_id: u32, pos: [2]f32, options: struct { pub fn uvCoordinatesFromTileId(this: @This(), tile_id: u32) seizer.geometry.AABB(f32) {
size: ?[2]f32 = null,
}) void {
const texture_sizef = [2]f32{ const texture_sizef = [2]f32{
@floatFromInt(this.texture.size[0]), @floatFromInt(this.texture.size[0]),
@floatFromInt(this.texture.size[1]), @floatFromInt(this.texture.size[1]),
@ -85,7 +83,7 @@ pub const TileSheet = struct {
pos_in_tilesf[0] * tile_stridef[0] + tile_offsetf[0], pos_in_tilesf[0] * tile_stridef[0] + tile_offsetf[0],
pos_in_tilesf[1] * tile_stridef[1] + tile_offsetf[1], pos_in_tilesf[1] * tile_stridef[1] + tile_offsetf[1],
}; };
const uv = seizer.geometry.AABB(f32){ return seizer.geometry.AABB(f32){
.min = .{ .min = .{
pixel_pos[0] / texture_sizef[0], pixel_pos[0] / texture_sizef[0],
pixel_pos[1] / texture_sizef[1], pixel_pos[1] / texture_sizef[1],
@ -95,14 +93,43 @@ pub const TileSheet = struct {
(pixel_pos[1] + tile_sizef[1]) / texture_sizef[1], (pixel_pos[1] + tile_sizef[1]) / texture_sizef[1],
}, },
}; };
}
canvas.rect(pos, options.size orelse tile_sizef, .{ pub fn renderTile(this: @This(), canvas: *seizer.Canvas, tile_id: u32, pos: [2]f32, options: struct {
size: ?[2]f32 = null,
}) 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, .texture = this.texture.glTexture,
.uv = uv, .uv = uv,
}); });
} }
}; };
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 /// A texture with a regular grid of sprites
pub const DeckSprites = struct { pub const DeckSprites = struct {
tilesheet: TileSheet, tilesheet: TileSheet,

View File

@ -12,6 +12,7 @@ const Response = union(enum) {
transition: struct { transition: struct {
game_state: GameState, game_state: GameState,
can_undo: bool, can_undo: bool,
reset_selection: bool = false,
}, },
}; };
@ -127,6 +128,36 @@ pub fn main() !void {
} }
defer seizer.backend.glfw.c.glfwTerminate(); defer seizer.backend.glfw.c.glfwTerminate();
// update joystick to gamepad mappings
update_gamepad_mappings_file: {
const sdl_controller_config_filepath = std.process.getEnvVarOwned(gpa.allocator(), "SDL_GAMECONTROLLERCONFIG_FILE") catch break :update_gamepad_mappings_file;
defer gpa.allocator().free(sdl_controller_config_filepath);
const controller_config_data = std.fs.cwd().readFileAllocOptions(
gpa.allocator(),
sdl_controller_config_filepath,
512 * 1024 * 1024,
null,
@alignOf(u8),
0,
) catch break :update_gamepad_mappings_file;
defer gpa.allocator().free(controller_config_data);
if (seizer.backend.glfw.c.glfwUpdateGamepadMappings(controller_config_data) == 0) {
std.log.warn("Failed to update gamepad mappings from file", .{});
}
}
update_gamepad_mappings: {
const sdl_controller_config = std.process.getEnvVarOwned(gpa.allocator(), "SDL_GAMECONTROLLERCONFIG") catch break :update_gamepad_mappings;
defer gpa.allocator().free(sdl_controller_config);
const sdl_controller_configz = gpa.allocator().dupeZ(u8, sdl_controller_config) catch break :update_gamepad_mappings;
if (seizer.backend.glfw.c.glfwUpdateGamepadMappings(sdl_controller_configz) == 0) {
std.log.warn("Failed to update gamepad mappings from environment variable", .{});
}
}
seizer.backend.glfw.c.glfwWindowHint(seizer.backend.glfw.c.GLFW_OPENGL_DEBUG_CONTEXT, seizer.backend.glfw.c.GLFW_TRUE); seizer.backend.glfw.c.glfwWindowHint(seizer.backend.glfw.c.GLFW_OPENGL_DEBUG_CONTEXT, seizer.backend.glfw.c.GLFW_TRUE);
seizer.backend.glfw.c.glfwWindowHint(seizer.backend.glfw.c.GLFW_CLIENT_API, seizer.backend.glfw.c.GLFW_OPENGL_ES_API); seizer.backend.glfw.c.glfwWindowHint(seizer.backend.glfw.c.GLFW_CLIENT_API, seizer.backend.glfw.c.GLFW_OPENGL_ES_API);
seizer.backend.glfw.c.glfwWindowHint(seizer.backend.glfw.c.GLFW_CONTEXT_VERSION_MAJOR, 3); seizer.backend.glfw.c.glfwWindowHint(seizer.backend.glfw.c.GLFW_CONTEXT_VERSION_MAJOR, 3);
@ -184,6 +215,21 @@ pub fn main() !void {
// TODO: Restore hovered_action when undoing // TODO: Restore hovered_action when undoing
var hovered_action: ?Action = null; var hovered_action: ?Action = null;
if (seizer.backend.glfw.c.glfwJoystickIsGamepad(seizer.backend.glfw.c.GLFW_JOYSTICK_1) != 0) {
std.log.info("detected gamepad = \"{?s}\" {?s}", .{ seizer.backend.glfw.c.glfwGetGamepadName(seizer.backend.glfw.c.GLFW_JOYSTICK_1), seizer.backend.glfw.c.glfwGetJoystickGUID(seizer.backend.glfw.c.GLFW_JOYSTICK_1) });
} else if (seizer.backend.glfw.c.glfwJoystickPresent(seizer.backend.glfw.c.GLFW_JOYSTICK_1) != 0) {
std.log.info("detected joystick = \"{?s}\" {?s}", .{ seizer.backend.glfw.c.glfwGetJoystickName(seizer.backend.glfw.c.GLFW_JOYSTICK_1), seizer.backend.glfw.c.glfwGetJoystickGUID(seizer.backend.glfw.c.GLFW_JOYSTICK_1) });
}
var prev_controller_input_state = InputState{
.left = false,
.right = false,
.up = false,
.down = false,
.action = false,
.undo = false,
};
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 = .{ input_state = .{
.left = false, .left = false,
@ -195,6 +241,55 @@ pub fn main() !void {
}; };
seizer.backend.glfw.c.glfwPollEvents(); seizer.backend.glfw.c.glfwPollEvents();
if (seizer.backend.glfw.c.glfwJoystickIsGamepad(seizer.backend.glfw.c.GLFW_JOYSTICK_1) != 0) {
var gamepad_state: seizer.backend.glfw.c.GLFWgamepadstate = undefined;
if (seizer.backend.glfw.c.glfwGetGamepadState(seizer.backend.glfw.c.GLFW_JOYSTICK_1, &gamepad_state) != 0) {
input_state.left = input_state.left or gamepad_state.buttons[seizer.backend.glfw.c.GLFW_GAMEPAD_BUTTON_DPAD_LEFT] != 0;
input_state.right = input_state.right or gamepad_state.buttons[seizer.backend.glfw.c.GLFW_GAMEPAD_BUTTON_DPAD_RIGHT] != 0;
input_state.up = input_state.up or gamepad_state.buttons[seizer.backend.glfw.c.GLFW_GAMEPAD_BUTTON_DPAD_UP] != 0;
input_state.down = input_state.down or gamepad_state.buttons[seizer.backend.glfw.c.GLFW_GAMEPAD_BUTTON_DPAD_DOWN] != 0;
}
} else if (seizer.backend.glfw.c.glfwJoystickPresent(seizer.backend.glfw.c.GLFW_JOYSTICK_1) != 0) {
var controller_input_state = InputState{
.left = false,
.right = false,
.up = false,
.down = false,
.action = false,
.undo = false,
};
var hats_count: c_int = undefined;
const hats_ptr = seizer.backend.glfw.c.glfwGetJoystickHats(seizer.backend.glfw.c.GLFW_JOYSTICK_1, &hats_count);
const hats = hats_ptr[0..@intCast(hats_count)];
const HAT_UP = seizer.backend.glfw.c.GLFW_HAT_UP;
const HAT_RIGHT = seizer.backend.glfw.c.GLFW_HAT_RIGHT;
const HAT_DOWN = seizer.backend.glfw.c.GLFW_HAT_DOWN;
const HAT_LEFT = seizer.backend.glfw.c.GLFW_HAT_LEFT;
controller_input_state.left = hats[0] & HAT_LEFT != 0;
controller_input_state.right = hats[0] & HAT_RIGHT != 0;
controller_input_state.up = hats[0] & HAT_UP != 0;
controller_input_state.down = hats[0] & HAT_DOWN != 0;
var buttons_count: c_int = undefined;
const buttons_ptr = seizer.backend.glfw.c.glfwGetJoystickButtons(seizer.backend.glfw.c.GLFW_JOYSTICK_1, &buttons_count);
const buttons = buttons_ptr[0..@intCast(buttons_count)];
controller_input_state.action = buttons[0] != 0;
controller_input_state.undo = buttons[1] != 0;
// detect rising for controller input
input_state.left = input_state.left or (!prev_controller_input_state.left and controller_input_state.left);
input_state.right = input_state.right or (!prev_controller_input_state.right and controller_input_state.right);
input_state.up = input_state.up or (!prev_controller_input_state.up and controller_input_state.up);
input_state.down = input_state.down or (!prev_controller_input_state.down and controller_input_state.down);
input_state.action = input_state.action or (!prev_controller_input_state.action and controller_input_state.action);
input_state.undo = input_state.undo or (!prev_controller_input_state.undo and controller_input_state.undo);
prev_controller_input_state = controller_input_state;
}
var request_command: ?[]const u8 = null; var request_command: ?[]const u8 = null;
defer if (request_command) |command| gpa.allocator().free(command); defer if (request_command) |command| gpa.allocator().free(command);
@ -202,7 +297,6 @@ pub fn main() !void {
if (input_state.action) { if (input_state.action) {
request_command = try gpa.allocator().dupe(u8, hovered.command); request_command = try gpa.allocator().dupe(u8, hovered.command);
root_element = null; root_element = null;
hovered_action = null;
} }
} else if (actions.items.len > 0) { } else if (actions.items.len > 0) {
hovered_action = actions.items[0]; hovered_action = actions.items[0];
@ -264,6 +358,9 @@ pub fn main() !void {
switch (response) { switch (response) {
.page => |page_root_element| root_element = page_root_element, .page => |page_root_element| root_element = page_root_element,
.transition => |transition| { .transition => |transition| {
if (transition.reset_selection) {
hovered_action = null;
}
if (!transition.can_undo) { if (!transition.can_undo) {
for (history.items) |*state| { for (history.items) |*state| {
state.deinit(); state.deinit();
@ -811,7 +908,7 @@ pub const CardElement = struct {
.card => |card| render_resources.deck.tilesheet.renderTile( .card => |card| render_resources.deck.tilesheet.renderTile(
canvas, canvas,
render_resources.deck.getTileForCard(card), render_resources.deck.getTileForCard(card),
.{ min[0] + mark_offset[0], min[1] + mark_offset[1] }, .{ @floor(min[0] + mark_offset[0]), @floor(min[1] + mark_offset[1]) },
.{}, .{},
), ),
} }
@ -981,7 +1078,33 @@ fn playerTurnHandler(arena: std.mem.Allocator, request: Request) HandlerError!Re
return Response{ .transition = .{ return Response{ .transition = .{
.game_state = new_game_state, .game_state = new_game_state,
.can_undo = true, .can_undo = true,
.reset_selection = false,
} }; } };
} else if (std.mem.eql(u8, command, "discard")) {
if (request.game_state.marked_cards.count() == 1) {
var new_game_state = try request.game_state.clone(arena);
try new_game_state.discard_pile.ensureUnusedCapacity(arena, 1);
new_game_state.hands[0].clearRetainingCapacity();
new_game_state.marked_cards.clearRetainingCapacity();
for (request.game_state.hands[0].items) |card| {
if (request.game_state.marked_cards.contains(card)) {
new_game_state.discard_pile.appendAssumeCapacity(card);
} else {
new_game_state.hands[0].appendAssumeCapacity(card);
}
}
new_game_state.handler = &endOfTurnConfirmHandler;
return Response{ .transition = .{
.game_state = new_game_state,
.can_undo = true,
.reset_selection = false,
} };
}
} }
} }
@ -1000,6 +1123,9 @@ fn playerTurnHandler(arena: std.mem.Allocator, request: Request) HandlerError!Re
draw_pile.hidden = true; draw_pile.hidden = true;
var discard_pile = try Pile.create(arena, request.game_state.discard_pile.items); var discard_pile = try Pile.create(arena, request.game_state.discard_pile.items);
if (request.game_state.marked_cards.count() == 1) {
discard_pile.command = "discard";
}
var hand = try HBox.create(arena); var hand = try HBox.create(arena);
for (request.game_state.hands[0].items) |card| { for (request.game_state.hands[0].items) |card| {
@ -1023,6 +1149,60 @@ fn playerTurnHandler(arena: std.mem.Allocator, request: Request) HandlerError!Re
return Response{ .page = page.element() }; return Response{ .page = page.element() };
} }
fn endOfTurnConfirmHandler(arena: std.mem.Allocator, request: Request) HandlerError!Response {
if (request.command) |command| {
if (std.mem.eql(u8, command, "end_turn")) {
var new_game_state = try request.game_state.clone(arena);
new_game_state.handler = &drawCardHandler;
return Response{ .transition = .{
.game_state = new_game_state,
.can_undo = false,
} };
}
}
var new_meld_text = try TextElement.create(arena, .{
.text = "New Meld",
.command = null,
});
var melds_hbox = try HBox.create(arena);
try melds_hbox.addElement(new_meld_text.element());
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 = null,
.marked = false,
});
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 end_turn_text = try TextElement.create(arena, .{
.text = "End Turn",
.command = "end_turn",
});
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());
try page.addElement(.{ 0.5, 0 }, .{ 0.5, 0 }, melds_hbox.element());
try page.addElement(.{ 0.5, 0.5 }, .{ 0.5, 0.5 }, end_turn_text.element());
return Response{ .page = page.element() };
}
fn isValidRummyMeld(cards: []const Card) bool { fn isValidRummyMeld(cards: []const Card) bool {
std.debug.assert(std.sort.isSorted(Card, cards, {}, rummyHandSort)); std.debug.assert(std.sort.isSorted(Card, cards, {}, rummyHandSort));
if (cards.len < 3) { if (cards.len < 3) {
@ -1102,6 +1282,10 @@ fn glfw_key_callback(window: ?*seizer.backend.glfw.c.GLFWwindow, key: c_int, sca
} }
} }
test {
_ = @import("./assets.zig");
}
const DeckSprites = assets.DeckSprites; const DeckSprites = assets.DeckSprites;
const Card = assets.Card; const Card = assets.Card;