Skip to content

Commit 88a42b0

Browse files
0e16edba82dec8ead95590c38e6a2639b59a047a
0 parents  commit 88a42b0

18 files changed

Lines changed: 4407 additions & 0 deletions

404.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
window.location.href = '/#' + window.location.href.replace(window.location.origin, '')
3+
</script>
4+
GitHub Pages 404
5+
<br />
6+
<a href="/">Go back to Home</a>

CNAME

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
algorithmic.games

FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: [ChrisAcrobat]

InterfaceHelper.js

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
'use strict'
2+
3+
class InterfaceHelper {
4+
static #initiated = false
5+
static #wired = false
6+
static #onInit = null
7+
static #onWorkerAdded = null
8+
static #workers = new Map()
9+
10+
static preInit() {
11+
if (InterfaceHelper.#initiated) {
12+
console.error('InterfaceHelper is already initiated.')
13+
return
14+
}
15+
InterfaceHelper.#initiated = true
16+
17+
const globalStyle = document.createElement('link')
18+
globalStyle.rel = 'stylesheet'
19+
globalStyle.href = '/global.css'
20+
document.head.prepend(globalStyle)
21+
22+
const fallbackStyle = document.createElement('style')
23+
fallbackStyle.textContent = 'html { background-color: var(--main-background-color); }'
24+
document.head.prepend(fallbackStyle)
25+
}
26+
27+
/** Coordinator shell: popup opener or `/join` parent. */
28+
static #target() {
29+
return globalThis.opener ?? globalThis.parent
30+
}
31+
32+
static #ensureWired() {
33+
if (InterfaceHelper.#wired) {
34+
return
35+
}
36+
InterfaceHelper.#wired = true
37+
globalThis.addEventListener('message', InterfaceHelper.#onWindowMessage)
38+
}
39+
40+
/**
41+
* Called once with cloned match settings (`{ settings, opponents, … }`).
42+
* Register workers with the `workerAdded` callback (slot `0` after this handler returns, then each `{ type: 'WorkerAdded', slot }`).
43+
* @param {(init: unknown, workerAdded: (participant: InterfaceHelperWorker) => void) => void} handler
44+
*/
45+
static onInit(handler) {
46+
InterfaceHelper.#onInit = handler
47+
InterfaceHelper.#ensureWired()
48+
}
49+
50+
/**
51+
* @deprecated Register workers via the `workerAdded` argument to {@link InterfaceHelper.onInit} instead.
52+
* @param {(worker: InterfaceHelperWorker) => void} handler
53+
*/
54+
static workerAdded(handler) {
55+
InterfaceHelper.#onWorkerAdded = handler
56+
InterfaceHelper.#ensureWired()
57+
}
58+
59+
static #registerWorker(handler) {
60+
InterfaceHelper.#onWorkerAdded = handler
61+
}
62+
63+
static #dispatchInit(init) {
64+
InterfaceHelper.#onInit?.(init, InterfaceHelper.#registerWorker)
65+
InterfaceHelper.#attachWorker(0)
66+
}
67+
68+
/** Tell the coordinator the interface applied init and is listening. */
69+
static signalReady() {
70+
const t = InterfaceHelper.#target()
71+
if (t) {
72+
t.postMessage(null, '*')
73+
}
74+
}
75+
76+
/** Coordinator init: `{ settings, opponents }` (see `cloneInterfaceWorkerInit`). */
77+
static #isInit(data) {
78+
if (typeof data !== 'object' || data === null) {
79+
return false
80+
}
81+
const type = data.type
82+
if (
83+
type === 'Post' || type === 'Kill' || type === 'WorkerAdded' || type === 'Response' ||
84+
type === 'Arena-Interface-Ready' || type === 'Arena-Interface-Disconnected'
85+
) {
86+
return false
87+
}
88+
const settings = data.settings
89+
if (settings !== undefined && typeof settings === 'object' && settings !== null) {
90+
return true
91+
}
92+
return data.general !== undefined && typeof data.general === 'object' && data.general !== null
93+
}
94+
95+
static #unwrapPostEnvelope(data) {
96+
if (
97+
typeof data === 'object' &&
98+
data !== null &&
99+
data.type !== 'Post' &&
100+
typeof data.message === 'object' &&
101+
data.message !== null &&
102+
data.message.type === 'Post'
103+
) {
104+
return data.message
105+
}
106+
return data
107+
}
108+
109+
static #isPost(data) {
110+
return typeof data === 'object' && data !== null && data.type === 'Post'
111+
}
112+
113+
static #isDisconnected(data) {
114+
return typeof data === 'object' && data !== null && data.type === 'Arena-Interface-Disconnected'
115+
}
116+
117+
static #showDisconnectedState(label) {
118+
const lock = document.getElementById('lock')
119+
if (!lock) {
120+
return
121+
}
122+
lock.classList.add('engaged')
123+
lock.classList.remove('booting')
124+
const span = lock.querySelector('span')
125+
if (span) {
126+
span.textContent = label
127+
}
128+
}
129+
130+
static #coerceWorkerSlot(value) {
131+
if (typeof value === 'number' && Number.isFinite(value)) {
132+
return value
133+
}
134+
if (typeof value === 'string' && value !== '') {
135+
const n = Number(value)
136+
return Number.isFinite(n) ? n : 0
137+
}
138+
return 0
139+
}
140+
141+
static #workerSlotFromEnvelope(data) {
142+
if (typeof data.workerSlot === 'number') {
143+
return data.workerSlot
144+
}
145+
if (typeof data.workerSlot === 'string') {
146+
return InterfaceHelper.#coerceWorkerSlot(data.workerSlot)
147+
}
148+
if (typeof data.workerName === 'string') {
149+
return data.workerName === '' ? 0 : InterfaceHelper.#coerceWorkerSlot(data.workerName)
150+
}
151+
if (data.type === 'Kill' && typeof data.slot === 'number') {
152+
return InterfaceHelper.#coerceWorkerSlot(data.slot)
153+
}
154+
return 0
155+
}
156+
157+
static #sendResponse(value, options, messageIndex) {
158+
const t = InterfaceHelper.#target()
159+
if (!t) {
160+
return
161+
}
162+
const executionSteps = options?.executionSteps ?? { toRespond: 0, toTerminate: 0 }
163+
const idx = options?.messageIndex ?? messageIndex
164+
t.postMessage({
165+
type: 'Response',
166+
response: {
167+
value,
168+
executionSteps,
169+
...(typeof idx === 'number' ? { messageIndex: idx } : {}),
170+
...(options?.console ? { console: options.console } : {}),
171+
},
172+
}, '*')
173+
}
174+
175+
static #createWorker() {
176+
/** @type {number | null} */
177+
let lastMessageIndex = null
178+
const worker = {
179+
onMessage: null,
180+
onKilled: null,
181+
respond(value, options) {
182+
InterfaceHelper.#sendResponse(value, options, options?.messageIndex ?? lastMessageIndex)
183+
},
184+
}
185+
Object.defineProperty(worker, '_lastMessageIndex', {
186+
get: () => lastMessageIndex,
187+
set: (v) => {
188+
lastMessageIndex = v
189+
},
190+
})
191+
return worker
192+
}
193+
194+
static #attachWorker(workerSlot) {
195+
if (!InterfaceHelper.#onWorkerAdded) {
196+
return
197+
}
198+
let worker = InterfaceHelper.#workers.get(workerSlot)
199+
if (worker) {
200+
return worker
201+
}
202+
worker = InterfaceHelper.#createWorker()
203+
InterfaceHelper.#workers.set(workerSlot, worker)
204+
InterfaceHelper.#onWorkerAdded(worker)
205+
return worker
206+
}
207+
208+
static #onWindowMessage(messageEvent) {
209+
const data = InterfaceHelper.#unwrapPostEnvelope(messageEvent.data)
210+
if (InterfaceHelper.#isDisconnected(data)) {
211+
const label = typeof data.message === 'string' && data.message.trim() ? data.message.trim() : 'Match has ended'
212+
InterfaceHelper.#showDisconnectedState(label)
213+
return
214+
}
215+
if (InterfaceHelper.#isInit(data)) {
216+
InterfaceHelper.#dispatchInit(data)
217+
return
218+
}
219+
if (data.type === 'WorkerAdded' && typeof data.slot === 'number') {
220+
InterfaceHelper.#attachWorker(data.slot)
221+
return
222+
}
223+
if (data.type === 'Kill') {
224+
const workerSlot = InterfaceHelper.#workerSlotFromEnvelope(data)
225+
const worker = InterfaceHelper.#workers.get(workerSlot)
226+
if (!worker) {
227+
InterfaceHelper.#sendResponse('Dead', {}, data.messageIndex)
228+
return
229+
}
230+
let responded = false
231+
const priorRespond = worker.respond
232+
worker.respond = function (value, options) {
233+
responded = true
234+
return priorRespond.call(
235+
this,
236+
value,
237+
{ ...options, messageIndex: options?.messageIndex ?? data.messageIndex },
238+
)
239+
}
240+
worker.onKilled?.()
241+
worker.respond = priorRespond
242+
if (!responded) {
243+
priorRespond.call(worker, 'Dead', { messageIndex: data.messageIndex })
244+
}
245+
return
246+
}
247+
if (InterfaceHelper.#isPost(data)) {
248+
const workerSlot = InterfaceHelper.#workerSlotFromEnvelope(data)
249+
const worker = InterfaceHelper.#workers.get(workerSlot)
250+
if (!worker) {
251+
return
252+
}
253+
worker._lastMessageIndex = typeof data.messageIndex === 'number' ? data.messageIndex : null
254+
const message = {
255+
data: data.message,
256+
respond(value, options) {
257+
InterfaceHelper.#sendResponse(value, options, data.messageIndex)
258+
},
259+
}
260+
worker.onMessage?.(message)
261+
}
262+
}
263+
}
264+
265+
globalThis.InterfaceHelper = InterfaceHelper
266+
InterfaceHelper.preInit()

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Algorithmic Games
2+
3+
Please read [here](https://github.com/AlgorithmicGames).

0 commit comments

Comments
 (0)