This commit is contained in:
Bob Farrell 2025-04-25 21:41:24 +01:00
parent 58403986d7
commit fa76b75c12
3 changed files with 151 additions and 87 deletions

View File

@ -120,3 +120,16 @@ body {
text-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; }
}

View File

@ -21,7 +21,7 @@ pub const Channel = struct {
game.evaluate();
if (game.victor != null) {
try channel.publish(.{ .err = "Game is already over." });
try channel.invoke(.game_over, .{});
return;
} else {
try movePlayer(channel, &game, cells, cell);

View File

@ -5,13 +5,23 @@
stateChangedCallbacks: [],
messageCallbacks: [],
invokeCallbacks: {},
elementMap: {},
transformerMap: {},
transformers: {},
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);
transform: function(ref, callback) {
if (Object.hasOwn(this.transformers, ref)) {
this.transformers[ref].push(callback);
} 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) {
@ -21,11 +31,22 @@
}
},
};
</script>
(() => {
@if (context.request) |request|
const transform = (value, state, element) => {
const id = element.getAttribute('jetzig-id');
const key = id && channel.transformerMap[id];
const transformers = key && channel.transformers[key];
if (transformers) {
return transformers.reduce((acc, val) => val(acc), value);
} else {
return value === undefined || value == null ? '' : `${value}`
}
};
@if (request.headers.get("host")) |host|
<script>
channel.websocket = new WebSocket('ws://{{host}}{{request.path.base_path}}');
channel.websocket.addEventListener("message", (event) => {
const state_tag = "__jetzig_channel_state__:";
@ -34,6 +55,10 @@
if (event.data.startsWith(state_tag)) {
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);
});
@ -74,23 +99,54 @@
}
});
const reduceState = (ref, state) => {
if (!ref.startsWith('$.')) throw new Error(`Unexpected ref format: ${ref}`);
const args = ref.split('.');
args.shift();
const isNumeric = (string) => [...string].every(char => '0123456789'.includes(char));
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
})();
</script>
@end
@end
<div id="results-wrapper">
<span class="trophy">&#127942;</span>
<div id="results">
<div>Player</div>
<div id="player-wins"></div>
<div id="player-wins" jetzig-connect="$.results.player"></div>
<div>CPU</div>
<div id="cpu-wins"></div>
<div id="cpu-wins" jetzig-connect="$.results.cpu"></div>
<div>Tie</div>
<div id="ties"></div>
<div id="ties" jetzig-connect="$.results.tie"></div>
</div>
<span class="trophy">&#127942;</span>
</div>
@ -98,15 +154,9 @@
<div id="party-container"></div>
<div class="board" id="board">
<div class="cell" jetzig-connect="$.cells.0" id="tic-tac-toe-cell-0" data-cell="0"></div>
<div class="cell" jetzig-connect="$.cells.1" id="tic-tac-toe-cell-1" data-cell="1"></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>
@for (0..9) |index| {
<div class="cell" jetzig-connect="$.cells.{{index}}" jetzig-transform="cell" id="tic-tac-toe-cell-{{index}}" data-cell="{{index}}"></div>
}
</div>
<button id="reset-button">Reset Game</button>
@ -118,19 +168,10 @@
<script>
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) {
const elem = document.querySelector("#victor");
elem.style.visibility = 'hidden';
const element = document.querySelector("#victor");
element.style.visibility = 'hidden';
}
Object.entries(state.cells).forEach(([cell, state]) => {
const element = document.querySelector(`#tic-tac-toe-cell-${cell}`);
element.innerHTML = { player: "&#9992;&#65039;", cpu: "&#129422;" }[state] || "";
});
});
channel.onMessage(message => {
@ -140,25 +181,35 @@
});
channel.receive("victor", (data) => {
console.log(data);
const elem = document.querySelector("#victor");
const element = document.querySelector("#victor");
const emoji = {
player: "&#9992;&#65039;",
cpu: "&#129422;",
tie: "&#129309;"
}[data.params.type] || "";
elem.innerHTML = `&#127942; ${emoji} &#127942;`;
elem.style.visibility = 'visible';
element.innerHTML = `&#127942; ${emoji} &#127942;`;
element.style.visibility = 'visible';
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: "&#9992;&#65039;", cpu: "&#129422;" }[value] || ""
));
document.querySelectorAll("#board div.cell").forEach(element => {
element.addEventListener("click", () => {
channel.actions.move(parseInt(element.dataset.cell));
});
});
document.querySelector("#reset-button").addEventListener("click", () => {
channel.actions.reset();
});
document.querySelectorAll("#board div.cell").forEach(element => {
element.addEventListener("click", () => {
channel.actions.move(parseInt(element.dataset.cell));
});
});
</script>