mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
JS RPC
This commit is contained in:
parent
e9802bf546
commit
58403986d7
@ -114,3 +114,9 @@ body {
|
|||||||
.trophy {
|
.trophy {
|
||||||
font-size: 5rem;
|
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 std = @import("std");
|
||||||
const jetzig = @import("jetzig");
|
const jetzig = @import("jetzig");
|
||||||
|
|
||||||
|
const Game = @import("../lib/Game.zig");
|
||||||
|
|
||||||
pub fn index(request: *jetzig.Request) !jetzig.View {
|
pub fn index(request: *jetzig.Request) !jetzig.View {
|
||||||
return request.render(.ok);
|
return request.render(.ok);
|
||||||
}
|
}
|
||||||
@ -11,56 +13,22 @@ pub const Channel = struct {
|
|||||||
try channel.sync();
|
try channel.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn receive(message: jetzig.channels.Message) !void {
|
pub const Actions = struct {
|
||||||
const params = try message.params() orelse return;
|
pub fn move(channel: jetzig.channels.Channel, cell: usize) !void {
|
||||||
|
const cells = channel.getT(.array, "cells") orelse return;
|
||||||
if (params.remove("reset")) {
|
const grid = Game.gridFromValues(cells.items());
|
||||||
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 };
|
var game = Game{ .grid = grid };
|
||||||
game.evaluate();
|
game.evaluate();
|
||||||
|
|
||||||
if (game.winner != null) {
|
if (game.victor != null) {
|
||||||
try message.channel.publish(.{ .err = "Game is already over." });
|
try channel.publish(.{ .err = "Game is already over." });
|
||||||
return;
|
return;
|
||||||
}
|
} else {
|
||||||
|
try movePlayer(channel, &game, cells, cell);
|
||||||
if (game.movePlayer(cell)) {
|
try channel.sync();
|
||||||
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 reset(channel: jetzig.channels.Channel) !void {
|
pub fn reset(channel: jetzig.channels.Channel) !void {
|
||||||
try resetGame(channel);
|
try resetGame(channel);
|
||||||
try channel.sync();
|
try channel.sync();
|
||||||
@ -68,7 +36,7 @@ pub const Channel = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fn resetGame(channel: jetzig.channels.Channel) !void {
|
fn resetGame(channel: jetzig.channels.Channel) !void {
|
||||||
try channel.put("winner", null);
|
try channel.put("victor", null);
|
||||||
var cells = try channel.put("cells", .array);
|
var cells = try channel.put("cells", .array);
|
||||||
for (0..9) |_| try cells.append(null);
|
for (0..9) |_| try cells.append(null);
|
||||||
}
|
}
|
||||||
@ -77,75 +45,31 @@ pub const Channel = struct {
|
|||||||
var results = try channel.put("results", .object);
|
var results = try channel.put("results", .object);
|
||||||
try results.put("cpu", 0);
|
try results.put("cpu", 0);
|
||||||
try results.put("player", 0);
|
try results.put("player", 0);
|
||||||
try results.put("ties", 0);
|
try results.put("tie", 0);
|
||||||
try resetGame(channel);
|
try resetGame(channel);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const Game = struct {
|
fn movePlayer(
|
||||||
grid: [9]State,
|
channel: jetzig.channels.Channel,
|
||||||
winner: ?State = null,
|
game: *Game,
|
||||||
|
cells: *const jetzig.data.Array,
|
||||||
pub const State = enum { empty, player, cpu, tie };
|
cell: usize,
|
||||||
|
) !void {
|
||||||
pub fn movePlayer(game: *Game, cell: usize) bool {
|
const values = cells.items();
|
||||||
if (cell >= game.grid.len) return false;
|
if (game.movePlayer(cell)) {
|
||||||
if (game.grid[cell] != .empty) return false;
|
values[cell] = channel.data.string("player");
|
||||||
|
if (game.victor == null) {
|
||||||
game.grid[cell] = .player;
|
values[game.moveCpu()] = channel.data.string("cpu");
|
||||||
game.evaluate();
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
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 {
|
fn setVictor(channel: jetzig.channels.Channel, victor: Game.State) !void {
|
||||||
var full = true;
|
try channel.put("victor", @tagName(victor));
|
||||||
for (game.grid) |cell| {
|
var results = channel.getT(.object, "results") orelse return;
|
||||||
if (cell == .empty) full = false;
|
const count = results.getT(.integer, @tagName(victor)) orelse return;
|
||||||
}
|
try results.put(@tagName(victor), count + 1);
|
||||||
if (full) {
|
try channel.invoke(.victor, .{ .type = @tagName(victor) });
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -4,8 +4,16 @@
|
|||||||
actions: {},
|
actions: {},
|
||||||
stateChangedCallbacks: [],
|
stateChangedCallbacks: [],
|
||||||
messageCallbacks: [],
|
messageCallbacks: [],
|
||||||
|
invokeCallbacks: {},
|
||||||
onStateChanged: function(callback) { this.stateChangedCallbacks.push(callback); },
|
onStateChanged: function(callback) { this.stateChangedCallbacks.push(callback); },
|
||||||
onMessage: function(callback) { this.messageCallbacks.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) {
|
publish: function(data) {
|
||||||
if (this.websocket) {
|
if (this.websocket) {
|
||||||
const json = JSON.stringify(data);
|
const json = JSON.stringify(data);
|
||||||
@ -22,12 +30,20 @@
|
|||||||
channel.websocket.addEventListener("message", (event) => {
|
channel.websocket.addEventListener("message", (event) => {
|
||||||
const state_tag = "__jetzig_channel_state__:";
|
const state_tag = "__jetzig_channel_state__:";
|
||||||
const actions_tag = "__jetzig_actions__:";
|
const actions_tag = "__jetzig_actions__:";
|
||||||
|
const event_tag = "__jetzig_event__:";
|
||||||
|
|
||||||
if (event.data.startsWith(state_tag)) {
|
if (event.data.startsWith(state_tag)) {
|
||||||
const state = JSON.parse(event.data.slice(state_tag.length));
|
const state = JSON.parse(event.data.slice(state_tag.length));
|
||||||
channel.stateChangedCallbacks.forEach((callback) => {
|
channel.stateChangedCallbacks.forEach((callback) => {
|
||||||
callback(state);
|
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)) {
|
} else if (event.data.startsWith(actions_tag)) {
|
||||||
const data = JSON.parse(event.data.slice(actions_tag.length));
|
const data = JSON.parse(event.data.slice(actions_tag.length));
|
||||||
data.actions.forEach(action => {
|
data.actions.forEach(action => {
|
||||||
@ -95,19 +111,20 @@
|
|||||||
|
|
||||||
<button id="reset-button">Reset Game</button>
|
<button id="reset-button">Reset Game</button>
|
||||||
|
|
||||||
|
<div id="victor"></div>
|
||||||
|
|
||||||
<script src="/party.js"></script>
|
<script src="/party.js"></script>
|
||||||
<link rel="stylesheet" href="/party.css" />
|
<link rel="stylesheet" href="/party.css" />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
channel.onStateChanged(state => {
|
channel.onStateChanged(state => {
|
||||||
console.log(state);
|
|
||||||
document.querySelector("#player-wins").innerText = state.results.player;
|
document.querySelector("#player-wins").innerText = state.results.player;
|
||||||
document.querySelector("#cpu-wins").innerText = state.results.cpu;
|
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) {
|
if (!state.victor) {
|
||||||
triggerPartyAnimation();
|
const elem = document.querySelector("#victor");
|
||||||
|
elem.style.visibility = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(state.cells).forEach(([cell, state]) => {
|
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", () => {
|
document.querySelector("#reset-button").addEventListener("click", () => {
|
||||||
channel.actions.reset();
|
channel.actions.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll("#board div.cell").forEach(element => {
|
document.querySelectorAll("#board div.cell").forEach(element => {
|
||||||
element.addEventListener("click", () => {
|
element.addEventListener("click", () => {
|
||||||
channel.publish({ cell: parseInt(element.dataset.cell) });
|
channel.actions.move(parseInt(element.dataset.cell));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -23,6 +23,32 @@ pub fn RoutedChannel(Routes: type) type {
|
|||||||
const writer = write_buffer.writer();
|
const writer = write_buffer.writer();
|
||||||
try std.json.stringify(data, .{}, writer);
|
try std.json.stringify(data, .{}, writer);
|
||||||
try write_buffer.flush();
|
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(
|
pub fn getT(
|
||||||
@ -45,6 +71,10 @@ pub fn RoutedChannel(Routes: type) type {
|
|||||||
return try channel.state.put(key, value);
|
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 {
|
pub fn sync(channel: Channel) !void {
|
||||||
try channel.websocket.syncState(channel);
|
try channel.websocket.syncState(channel);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user