mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
JS RPC
This commit is contained in:
parent
e9802bf546
commit
58403986d7
@ -114,3 +114,9 @@ body {
|
||||
.trophy {
|
||||
font-size: 5rem;
|
||||
}
|
||||
#victor {
|
||||
margin: 3rem;
|
||||
font-size: 5rem;
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
}
|
||||
|
83
demo/src/app/lib/Game.zig
Normal file
83
demo/src/app/lib/Game.zig
Normal file
@ -0,0 +1,83 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
grid: Grid,
|
||||
victor: ?State = null,
|
||||
|
||||
pub const Grid = [9]State;
|
||||
pub const State = enum { empty, player, cpu, tie };
|
||||
|
||||
const Game = @This();
|
||||
|
||||
pub fn gridFromValues(values: []*jetzig.data.Value) Grid {
|
||||
var grid: [9]Game.State = undefined;
|
||||
for (0..9) |id| {
|
||||
if (values[id].* != .null) {
|
||||
grid[id] = if (values[id].eql("player")) .player else .cpu;
|
||||
} else {
|
||||
grid[id] = .empty;
|
||||
}
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
pub fn movePlayer(game: *Game, cell: usize) bool {
|
||||
if (cell >= game.grid.len) return false;
|
||||
if (game.grid[cell] != .empty) return false;
|
||||
|
||||
game.grid[cell] = .player;
|
||||
game.evaluate();
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn moveCpu(game: *Game) usize {
|
||||
std.debug.assert(game.victor == null);
|
||||
var available: [9]usize = undefined;
|
||||
var available_len: usize = 0;
|
||||
for (game.grid, 0..) |cell, cell_index| {
|
||||
if (cell == .empty) {
|
||||
available[available_len] = cell_index;
|
||||
available_len += 1;
|
||||
}
|
||||
}
|
||||
std.debug.assert(available_len > 0);
|
||||
const choice = available[std.crypto.random.intRangeAtMost(usize, 0, available_len - 1)];
|
||||
game.grid[choice] = .cpu;
|
||||
game.evaluate();
|
||||
return choice;
|
||||
}
|
||||
|
||||
pub fn evaluate(game: *Game) void {
|
||||
var full = true;
|
||||
for (game.grid) |cell| {
|
||||
if (cell == .empty) full = false;
|
||||
}
|
||||
if (full) {
|
||||
game.victor = .tie;
|
||||
return;
|
||||
}
|
||||
|
||||
const patterns = [_][3]usize{
|
||||
.{ 0, 1, 2 },
|
||||
.{ 3, 4, 5 },
|
||||
.{ 6, 7, 8 },
|
||||
.{ 0, 3, 6 },
|
||||
.{ 1, 4, 7 },
|
||||
.{ 2, 5, 8 },
|
||||
.{ 0, 4, 8 },
|
||||
.{ 2, 4, 6 },
|
||||
};
|
||||
for (patterns) |pattern| {
|
||||
var cpu_victor = true;
|
||||
var player_victor = true;
|
||||
|
||||
for (pattern) |cell_index| {
|
||||
if (game.grid[cell_index] != .cpu) cpu_victor = false;
|
||||
if (game.grid[cell_index] != .player) player_victor = false;
|
||||
}
|
||||
|
||||
std.debug.assert(!(cpu_victor and player_victor));
|
||||
if (cpu_victor) game.victor = .cpu;
|
||||
if (player_victor) game.victor = .player;
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
const Game = @import("../lib/Game.zig");
|
||||
|
||||
pub fn index(request: *jetzig.Request) !jetzig.View {
|
||||
return request.render(.ok);
|
||||
}
|
||||
@ -11,56 +13,22 @@ pub const Channel = struct {
|
||||
try channel.sync();
|
||||
}
|
||||
|
||||
pub fn receive(message: jetzig.channels.Message) !void {
|
||||
const params = try message.params() orelse return;
|
||||
|
||||
if (params.remove("reset")) {
|
||||
try resetGame(message.channel);
|
||||
try message.channel.sync();
|
||||
return;
|
||||
}
|
||||
|
||||
const cell: usize = if (params.getT(.integer, "cell")) |integer|
|
||||
@intCast(integer)
|
||||
else
|
||||
return;
|
||||
const cells = message.channel.getT(.array, "cells") orelse return;
|
||||
const items = cells.items();
|
||||
|
||||
var grid: [9]Game.State = undefined;
|
||||
for (0..9) |id| {
|
||||
if (items[id].* != .null) {
|
||||
grid[id] = if (items[id].eql("player")) .player else .cpu;
|
||||
} else {
|
||||
grid[id] = .empty;
|
||||
}
|
||||
}
|
||||
|
||||
var game = Game{ .grid = grid };
|
||||
game.evaluate();
|
||||
|
||||
if (game.winner != null) {
|
||||
try message.channel.publish(.{ .err = "Game is already over." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.movePlayer(cell)) {
|
||||
items[cell] = message.data.string("player");
|
||||
if (game.winner == null) {
|
||||
items[game.moveCpu()] = message.data.string("cpu");
|
||||
}
|
||||
if (game.winner) |winner| {
|
||||
try message.channel.put("winner", @tagName(winner));
|
||||
var results = message.channel.getT(.object, "results") orelse return;
|
||||
const count = results.getT(.integer, @tagName(winner)) orelse return;
|
||||
try results.put(@tagName(winner), count + 1);
|
||||
}
|
||||
}
|
||||
|
||||
try message.channel.sync();
|
||||
}
|
||||
|
||||
pub const Actions = struct {
|
||||
pub fn move(channel: jetzig.channels.Channel, cell: usize) !void {
|
||||
const cells = channel.getT(.array, "cells") orelse return;
|
||||
const grid = Game.gridFromValues(cells.items());
|
||||
var game = Game{ .grid = grid };
|
||||
game.evaluate();
|
||||
|
||||
if (game.victor != null) {
|
||||
try channel.publish(.{ .err = "Game is already over." });
|
||||
return;
|
||||
} else {
|
||||
try movePlayer(channel, &game, cells, cell);
|
||||
try channel.sync();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(channel: jetzig.channels.Channel) !void {
|
||||
try resetGame(channel);
|
||||
try channel.sync();
|
||||
@ -68,7 +36,7 @@ pub const Channel = struct {
|
||||
};
|
||||
|
||||
fn resetGame(channel: jetzig.channels.Channel) !void {
|
||||
try channel.put("winner", null);
|
||||
try channel.put("victor", null);
|
||||
var cells = try channel.put("cells", .array);
|
||||
for (0..9) |_| try cells.append(null);
|
||||
}
|
||||
@ -77,75 +45,31 @@ pub const Channel = struct {
|
||||
var results = try channel.put("results", .object);
|
||||
try results.put("cpu", 0);
|
||||
try results.put("player", 0);
|
||||
try results.put("ties", 0);
|
||||
try results.put("tie", 0);
|
||||
try resetGame(channel);
|
||||
}
|
||||
};
|
||||
|
||||
const Game = struct {
|
||||
grid: [9]State,
|
||||
winner: ?State = null,
|
||||
|
||||
pub const State = enum { empty, player, cpu, tie };
|
||||
|
||||
pub fn movePlayer(game: *Game, cell: usize) bool {
|
||||
if (cell >= game.grid.len) return false;
|
||||
if (game.grid[cell] != .empty) return false;
|
||||
|
||||
game.grid[cell] = .player;
|
||||
game.evaluate();
|
||||
return true;
|
||||
fn movePlayer(
|
||||
channel: jetzig.channels.Channel,
|
||||
game: *Game,
|
||||
cells: *const jetzig.data.Array,
|
||||
cell: usize,
|
||||
) !void {
|
||||
const values = cells.items();
|
||||
if (game.movePlayer(cell)) {
|
||||
values[cell] = channel.data.string("player");
|
||||
if (game.victor == null) {
|
||||
values[game.moveCpu()] = channel.data.string("cpu");
|
||||
}
|
||||
if (game.victor) |victor| try setVictor(channel, victor);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn moveCpu(game: *Game) usize {
|
||||
std.debug.assert(game.winner == null);
|
||||
var available: [9]usize = undefined;
|
||||
var available_len: usize = 0;
|
||||
for (game.grid, 0..) |cell, cell_index| {
|
||||
if (cell == .empty) {
|
||||
available[available_len] = cell_index;
|
||||
available_len += 1;
|
||||
}
|
||||
}
|
||||
std.debug.assert(available_len > 0);
|
||||
const choice = available[std.crypto.random.intRangeAtMost(usize, 0, available_len - 1)];
|
||||
game.grid[choice] = .cpu;
|
||||
game.evaluate();
|
||||
return choice;
|
||||
}
|
||||
|
||||
fn evaluate(game: *Game) void {
|
||||
var full = true;
|
||||
for (game.grid) |cell| {
|
||||
if (cell == .empty) full = false;
|
||||
}
|
||||
if (full) {
|
||||
game.winner = .tie;
|
||||
return;
|
||||
}
|
||||
|
||||
const patterns = [_][3]usize{
|
||||
.{ 0, 1, 2 },
|
||||
.{ 3, 4, 5 },
|
||||
.{ 6, 7, 8 },
|
||||
.{ 0, 3, 6 },
|
||||
.{ 1, 4, 7 },
|
||||
.{ 2, 5, 8 },
|
||||
.{ 0, 4, 8 },
|
||||
.{ 2, 4, 6 },
|
||||
};
|
||||
for (patterns) |pattern| {
|
||||
var cpu_winner = true;
|
||||
var player_winner = true;
|
||||
|
||||
for (pattern) |cell_index| {
|
||||
if (game.grid[cell_index] != .cpu) cpu_winner = false;
|
||||
if (game.grid[cell_index] != .player) player_winner = false;
|
||||
}
|
||||
|
||||
std.debug.assert(!(cpu_winner and player_winner));
|
||||
if (cpu_winner) game.winner = .cpu;
|
||||
if (player_winner) game.winner = .player;
|
||||
}
|
||||
fn setVictor(channel: jetzig.channels.Channel, victor: Game.State) !void {
|
||||
try channel.put("victor", @tagName(victor));
|
||||
var results = channel.getT(.object, "results") orelse return;
|
||||
const count = results.getT(.integer, @tagName(victor)) orelse return;
|
||||
try results.put(@tagName(victor), count + 1);
|
||||
try channel.invoke(.victor, .{ .type = @tagName(victor) });
|
||||
}
|
||||
};
|
||||
|
@ -4,8 +4,16 @@
|
||||
actions: {},
|
||||
stateChangedCallbacks: [],
|
||||
messageCallbacks: [],
|
||||
invokeCallbacks: {},
|
||||
onStateChanged: function(callback) { this.stateChangedCallbacks.push(callback); },
|
||||
onMessage: function(callback) { this.messageCallbacks.push(callback); },
|
||||
receive: function(event, callback) {
|
||||
if (Object.hasOwn(this.invokeCallbacks, event)) {
|
||||
this.invokeCallbacks[event].push(callback);
|
||||
} else {
|
||||
this.invokeCallbacks[event] = [callback];
|
||||
}
|
||||
},
|
||||
publish: function(data) {
|
||||
if (this.websocket) {
|
||||
const json = JSON.stringify(data);
|
||||
@ -22,12 +30,20 @@
|
||||
channel.websocket.addEventListener("message", (event) => {
|
||||
const state_tag = "__jetzig_channel_state__:";
|
||||
const actions_tag = "__jetzig_actions__:";
|
||||
const event_tag = "__jetzig_event__:";
|
||||
|
||||
if (event.data.startsWith(state_tag)) {
|
||||
const state = JSON.parse(event.data.slice(state_tag.length));
|
||||
channel.stateChangedCallbacks.forEach((callback) => {
|
||||
callback(state);
|
||||
});
|
||||
} else if (event.data.startsWith(event_tag)) {
|
||||
const data = JSON.parse(event.data.slice(event_tag.length));
|
||||
if (Object.hasOwn(channel.invokeCallbacks, data.method)) {
|
||||
channel.invokeCallbacks[data.method].forEach(callback => {
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
} else if (event.data.startsWith(actions_tag)) {
|
||||
const data = JSON.parse(event.data.slice(actions_tag.length));
|
||||
data.actions.forEach(action => {
|
||||
@ -95,19 +111,20 @@
|
||||
|
||||
<button id="reset-button">Reset Game</button>
|
||||
|
||||
<div id="victor"></div>
|
||||
|
||||
<script src="/party.js"></script>
|
||||
<link rel="stylesheet" href="/party.css" />
|
||||
|
||||
<script>
|
||||
channel.onStateChanged(state => {
|
||||
console.log(state);
|
||||
document.querySelector("#player-wins").innerText = state.results.player;
|
||||
document.querySelector("#cpu-wins").innerText = state.results.cpu;
|
||||
document.querySelector("#ties").innerText = state.results.ties;
|
||||
document.querySelector("#ties").innerText = state.results.tie;
|
||||
|
||||
if (state.winner) {
|
||||
triggerPartyAnimation();
|
||||
if (!state.victor) {
|
||||
const elem = document.querySelector("#victor");
|
||||
elem.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
Object.entries(state.cells).forEach(([cell, state]) => {
|
||||
@ -122,13 +139,26 @@
|
||||
}
|
||||
});
|
||||
|
||||
channel.receive("victor", (data) => {
|
||||
console.log(data);
|
||||
const elem = document.querySelector("#victor");
|
||||
const emoji = {
|
||||
player: "✈️",
|
||||
cpu: "🦎",
|
||||
tie: "🤝"
|
||||
}[data.params.type] || "";
|
||||
elem.innerHTML = `🏆 ${emoji} 🏆`;
|
||||
elem.style.visibility = 'visible';
|
||||
triggerPartyAnimation();
|
||||
});
|
||||
|
||||
document.querySelector("#reset-button").addEventListener("click", () => {
|
||||
channel.actions.reset();
|
||||
});
|
||||
|
||||
document.querySelectorAll("#board div.cell").forEach(element => {
|
||||
element.addEventListener("click", () => {
|
||||
channel.publish({ cell: parseInt(element.dataset.cell) });
|
||||
channel.actions.move(parseInt(element.dataset.cell));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -23,6 +23,32 @@ pub fn RoutedChannel(Routes: type) type {
|
||||
const writer = write_buffer.writer();
|
||||
try std.json.stringify(data, .{}, writer);
|
||||
try write_buffer.flush();
|
||||
channel.websocket.logger.DEBUG(
|
||||
"Published Channel message for `{s}`",
|
||||
.{channel.websocket.route.path},
|
||||
) catch {};
|
||||
}
|
||||
|
||||
pub fn invoke(
|
||||
channel: Channel,
|
||||
comptime method: @TypeOf(.enum_literal),
|
||||
args: anytype,
|
||||
) !void {
|
||||
// TODO: DRY
|
||||
var stack_fallback = std.heap.stackFallback(4096, channel.allocator);
|
||||
const allocator = stack_fallback.get();
|
||||
|
||||
var write_buffer = channel.websocket.connection.writeBuffer(allocator, .text);
|
||||
defer write_buffer.deinit();
|
||||
|
||||
const writer = write_buffer.writer();
|
||||
try writer.writeAll("__jetzig_event__:");
|
||||
try std.json.stringify(.{ .method = method, .params = args }, .{}, writer);
|
||||
try write_buffer.flush();
|
||||
channel.websocket.logger.DEBUG(
|
||||
"Invoked Javascript function `{s}` for `{s}`",
|
||||
.{ @tagName(method), channel.websocket.route.path },
|
||||
) catch {};
|
||||
}
|
||||
|
||||
pub fn getT(
|
||||
@ -45,6 +71,10 @@ pub fn RoutedChannel(Routes: type) type {
|
||||
return try channel.state.put(key, value);
|
||||
}
|
||||
|
||||
pub fn remove(channel: Channel, key: []const u8) bool {
|
||||
return channel.state.remove(key);
|
||||
}
|
||||
|
||||
pub fn sync(channel: Channel) !void {
|
||||
try channel.websocket.syncState(channel);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user