mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 14:06:08 +00:00
WIP
This commit is contained in:
parent
58403986d7
commit
fa76b75c12
@ -120,3 +120,16 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: center;
|
vertical-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#grid {
|
||||||
|
transition: background-color 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash-animation {
|
||||||
|
animation: flashRed 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flashRed {
|
||||||
|
0% { background-color: #e55; }
|
||||||
|
100% { background-color: white; }
|
||||||
|
}
|
||||||
|
@ -21,7 +21,7 @@ pub const Channel = struct {
|
|||||||
game.evaluate();
|
game.evaluate();
|
||||||
|
|
||||||
if (game.victor != null) {
|
if (game.victor != null) {
|
||||||
try channel.publish(.{ .err = "Game is already over." });
|
try channel.invoke(.game_over, .{});
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
try movePlayer(channel, &game, cells, cell);
|
try movePlayer(channel, &game, cells, cell);
|
||||||
|
@ -5,13 +5,23 @@
|
|||||||
stateChangedCallbacks: [],
|
stateChangedCallbacks: [],
|
||||||
messageCallbacks: [],
|
messageCallbacks: [],
|
||||||
invokeCallbacks: {},
|
invokeCallbacks: {},
|
||||||
|
elementMap: {},
|
||||||
|
transformerMap: {},
|
||||||
|
transformers: {},
|
||||||
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) {
|
transform: function(ref, callback) {
|
||||||
if (Object.hasOwn(this.invokeCallbacks, event)) {
|
if (Object.hasOwn(this.transformers, ref)) {
|
||||||
this.invokeCallbacks[event].push(callback);
|
this.transformers[ref].push(callback);
|
||||||
} else {
|
} else {
|
||||||
this.invokeCallbacks[event] = [callback];
|
this.transformers[ref] = [callback];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
receive: function(ref, callback) {
|
||||||
|
if (Object.hasOwn(this.invokeCallbacks, ref)) {
|
||||||
|
this.invokeCallbacks[ref].push(callback);
|
||||||
|
} else {
|
||||||
|
this.invokeCallbacks[ref] = [callback];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
publish: function(data) {
|
publish: function(data) {
|
||||||
@ -21,76 +31,122 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
|
||||||
|
(() => {
|
||||||
|
|
||||||
@if (context.request) |request|
|
@if (context.request) |request|
|
||||||
@if (request.headers.get("host")) |host|
|
const transform = (value, state, element) => {
|
||||||
<script>
|
const id = element.getAttribute('jetzig-id');
|
||||||
channel.websocket = new WebSocket('ws://{{host}}{{request.path.base_path}}');
|
const key = id && channel.transformerMap[id];
|
||||||
channel.websocket.addEventListener("message", (event) => {
|
const transformers = key && channel.transformers[key];
|
||||||
const state_tag = "__jetzig_channel_state__:";
|
if (transformers) {
|
||||||
const actions_tag = "__jetzig_actions__:";
|
return transformers.reduce((acc, val) => val(acc), value);
|
||||||
const event_tag = "__jetzig_event__:";
|
} else {
|
||||||
|
return value === undefined || value == null ? '' : `${value}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (event.data.startsWith(state_tag)) {
|
@if (request.headers.get("host")) |host|
|
||||||
const state = JSON.parse(event.data.slice(state_tag.length));
|
channel.websocket = new WebSocket('ws://{{host}}{{request.path.base_path}}');
|
||||||
channel.stateChangedCallbacks.forEach((callback) => {
|
channel.websocket.addEventListener("message", (event) => {
|
||||||
callback(state);
|
const state_tag = "__jetzig_channel_state__:";
|
||||||
});
|
const actions_tag = "__jetzig_actions__:";
|
||||||
} else if (event.data.startsWith(event_tag)) {
|
const event_tag = "__jetzig_event__:";
|
||||||
const data = JSON.parse(event.data.slice(event_tag.length));
|
|
||||||
if (Object.hasOwn(channel.invokeCallbacks, data.method)) {
|
if (event.data.startsWith(state_tag)) {
|
||||||
channel.invokeCallbacks[data.method].forEach(callback => {
|
const state = JSON.parse(event.data.slice(state_tag.length));
|
||||||
|
Object.entries(channel.elementMap).forEach(([ref, elements]) => {
|
||||||
|
const value = reduceState(ref, state);
|
||||||
|
elements.forEach(element => element.innerHTML = transform(value, state, element));
|
||||||
|
});
|
||||||
|
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 => {
|
||||||
|
channel.actions[action.name] = (...params) => {
|
||||||
|
if (action.params.length != params.length) {
|
||||||
|
throw new Error(`Invalid params for action '${action.name}'`);
|
||||||
|
}
|
||||||
|
[...action.params].forEach((param, index) => {
|
||||||
|
const map = {
|
||||||
|
s: "string",
|
||||||
|
b: "boolean",
|
||||||
|
i: "number",
|
||||||
|
f: "number",
|
||||||
|
};
|
||||||
|
if (map[param] !== typeof params[index]) {
|
||||||
|
throw new Error(`Incorrect argument type for argument ${index} in '${action.name}'. Expected: ${map[param]}, found ${typeof params[index]}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
channel.websocket.send(`_invoke:${action.name}:${JSON.stringify(params)}`);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
channel.messageCallbacks.forEach((callback) => {
|
||||||
callback(data);
|
callback(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (event.data.startsWith(actions_tag)) {
|
|
||||||
const data = JSON.parse(event.data.slice(actions_tag.length));
|
|
||||||
data.actions.forEach(action => {
|
|
||||||
channel.actions[action.name] = (...params) => {
|
|
||||||
if (action.params.length != params.length) {
|
|
||||||
throw new Error(`Invalid params for action '${action.name}'`);
|
|
||||||
}
|
|
||||||
[...action.params].forEach((param, index) => {
|
|
||||||
const map = {
|
|
||||||
s: "string",
|
|
||||||
b: "boolean",
|
|
||||||
i: "number",
|
|
||||||
f: "number",
|
|
||||||
};
|
|
||||||
if (map[param] !== typeof params[index]) {
|
|
||||||
throw new Error(`Incorrect argument type for argument ${index} in '${action.name}'. Expected: ${map[param]}, found ${typeof params[index]}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
channel.websocket.send(`_invoke:${action.name}:${JSON.stringify(params)}`);
|
});
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
channel.messageCallbacks.forEach((callback) => {
|
|
||||||
callback(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
const reduceState = (ref, state) => {
|
||||||
@// channel.websocket.addEventListener("open", (event) => {
|
if (!ref.startsWith('$.')) throw new Error(`Unexpected ref format: ${ref}`);
|
||||||
@// // TODO
|
const args = ref.split('.');
|
||||||
@// channel.publish("websockets", {});
|
args.shift();
|
||||||
@// });
|
const isNumeric = (string) => [...string].every(char => '0123456789'.includes(char));
|
||||||
</script>
|
const isObject = (object) => object && typeof object === 'object';
|
||||||
|
return args.reduce((acc, arg) => {
|
||||||
|
if (isNumeric(arg)) {
|
||||||
|
if (acc && Array.isArray(acc) && acc.length > arg) return acc[parseInt(arg)];
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
if (acc && isObject(acc)) return acc[arg];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, state);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelectorAll('[jetzig-connect]').forEach(element => {
|
||||||
|
const ref = element.getAttribute('jetzig-connect');
|
||||||
|
if (!channel.elementMap[ref]) channel.elementMap[ref] = [];
|
||||||
|
const id = `jetzig-${crypto.randomUUID()}`;
|
||||||
|
element.setAttribute('jetzig-id', id);
|
||||||
|
channel.elementMap[ref].push(element);
|
||||||
|
channel.transformerMap[id] = element.getAttribute('jetzig-transform');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
@// channel.websocket.addEventListener("open", (event) => {
|
||||||
|
@// // TODO
|
||||||
|
@// channel.publish("websockets", {});
|
||||||
|
@// });
|
||||||
@end
|
@end
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<div id="results-wrapper">
|
<div id="results-wrapper">
|
||||||
<span class="trophy">🏆</span>
|
<span class="trophy">🏆</span>
|
||||||
<div id="results">
|
<div id="results">
|
||||||
<div>Player</div>
|
<div>Player</div>
|
||||||
<div id="player-wins"></div>
|
<div id="player-wins" jetzig-connect="$.results.player"></div>
|
||||||
<div>CPU</div>
|
<div>CPU</div>
|
||||||
<div id="cpu-wins"></div>
|
<div id="cpu-wins" jetzig-connect="$.results.cpu"></div>
|
||||||
<div>Tie</div>
|
<div>Tie</div>
|
||||||
<div id="ties"></div>
|
<div id="ties" jetzig-connect="$.results.tie"></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="trophy">🏆</span>
|
<span class="trophy">🏆</span>
|
||||||
</div>
|
</div>
|
||||||
@ -98,15 +154,9 @@
|
|||||||
<div id="party-container"></div>
|
<div id="party-container"></div>
|
||||||
|
|
||||||
<div class="board" id="board">
|
<div class="board" id="board">
|
||||||
<div class="cell" jetzig-connect="$.cells.0" id="tic-tac-toe-cell-0" data-cell="0"></div>
|
@for (0..9) |index| {
|
||||||
<div class="cell" jetzig-connect="$.cells.1" id="tic-tac-toe-cell-1" data-cell="1"></div>
|
<div class="cell" jetzig-connect="$.cells.{{index}}" jetzig-transform="cell" id="tic-tac-toe-cell-{{index}}" data-cell="{{index}}"></div>
|
||||||
<div class="cell" jetzig-connect="$.cells.2" id="tic-tac-toe-cell-2" data-cell="2"></div>
|
}
|
||||||
<div class="cell" jetzig-connect="$.cells.3" id="tic-tac-toe-cell-3" data-cell="3"></div>
|
|
||||||
<div class="cell" jetzig-connect="$.cells.4" id="tic-tac-toe-cell-4" data-cell="4"></div>
|
|
||||||
<div class="cell" jetzig-connect="$.cells.5" id="tic-tac-toe-cell-5" data-cell="5"></div>
|
|
||||||
<div class="cell" jetzig-connect="$.cells.6" id="tic-tac-toe-cell-6" data-cell="6"></div>
|
|
||||||
<div class="cell" jetzig-connect="$.cells.7" id="tic-tac-toe-cell-7" data-cell="7"></div>
|
|
||||||
<div class="cell" jetzig-connect="$.cells.8" id="tic-tac-toe-cell-8" data-cell="8"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="reset-button">Reset Game</button>
|
<button id="reset-button">Reset Game</button>
|
||||||
@ -118,19 +168,10 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
channel.onStateChanged(state => {
|
channel.onStateChanged(state => {
|
||||||
document.querySelector("#player-wins").innerText = state.results.player;
|
|
||||||
document.querySelector("#cpu-wins").innerText = state.results.cpu;
|
|
||||||
document.querySelector("#ties").innerText = state.results.tie;
|
|
||||||
|
|
||||||
if (!state.victor) {
|
if (!state.victor) {
|
||||||
const elem = document.querySelector("#victor");
|
const element = document.querySelector("#victor");
|
||||||
elem.style.visibility = 'hidden';
|
element.style.visibility = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(state.cells).forEach(([cell, state]) => {
|
|
||||||
const element = document.querySelector(`#tic-tac-toe-cell-${cell}`);
|
|
||||||
element.innerHTML = { player: "✈️", cpu: "🦎" }[state] || "";
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
channel.onMessage(message => {
|
channel.onMessage(message => {
|
||||||
@ -140,25 +181,35 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
channel.receive("victor", (data) => {
|
channel.receive("victor", (data) => {
|
||||||
console.log(data);
|
const element = document.querySelector("#victor");
|
||||||
const elem = document.querySelector("#victor");
|
|
||||||
const emoji = {
|
const emoji = {
|
||||||
player: "✈️",
|
player: "✈️",
|
||||||
cpu: "🦎",
|
cpu: "🦎",
|
||||||
tie: "🤝"
|
tie: "🤝"
|
||||||
}[data.params.type] || "";
|
}[data.params.type] || "";
|
||||||
elem.innerHTML = `🏆 ${emoji} 🏆`;
|
element.innerHTML = `🏆 ${emoji} 🏆`;
|
||||||
elem.style.visibility = 'visible';
|
element.style.visibility = 'visible';
|
||||||
triggerPartyAnimation();
|
triggerPartyAnimation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
channel.receive("game_over", (data) => {
|
||||||
|
const element = document.querySelector("#board");
|
||||||
|
element.classList.remove('flash-animation');
|
||||||
|
void element.offsetWidth;
|
||||||
|
element.classList.add('flash-animation');
|
||||||
|
});
|
||||||
|
|
||||||
|
channel.transform("cell", (value) => (
|
||||||
|
{ player: "✈️", cpu: "🦎" }[value] || ""
|
||||||
|
));
|
||||||
|
document.querySelectorAll("#board div.cell").forEach(element => {
|
||||||
|
element.addEventListener("click", () => {
|
||||||
|
channel.actions.move(parseInt(element.dataset.cell));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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 => {
|
|
||||||
element.addEventListener("click", () => {
|
|
||||||
channel.actions.move(parseInt(element.dataset.cell));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user