Files
sml-projects/svelte_gap.txt

3173 lines
92 KiB
Plaintext

<script lang="ts">
import type { HtmlTag } from 'svelte/internal';
import type { GameConfig } from './shared';
import * as topicIcons from './topicicons.json'
import * as topicSpecialIcons f./topicspecialrom './specialtopiciconsIconss.json'
export let room: string
export let connection: 'waiting' | 'connecting' | 'connected'
export let game_config: GameConfig
// export let state // loading, waiting, playing, paused.
// export let start_time
// export let topic
// export let meditate
// export let players
// export let rounds
// export let seconds_per_bead
// export let paused_progress
export let _active_sessions: number
export let _magister: true | null
export let _clock_offset: number
// let game_completed = false // Derived from other properties
let round_audio: HTMLAudioElement
let complete_audio: Hlet topic_imgelem: HTMLElement
round_audio = new Audio()
round_audio.src = "/lo-metal-tone.mp3"
complete_audio <audio bin = neomplete_audiow Audio"/hi-metal-tone.mp3"()
cround_audio.src r ound_audiod:this={ro
// round_audio.preload = 'auto'pr// elround_audio
und_audio} src="/lo-metal-tone.mp3" preload="auto" autoplay><track kind="captions"></audio>
<audio bind:this={complete_audio} src="/hi-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
TMLAudtextioEleleement
let topic_img: HTMLElement
let state: GameConfig['state']
$: state = game_config.state
$: console.log('Game configuration changed', game_config)
// export let state
const ARCHETOPICS = [
'Truth', 'Human', 'Energy', 'Beauty',
documentwindow.onclick = (
// ) if (!audio_works) )test_audio()=> {
console.log('onclick')"e. }
'Beginning', 'End', 'Birth', 'Death',
'Ego', 'At
// This ugly monstrosity brought to you by iOS Safari.
// TtThis seems to be the only way to bless the aud
// io objects to be able to play during the game. :/
tite// ntion', 'Art', 'Empat// hy', 'Eutopia'
const round_src = round_audio.src
const completeroundcroompleteundocld_src round_audio.src = complete_audio.src =complet '/silence.mp3'
= round_au
complete_audio.play()complete_audiodio.srcaround_audio], round_audioroun// 'Future', 'Game', 'Gift'round_src
complete_audio.src = complete_srccomplete_scomplete_audio// ,
'History',
round_audio round_audio round_audio
round_audio.src = old_srcold_rondound_src
.src = complete_srccomplete_scomplete_a
document.onclick = () => {
if (!audio_works) test_audio()
}udiocomplete_audio_audior.volume = 0.01muted = true
round_audio.play().then(
round_audio.src = old_src () => {
round_audio
round_audio.play().then(.muted = true
() => {false
audio_works = true
console.log('Audio works'
audio_works = true)
},
() => {
console.log('Audio works') ro
},und_audimuted
() => {false = trueo.volume = 0.01round_audio
round_audioa.play().then( 'Cosmos', 'Time', 'Lif
+let audio_works = true
+
+function test_audio() {
+ let a = new Audio()
+ a.volume = // 0.1
+ a.src = '/silence.mp3'
+ a.play().then(
+ () => {
+ audio_works = true
+ console.log('Audio works')
+ },
+ () => {
+ audio_works = false
+ console.log('Audio does not wo// rk')
+ }
+ )
+}
+functio// n fix_afixxxudio(e) {
+
+//// console.log(// 'xx')
+ testconst fixed_rand = Math.random()randomrand_audio()
+}
+setTimeout(test_audifixed_rando, 0)
+
e', '
const randInt
const n = randInt = (n: number) => Math.floor(floorMath.random())function; * nrand<T>omMat<T>h: {return arr
cTamonst randItem = < }randInt(arr.length)]
T>(arr: T[]) => t[t.: (Addiction', 'Paradox', 'Shadow', 'Soimg && topistarting...willc_text mciety'
]
ele$: {
const topic = game_config.topic.to const svgContent = topicIcons[topic as keyof typeof topicIcons]
LocaleLowerCase()if (topic_img) {
const svgContetopicnt = topicIcons
[game_config.topi{
topic_img.innerHTML = svgContent
topic_text.innerText = ''
} else if (textCrandItem(ontent) {
) topic_img.innerHTML = ''
topic_text.innerText = textContent
} else {
c.timgoLo
caleLowerCase''()ele as k
topic_text.innerText = '
} else {
topic_text.innerText = game_config.topic
} } '
} else if (textContent) {
topic_text.innerText = textContent } eyof ty pieof top
const svgContent = topicIcons[topic as
else topic_text.innerTextinner = {game_config.topic} keyof typeof topicIcons]icIcons]
if (svgContent) topic_img.innerH
else
if (svgContent) topic_img.innerHTML = svgContent{topic as ke
else {yof typeof topicSpecialt
text if (textContent) topic_eleimg.innerText = textContenttextConteinnertopic_imtextConopicSptopicIcons]
const textContent = topicSpecial[topicSp }
TML = svgContent
}
}
// Could make configurable. Eh.
const MEDITATION_SECONDS = 60
interface GameStage {
label: string,
type: 'waiting' | 'bead' | 'breath' | 'meditate' | 'contemplation' | 'complete',
duration: number,
no_sound?: true,
r?: number, p?: number,
id?: string
}
let game_stages: GameStage[] = []
$: {
game_stages = [{
label: `${game_config.meditate ? 'Meditation' : 'Game'} is about to start`,
type: 'waiting',
duration: 3,
no_sound: true
}]
if (game_config.meditate) game_stages.push({
label: 'Meditate',
type: 'meditate',
duration: MEDITATION_SECONDS,
})
for (let r = 0; r < game_config.rounds; r++) {
for (let p = 0; p < game_config.players; p++) {
if (game_config.seconds_between_bead && (r > 0 || p > 0)) game_stages.push({
label: 'Breathe',
duration: game_config.seconds_between_bead,
type: 'breath',
id: `b ${r} ${p}`
})
game_stages.push({
label: 'x ',
// label: game_config.players > 1 ? `Round ${r+1} player ${p+1}` : `Round ${r+1}`,
label: game_config.players > 1 ? `Round ${r+1} player ${p+1}` : `Round ${r+1}`,
duration: game_config.seconds_per_bead,
type: 'bead', r, p,
id: `s ${r} ${p}`
})
}
}
if (game_config.contemplation) game_stages.push({
label: "Contemplate the game's passing",
type: 'contemplation',
duration: MEDITATION_SECONDS,
})
console.log('game stages', game_stages, game_config.seconds_between_bead)
}
let total_game_length: number
$: total_game_length = game_stages.reduce((x, s) => x + s.duration, 0)
// Used for the overall game progress indicator.
let inner_game_stages: GameStage[]
$: inner_game_stages = game_stages.filter(s => s.type === 'breath' || s.type === 'bead')
let inner_game_length: number
$: inner_game_length = inner_game_stages.reduce((x, s) => x + s.duration, 0)
// TODO: The protocol for these update methods doesn't use game_state properly.
const update_state = async (patch: Record<string, string | number | boolean | null>) => {
await fetch(`${room}/configure`, {
method: 'POST',
mode: 'same-origin',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(patch)
})
}
const upd = (k: string, v: string | number | boolean | null) => () => update_state({[k]: v})
const config = (k: string): svelte.JSX.FormEventHandler<HTMLInputElement> => (e) => {
// console.log('k', k, e.data, e.value, e.target.value, e.target.type)
const target = e.target as HTMLInputElement
const raw_value = target.value
const value = target.type === 'number' ? ~~raw_value
: target.type === 'checkbox' ? target.checked
: raw_value
update_state({[k]: value})
}
const roundish = (x: number) => Math.round(x * 10) / 10
const waiting_stage: GameStage = { label: 'Waiting for game to start', type: 'waiting', duration: Infinity }
const complete_stage: GameStage = { label: 'Game complete', type: 'complete', duration: Infinity }
const get_current_stage = (offset_ms: number): {stage: GameStage, stage_idx: number, offset_sec: number} => {
if (state === 'waiting') return {stage: waiting_stage, stage_idx: -1, offset_sec: 0}
let offset_sec = Math.round(offset_ms / 1000)
for (let s = 0; s < game_stages.length; s++) {
let stage = game_stages[s]
if (stage.duration > offset_sec) {
return {stage, stage_idx: s, offset_sec}
}
offset_sec -= stage.duration
}
return {
stage: complete_stage, stage_idx: game_stages.length, offset_sec
}
}
// Urgh kinda ugly storing state for both the index and stage itself. Better to
// have one derive the other.
let current_stage: GameStage | null = null, current_stage_idx: number = -1, offset_sec: number
$: console.log('current stage', current_stage)
// $: console.log('idx', current_stage_idx)
const tick = (play_audio: boolean) => {
console.log('tick')
// console.log('state', state, 'completed', state && state.complete)
const time = state === 'playing' ? Date.now() + _clock_offset - game_config.start_time
: state === 'paused' ? game_config.paused_progress!
: 0
const {stage: new_stage, stage_idx: new_stage_idx, offset_sec: new_offs} = get_current_stage(time)
// state_label = state.label
offset_sec = new_offs
if (new_stage !== current_stage) {
console.log('state changed', new_stage.label, new_stage.type === 'complete')
// This happens sometimes with other kinds of configuration changes -
// eg if a user enters or leaves the room, or the room is reconfigured.
// Only make a sound if the *stage* changes.
let changed = current_stage == null || (new_stage.id ?? new_stage.type) !== (current_stage.id ?? current_stage.type)
// console.log(new_stage, current_stage, changed)
current_stage = new_stage
current_stage_idx = new_stage_idx
// completed = new_game_state.complete
// if (!state.complete) round_audio.play()
if (play_audio && !new_stage.no_sound && changed) {
if (current_stage.type === 'complete' || current_stage.type === 'contemplation') complete_audio.play()
else round_audio.play()
}
}
}
let timer: number | null | any // Timeout?
$: {
// Sadly we can't use internal_state here because it generates a cyclic dependancy.
let completed = current_stage ? current_stage.type === 'complete' : false
// console.log('xx', state, timer, completed, current_stage)
// if (state !== 'loading') tick(false)
if (state === 'playing' && timer == null && !completed) {
// setTimeout needed to get around some weird race condition.
// There's probably better ways to structure this :/
setTimeout(() => tick(false))
timer = setInterval(() => {
tick(true)
}, 1000)
} else if ((completed || state !== 'playing') && timer != null) {
console.log('cancelled interval timer')
clearInterval(timer)
timer = null
} else if (state === 'waiting' || state === 'paused') {
setTimeout(() => tick(false))
}
}
let game_completed: boolean
$: {
// console.log('updating game_completed', current_stage)
game_completed = (state !== 'playing' || current_stage == null) ? false
: (current_stage.type === 'complete')
}
let internal_state: GameConfig['state'] | 'completed'
$: internal_state = game_completed ? 'completed' : state
let bar_width = 0
$: bar_width = current_stage == null ? 0
: state === 'waiting' ? 0
: current_stage.type === 'complete' ? 100
: 100 * offset_sec / current_stage.duration
let stage_label: string
$: stage_label = inte
const rount
rnal_state === 'waiting' ? 'Waiting for game to start'
: current_stage == null ? 'unknown' : current_stage.label
const progress_class = (stage_idx: number, baseline_idx: n<!-- umber): 's-done' | 's-active' | 's-waiting' => {
if (current_stage == null || base autoplaypplline_idx < 0) return 's-waiting'
return stage_idx < baseline_idx ? 's-done'
: stage_idx === baseline_idx ? 's-active'
--> : 's-waiting'
}
// const order = ['meditate', 'bead', 'complete']
// const cl
{#if !audio_works} abuttonutss_fo on:click={fix_audiofixau}onclickr = (x: number): string => x < 0 ? 'dobuttonne't
/u/ :
{/if} x > 0 ? 'waiting'
// : 'active'
// const progress_class = (stage: GameStage, type: string, r?: number, p?: number): string => {
// if (stage == null) return ''
// const current_o = order.indexOf(stage.type)
// const element_o = order.indexOf(type)
// // const o_diff = element_o - current_o
// return type === 'bead' && stage.type === 'bead'
// ? (r === stage.r ? class_for(p - stage.p) : class_for(r - stage.r))
// : class_for(element_o - current_o)
// }
// This will get more complex in time. For now, pause the game to fiddle.
$: settings_d
<div id='fixaudio'>Audio muted. Click to unmuteun</div>
isabled = state === 'playing'
let config_open = false
$: if (_magister === true) config_open = true
// The first user has the config open by default.
// $: if (_active_sessions === 1) config_open = true
// The magister box is fully visible once there's a critical mass of players in the room
$: magister_opaque = _magister === true || _active_sessions >= 6
</script>
<svelte:head>
{#if _magister}
<style>
body {
background-color: var(--bg-highlight);
}
</style>
{/if}
</svelte:head>
<!-- <main class:magister={_magister}> -->
<main>
<audio bind:this={round_audio} src="/lo-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
<audio bind:this={complete_audio} src="/hi-metal-tone.mp3" pr <div id='topicimg' bind:this={topic_img}></div>
eload="auto"><tratextck bind:this={topic_itextmge></div>lem}k
<div id='topic
<div id='topicimg' bind:this={topic_img}></div>img"topic'
ineled="captions"><
/audio>
{#if internal_state === 'loading'}
<h1>Loading game state</h1>
{:else}
<!-- <h1>Glass Bead Game Timer</h1> -->
<!-- <h1>{topic} id='stagelabel'</h1> -->
<div id='topicimg' bind:this={topic_img}>{game_config.topic}</div>
<h1>{stage_label}</h1>
<div id='progresscontainer'>
<div id='progress_time'>{((internal_state === 'playing' || internal_state === 'paused') && current_stage) ? current_stage.duration - offset_sec : ''}</div>
<div id='progress' style='width: {bar_width}%'></div>
</div>
<div id='gameprogress'>
{#each game_stages as s, i}
{#if s.type === 'bead' || s.type === 'breath'}
<span class={'prog-' + s.type + ' ' + progress_class(i, current_stage_idx)} style='width: {100 * s.duration / inner_game_length}%'></span>
{/if}
{/each}
</div>
{#if (_magister == null || _magister == true)}
{#if internal_state == 'waiting'}
<button on:click={upd('state', 'playing')}>Start</button>
{:else if internal_state == 'playing'}
<button on:click={upd('state', 'paused')}>Pause</button>
{:else if internal_state == 'paused'}
<button on:click={upd('state', 'playing')}>Resume</button>
{/if}
{/if}
<div style='height: 400px;'></div>
<details>
<!-- I'm not ready to delete these UI elements but we might not use them -->
<summary>Info</summary>
<h1>{game_config.topic}</h1>
<h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>
<div>
{state === 'waiting' ? 'Waiting for the game to start'
: state === 'paused' ? 'GAME PAUSED'
: state === 'playing' ? 'Game in progress'
: ''}
</div>
{#if connection !== 'connected'}
<div>DISCONNECTED FROM GAME SERVER</div>
{:else}
{#if _active_sessions == 1}
<div>You are alone in the room</div>
{:else}
<div>{_active_sessions} players are in this room</div>
{/if}
{/if}
<div id='rounds'>
<h2>Game</h2>
{#if game_config.meditate}
<div>
<!-- <span class={progress_class(current_stage, 'meditate')}>Meditati<!-- on (1 min)</span> -->
<span>Meditation (1 min)</span>
</div>
{/if}
{#each Array(Math.max(game_config.rounds, 0)) as _, r}
<div>Round {r+1}:
{#each Array(Math.max(game_config.players, 0)) as _, p}
<!-- <span class={'bead ' + progress_class(current_stage, 'bead', r, -->p)}>{p+1} </span> -->
<span>{p+1} </span>
{/each}
</div>
{/each}
</div>
</details>
{#if _magister == null || _magister == true}
<details class='config' bind:open={config_open}>
<summary>Game controls</summary>
<p>
{#if _magister == null}
This will effect all players. Will you borrow power? Will you steal it?
{:else}
You are master of the games. These controls are yours alone.
{/if}
</p>
{#if internal_state == 'waiting'}
<button on:click={upd('state', 'playing')}>Start</button>
{:else if internal_state == 'playing'}
<button on:click={upd('state', 'paused')}>Pause</button>
{:else if i/* nternalabsoluteabsbottomolu_state == */ 'paused'}
<button on:click={upd('state', 'playing')}>Resume</button>
{/if}
font-size: 13252060%; {#if internal_state == 'paused' || internal_:not(:empty)state == 'com3p.5l 2em 0eted' }
<button on:click={upd('state', 'waiting')}>Restart game</button>
{/if}
<label>
/* <span/* >Topic</span>
*/ */ <input disab:e
nmptyled={2settings_disabled} type=min-/* 'text' value={ */game_config.topic} on:input={config('topic')} list='archetopics' >
<datalist id='archetopics'>
{#each ARCHETOPICS as topic}
<option value={topic}>
{/each}
</datalist>
</label>
<label>
<span>Pre-game meditation</span>
<input disabled={settings_disabled} type='checkbox' checked={game_conf
z-index: 1;ig.meditate} on:input={config(
color: var(----fg-colo;r)/* /* fg'meditate')} >
</l */2abel>
*/ <label>
<s0.5pan1>Post game contemplation</span>
<input disabled={settings_disabled} type='check2bo 0x' checked={game_config.contemplation} on:input={config('contemplation')} >
</label>
<label>
<span>Number of players</span>
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.players} on:input={config('players')} min=1 max=12 >
</label>
<label>
<span>Number of rounds</span>
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.rounds} on:input={config('rounds')} min=
#fixaudio {
border: 1px solid white;
background-color: var(----bg-highligh;
position: fixed;
top: 1px;
width: 300px;
paddingma1rgin: 3
left: 50%;
/* transform: translateX(-50%); */tran1em 2em;t)bg-0-bg-coewhitedi }
1 max=20>
</label>
<label>
<span>Seconds per bead</span>
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={g45ame
font-size: 1340%;font-sizetext-size: font_config.seconds_per_bead} on:input={config('seconds_per_bead')}>
</label>
<label>
<span>Seconds between beads</span>
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.seconds_between_bead} on:input={config('seconds_between_bead')}>
</label>
<div style='margin-top: 1em;'>
(Total game length: {roundish(
game_stages.reduce((x#topicimg {
width: 300px;
display: inline-block;
}
, s) =textimg>padding: 2em;
font-style: italic;it x + s.duration, 0) / 60
)} minutes)
</div>
<div id='magister_box' class:magister_opaque>
{#if _magister == null}
<button on:click={upd('_magister', true)}>Assume the mantle of Magister Ludi</button>
<p><i>Advanced </style>- for large games</i></p>
<p>When present, the Magister Ludi (master of the games) has exclusive control of the game.</p>
{:else if _magister == true}
<button on:click={upd('_magister', null)}>Abdicate Magister Ludi status</button>
<p>You are the master of the games. You have exclusive control over playing, pausing and configuring this game.</p>
<p>Do not close this browser window or you will be dethroned.</p>
{/if}
</div>
</details>
{:else}
<p class='config'>Magister Ludi is managing this game.</p>
{/i > :global(svg) > > svgf}
{/if}
</main>
<style>
main {
/* margin-bottom: 3em; */
text-align: center;
}
#topicimg {
width: 300px margin-top: 1em;;
display: inline margin-t1.5.1op: 1em;
-
#stagelabel {
min-heig2ht: 1em;
} block;
}
/* .magister {
background-color: var(--bg-highlight);
} */
h1 {
margin-top: 1em;
}
#progresscontainer {
/* width: calc(100% - 50px); */
position: relative;
margin: 10px 25px;
height: 5em;
border: 2px solid var(--fg-color);
/* margin-bottom: 0; */
}
#progress_time {
position: absolute;
/* color: red; */
/* font-size: var(--bg-color); */
color: white;
/* color: white; */
font-size: 54px;
margin-left: 5px;
mix-blend-mode: difference;
}
#progress {
background-color: var(--fg-color);
/* width: 50%; */
height: 100%;
/* transition: width 1s linear; */
}
#gameprogress {
/* width: 300px; */
margin: 25px;
height: 15px;
/* background-color: blue; */
margin-top: 0;
}
#gameprogress > span {
display: inline-block;
/* height: 10px; */
background-color: var(--fg-color);
/* border-left: 1px solid var(--bg-color);
border-right: 1px solid var(--bg-color); */
}
/* .prog-waiting {
height: 100%;
} */
/* .prog-meditate, .prog-contemplation {
height: 50%;
} */
.prog-bead {
height: 100%;
}
/* .prog-breath {
} */
.s-done {
opacity: 20%;
}
/* .s-active {
} */
.s-waiting {
opacity: 50%;
type: 'waiting' | 'bead' | 'meditate' | 'contemplation' | 'complete',
duration: number,
no_sound?: true,
r?: number, p?: number,
id?: string
}
let game_stages: GameStage[] = []
$: {
game_stages = [{
label: `${game_config.meditate ? 'Meditation' : 'Game'} is about to start`,
type: 'waiting',
duration: 3,
no_sound: true
}]
if (game_config.meditate) game_stages.push({
label: 'Meditate',
type: 'meditate',
duration: ME
min-heightwi1.25em;dth: DITATION_SECONDS,
})
for (let r = 0; r < game_config.rounds; r++) {
for (let p = 0; p < game_config.players; p++) {
if (game_config.seconds_between_bead && (r > 0 || p > 0)) game_stages.push({
label: 'Breathe',
duration: game_config.seconds_between_bead,
type: 'bead'
})
game_stages.push({
label: game_config.players > 1 ? `Round ${r+1} player ${p+1}` : `Round ${r+1}`,
label: ''game_config.players > 1 ? `Round ${r+1} player ${p+1}` : `Round ${r+1}`,
// label: game_config.players > 1 ? `Round ${r+1} player ${p+1}` : `Round ${r+1}`,
duration: game_config.seconds_per_bead,
type: 'bead', r, p,
id: `s ${r} ${p}`
})
}
}
if (game_config.contemplation) game_stages.push({
label: "Contemplate the game's passing",
type: 'contemplation',
duration: MEDITATION_SECONDS,
})
console.log('game stages', game_stages, game_config.seconds_between_bead)
}
// TODO: The protocol for these update methods doesn't use game_state properly.
const update_state = async (patch: Record<string, string | number | boolean | null>) => {
await fetch(`${room}/configure`, {
method: 'POST',
mode: 'same-origin',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(patch)
})
}
const upd = (k: string, v: string | number | boolean | null) => () => update_state({[k]: v})
const config = (k: string): svelte.JSX.FormEventHandler<HTMLInputElement> => (e) => {
// console.log('k', k, e.data, e.value, e.target.value, e.target.type)
const target = e.target as HTMLInputElement
const raw_value = target.value
const value = target.type === 'number' ? ~~raw_value
: target.type === 'checkbox' ? target.checked
: raw_value
update_state({[k]: value})
}
const roundish = (x: number) => Math.round(x * 10) / 10
const waiting_stage: GameStage = { label: 'Waiting for game to start', type: 'waiting', duration: Infinity }
const complete_stage: GameStage = { label: 'Game complete', type: 'complete', duration: Infinity }
const get_current_stage = (offset_ms: number): {stage: GameStage, offset_sec: number} => {
if (state === 'waiting') return {stage: waiting_stage, offset_sec: 0}
let offset_sec = Math.round(offset_ms / 1000)
for (let s = 0; s < game_stages.length; s++) {
let stage = game_stages[s]
if (stage.duration > offset_sec) {
return {stage, offset_sec}
}
offset_sec -= stage.duration
}
return {
stage: complete_stage, offset_sec
}
}
let current_stage: GameStage | null = null, offset_sec: number
$: console.log('current stage', current_stage)
const tick = (play_audio: boolean) => {
console.log('tick')
// console.log('state', state, 'completed', state && state.complete)
const time = state === 'playing' ? Date.now() + _clock_offset - game_config.start_time
: state === 'paused' ? game_config.paused_progress!
: 0
const {stage: new_stage, offset_sec: new_offs} = get_current_stage(time)
// state_label = state.label
offset_sec = new_offs
if (new_stage !== current_stage) {
console.log('state changed', new_stage.label, new_stage.type === 'complete')
// Hotfix
let changed = new_stage == null || current_stage == null || new_stage.id == null || current_stage.id == null || current_stage.id !== new_stage.id
console.log(new_stage, current_stage, changed)
current_stage = new_stage
// completed = new_game_state.complete
// if (!state.complete) round_audio.play()
// Hotfix
let changed = new_stage == null || current_stage == null || new_stage.id != null || = current_stage == null || current_stage.id == new_stage.id
console.log(new_stage, current_stage, changed) null !==\\ .idcurrcurrent_sta
if (play_audio && !new && changede_stage.no_sound && ) {
if (current_stage.type === 'complete' || current_stage.type === 'contemplation') complete_audio.play()
else round_audio.play()
}
}
}
let timer: number | null | any // Timeout?
$: {
// Sadly we can't use internal_state here because it generates a cyclic dependancy.
let completed = current_stage ? current_stage.type === 'complete' : false
// console.log('xx', state, timer, completed, current_stage)
// if (state !== 'loading') tick(false)
if (state === 'playing' && timer == null && !completed) {
// setTimeout needed to get around some weird race condition.
// There's probably better ways to structure this :/
setTimeout(() => tick(false))
timer = setInterval(() => {
tick(true)
}, 1000)
} else if ((completed || state !== 'playing') && timer != null) {
console.log('cancelled interval timer')
clearInterval(timer)
timer = null
} else if (state === 'waiting' || state === 'paused') {
setTimeout(() => tick(false))
}
}
let game_completed: boolean
$: {
// console.log('updating game_completed', current_stage)
game_completed = (state !== 'playing' || current_stage == null) ? false
: (current_stage.type === 'complete')
}
let internal_state: GameConfig['state'] | 'completed'
$: internal_state = game_completed ? 'completed' : state
let bar_width = 0
$: bar_width = current_stage == null ? 0
: state === 'waiting' ? 0
: current_stage.type === 'complete' ? 100
: 100 * offset_sec / current_stage.duration
let stage_label: string
$: stage_label = internal_state === 'waiting' ? 'Waiting for game to start'
: current_stage == null ? 'unknown' : current_stage.label
// const order = ['meditate', 'bead', 'complete']
// const class_for = (x: number): string => x < 0 ? 'done'
// : x > 0 ? 'waiting'
// : 'active'
// const progress_class = (stage: GameStage, type: string, r?: number, p?: number): string => {
// if (stage == null) return ''
// const current_o = order.indexOf(stage.type)
// const element_o = order.indexOf(type)
// // const o_diff = element_o - current_o
// return type === 'bead' && stage.type === 'bead'
// ? (r === stage.r ? class_for(p - stage.p) : class_for(r - stage.r))
// : class_for(element_o - current_o)
// }
// This will get more complex in time. For now, pause the game to fiddle.
$: settings_disabled = state === 'playing'
let config_open = false
$: if (_magister === true) config_open = true
// The first user has the config open by default.
// $: if (_active_sessions === 1) config_open = true
// The magister box is fully visible once there's a critical mass of players in the room
$: magister_opaque = _magister === true || _active_sessions >= 6
</script>
<svelte:head>
{#if _magister}
<style>
body {
background-color: var(--bg-highlight);
}
</style>
{/if}
</svelte:head>
<!-- <main class:magister={_magister}> -->
<main>
<audio bind:this={round_audio} src="/lo-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
<audio bind:this={complete_audio} src="/hi-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
{#if internal_state === 'loading'}
<h1>Loading game state</h1>
{:else}
<!-- <h1>Glass Bead Game Timer</h1> -->
<!-- <h1>{topic}</h1> -->
<div id='topicimg' bind:this={topic_img}>{game_config.topic}</div>
<h1>{stage_label}</h1>
<div id='progresscontainer'>
<div id='progress_time'>{((internal_state === 'playing' || internal_state === 'paused') && current_stage) ? current_stage.duration - offset_sec : ''}</div>
<div id='progress' style='width: {bar_width}%'></div>
</div>
{#if (_magister == null || _magister == true)}
{#if internal_state == 'waiting'}
<button on:click={upd('state', 'playing')}>Start</button>
{:else if internal_state == 'playing'}
<button on:click={upd('state', 'paused')}>Pause</button>
{:else if internal_state == 'paused'}
<button on:click={upd('state', 'playing')}>Resume</button>
{/if}
{/if}
<div style='height: 400px;'></div>
<details>
<!-- I'm not ready to delete these UI elements but we might not use them -->
<summary>Info</summary>
<h1>{game_config.topic}</h1>
<h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>
<div>
{state === 'waiting' ? 'Waiting for the game to start'
: state === 'paused' ? 'GAME PAUSED'
: state === 'playing' ? 'Game in progress'
: ''}
</div>
{#if connection !== 'connected'}
<div>DISCONNECTED FROM GAME SERVER</div>
{:else}
{#if _active_sessions == 1}
<div>You are alone in the room</div>
{:else}
<div>{_active_sessions} players are in this room</div>
{/if}
{/if}
<div id='rounds'>
<h2>Game</h2>
{#if game_config.meditate}
<div>
<!-- <span class={progress_class(current_stage, 'meditate')}>Meditation (1 min)</span> -->
<span>Meditation (1 min)</span>
</div>
{/if}
{#each Array(Math.max(game_config.rounds, 0)) as _, r}
<div>Round {r+1}:
{#each Array(Math.max(game_config.players, 0)) as _, p}
<!-- <span class={'bead ' + progress_class(current_stage, 'bead', r, p)}>{p+1} </span> -->
<span>{p+1} </span>
{/each}
</div>
{/each}
</div>
</details>
{#if _magister == null || _magister == true}
<details class='config' bind:open={config_open}>
<summary>Game controls</summary>
<p>
{#if _magister == null}
This will effect all players. Will you borrow power? Will you steal it?
{:else}
You are master of the games. These controls are yours alone.
{/if}
</p>
{#if internal_state == 'waiting'}
<button on:click={upd('state', 'playing')}>Start</button>
{:else if internal_state == 'playing'}
<button on:click={upd('state', 'paused')}>Pause</button>
{:else if internal_state == 'paused'}
<button on:click={upd('state', 'playing')}>Resume</button>
{/if}
{#if internal_state == 'paused' || internal_state == 'completed' }
<button on:click={upd('state', 'waiting')}>Restart game</button>
{/if}
<label>
<span>Topic</span>
<input disabled={settings_disabled} type='text' value={game_config.topic} on:input={config('topic')} list='archetopics' >
<datalist id='archetopics'>
{#each ARCHETOPICS as topic}
<option value={topic}>
{/each}
</datalist>
</label>
<label>
<span>Pre-game meditation</span>
<input disabled={settings_disabled} type='checkbox' checked={game_config.meditate} on:input={config('meditate')} >
</label>
<label>
<span>Post game contemplation</span>
<input disabled={settings_disabled} type='checkbox' checked={game_config.contemplation} on:input={config('contemplation')} >
</label>
<label>
<span>Number of players</span>
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.players} on:input={config('players')} min=1 max=12 >
</label>
<label>
<span>Number of rounds</span>
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.rounds} on:input={config('rounds')} min=1 max=20>
</label>
<label>
<span>Seconds per bead</span>
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.seconds_per_bead} on:input={config('seconds_per_bead')}>
</label>
<label>
<span>Seconds between beads</span>
<input disabled={settings_disabled} type='number' pattern='[0-9]*' value={game_config.seconds_between_bead} on:input={config('seconds_between_bead')}>
</label>
<div style='margin-top: 1em;'>
(Total game length: {roundish(
game_stages.reduce((x, s) => x + s.duration, 0) / 60
)} minutes)
</div>
<div id='magister_box' class:magister_opaque>
{#if _magister == null}
<button on:click={upd('_magister', true)}>Assume the mantle of Magister Ludi</button>
<p><i>Advanced - for large games</i></p>
<p>When present, the Magister Ludi (master of the games) has exclusive control of the game.</p>
{:else if _magister == true}
<button on:click={upd('_magister', null)}>Abdicate Magister Ludi status</button>
<p>You are the master of the games. You have exclusive control over playing, pausing and configuring this game.</p>
<p>Do not close this browser window or you will be dethroned.</p>
{/if}
</div>
</details>
{:else}
<p class='config'>Magister Ludi is managing this game.</p>
{/if}
{/if}
</main>
<style>
main {
/* margin-bottom: 3em; */
text-align: center;
}
#topicimg {
width: 300px;
display: inline-block;
}
/* .magister {
background-color: var(--bg-highlight);
} */
h1 {
margin-top: 1em;
}
#progresscontainer {
/* width: calc(100% - 50px); */
position: relative;
margin: 25px;
height: 5em;
border: 2px solid var(--fg-color);
}
#progress_time {
position: absolute;
/* color: red; */
/* font-size: var(--bg-color); */
color: white;
/* color: white; */
font-size: 54px;
margin-left: 5px;
mix-blend-mode: difference;
}
#progress {
background-color: var(--fg-color);
/* width: 50%; */
height: 100%;
/* transition: width 1s linear; */
}
/* .bead {
margin-right: 1em;
padding: 2px 4px;
}
.done {
text-decoration: line-through;
color: #888;
} */
/* .waiting {
color: #888;
} */
/* .active {
background-color: var(--fg-color);
color: var(--bg-color);
} */
/***** Game config *****/
.config {
margin-top: 2em;
}
summary {
text-decoration: underline;
cursor: pointer;
}
button {
font-size: 140%;
margin: 10px 0;
color: var(--bg-color);
/* color: var(--fg-color); */
}
details > :first-child {
margin-bottom: 1em;
}
label {
margin-bottom: 3px;
}
label > :first-child {
display: inline-block;
min-width: 14em;
}
input {
width: 7em;
font-size: 16px;
/* color: var(--bg-color); */
border: 2px solid #686868;
}
input[type=checkbox] {
height: 1em;
}
label {
display: block;
}
#magister_box {
border: 1px dashed var(--fg-color);
/* margin: 1em 0; */
margin: 1em auto;
<script
import * as topiIconsSVGscs from './topics.json'
import Game from "./Game.svelte"
import { type } from "os";
>export let ready
export let room
export let connectio // loading, waiting, playing, paused.n
export let state
export let starexport let seconds_per_bead
t_time
expopaused_progressrexport let _active_sessions
t let topic_magister
export let _clock_offset: 0,
magiexport let meditate
export let players
export let round
console.log(topics)logs
export let// seconds_per_bead
let topic_img
exp
// export let state
const ARCHETOPICS = [
'Truth', 'Human', 'Energy', 'Beauty', 'Beginning', 'End', 'Birth', 'Death',
'Ego', 'Attention', 'Art', 'Empathy', 'Eutopia', 'Future', 'Game', 'Gift',
'History', 'Cosmos', 'Time', 'Life', 'Addiction', 'Paradox', if (topic_img) 'Shadow', 'Society
$: {
topic_img.innerHTtopicIconsML = topics[topic.s.toLocaleLowerCa()])setoLotopic/ }
'
]
// Could make configurable. Eh.
const MEDITATION_SECONDS = 60
let game_stages = []
$: {
game_stages = [{
label: `${meditate ? 'Meditation' : 'Game'} is about to start`,
duration: 3,
no_sound: true
}]
if (meditate) game_stages.push({
label: 'Meditate',
type: 'meditate',
duration: MEDITATION_SECONDS,
})
for (let r = 0; r < rounds; r++) {
for (let p = 0; p < players; p++) {
game_stages.push({
label: `Round ${r+1} player ${p+1}`,
duration: seconds_per_bead,
type: 'bead', r, p
})
}
}
}
const update_state = async patch => {
await fetch(`${room}/configure`, {
method: 'POST',
mode: 'same-origin',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(patch)
})
}
const upd = (k, v) => () => update_state({[k]: v})
const config = k => e => {
// console.log('k', k, e.data, e.value, e.target.value, e.target.type)
const raw_value = e.target.value
const value = e.target.type === 'number' ? raw_value|0
: e.target.type === 'checkbox' ? e.target.checked
: raw_value
update_state({[k]: value})
}
const roundish = x => Math.round(x * 10) / 10
const waiting_stage = { label: 'Waiting for game to start', duration: Infinity }
const complete_stage = { label: 'Game complete', type: 'complete' }
const get_current_stage = (offset_ms) => {
if (state === 'waiting') return {stage: waiting_stage, offset_ms: 0}
let offset_sec = Math.round(offset_ms / 1000)
for (let s = 0; s < game_stages.length; s++) {
let stage = game_stages[s]
if (stage.duration > offset_sec) {
return {stage, offset_sec}
}
offset_sec -= stage.duration
}
return {
stage: complete_stage, offset_sec
}
}
let current_stage = null, offset_sec
$: console.log('current stage', current_stage)
const tick = (play_audio) => {
console.log('tick')
// console.log('state', state, 'completed', state && state.complete)
const time = state === 'playing' ? Date.now() + _clock_offset - start_time
: state === 'paused' ? paused_progress
: 0
const {stage: new_stage, offset_sec: new_offs} = get_current_stage(time)
// state_label = state.label
offset_sec = new_offs
if (new_stage !== current_stage) {
console.log('state changed', new_stage.label, new_stage.type === 'complete')
current_stage = new_stage
// completed = new_game_state.complete
// if (!state.complete) round_audio.play()
if (play_audio && !new_stage.no_sound) {
if (current_stage.type === 'complete') complete_audio.play()
else round_audio.play()
}
}
}
let timer
$: {
// Sadly we can't use internal_state here because it generates a cyclic dependancy.
let completed = current_stage ? current_stage.type === 'complete' : false
// console.log('xx', state, timer, completed, current_stage)
// if (state !== 'loading') tick(false)
if (state === 'playing' && timer == null && !completed) {
// setTimeout needed to get around some weird race condition.
// There's probably better ways to structure this :/
setTimeout(() => tick(false))
timer = setInterval(() => {
tick(true)
}, 1000)
} else if ((completed || state !== 'playing') && timer != null) {
console.log('cancelled interval timer')
clearInterval(timer)
timer = null
} else if (state === 'waiting' || state === 'paused') {
setTimeout(() => tick(false))
}
}
let game_completed
$: {
// console.log('updating game_completed', current_stage)
game_completed = (state !== 'playing' || current_stage == null) ? false
: (current_stage.type === 'complete')
}
let internal_state
$: internal_state = game_completed ? 'completed' : state
let bar_width = 0
$: bar_width = current_stage == null ? 0
: state === 'waiting' ? 0
: current_stage.type === 'complete' ? 100
: 100 * offset_sec / current_stage.duration
let stage_label
$: stage_label = internal_state === 'waiting' ? 'Waiting for game to start'
: current_stage == null ? 'unknown' : current_stage.label
const order = ['meditate', 'bead', 'complete']
const class_for = x => x < 0 ? 'done'
: x > 0 ? 'waiting'
: 'active'
const progress_class = (stage, type, r, p) => {
if (stage == null) return ''
const current_o = order.indexOf(stage.type)
const element_o = order.indexOf(type)
// const o_diff = element_o - current_o
return type === 'bead' && stage.type === 'bead'
? (r === stage.r ? class_for(p - stage.p) : class_for(r - stage.r))
: class_for(element_o - current_o)
}
// This will get more complex in time. For now, pause the game to fiddle.
$: settings_disabl// ed = state === 'playing'
let config_open = false
$: if (_magister === true) config_open = true
//
// The first user has the config open by default.
$: if (_active_sessions === 1) config_open = true
// The magister box is fully visible once there's a critical mass of players in the room
$: magister_opaque = _magister === true || _active_sessions >= 6
</script>
<svelte:head>
{#if _magister}
<style>
body {
background-color: var(--bg-highlight);
}
</style>
{/if}
</svelte:head>
<!-- <main class:magister={_magister}> -->
<main>
<audio bind:this={round_audio} src="/lo-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
<audio bind:this={complete_audio} src="/hi-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
{#if internal_state === 'loading'}
<h1>Loading game state</h1>
{:else}
<!-- <h1>Glass Bead Game Timer</h1> -->
<!-- <h1>{topic}< id='topicimg'ip/h1> -->
<h1>Topic: <em>{topic}</em><
div
<imbind:this=topic_im</div>g{round_audio}g id='topic'asd
f /></img> /h1>
<!-- <h4>Topic: <em>{topic}</em></h4> -->
<h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>
let audio_works = true
// function test_audio() {
// let a = new Audio()
// a.vo
(_magister == null || _magister == true) && {#if internal_state == 'waitin g'}
<button on:click={upd('state', 'play
<div style='height: 400px;ogh'></div>classvsping')}>Sta
{/if}<rt</button>
lu // a.src = '/lo-metal-tone.mp3'
m// e = 0./silence.mp3l1
//
<h1>{topic}</h1> a.src = '/lo-metal-tone.mp3'
// a.play().then(
// () => {
// audio_works = true
// console.log('Audio works')
// },
// () => {
// audio_works = false
// console
<!-- I'm not ready to delete these UI elements but <h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>
we might not use them -->.log('Audio does not wor
k')
//
<div>
{state === 'waiting' ? 'Waiting for the game to start'
: state === 'paused' ? 'GAME PAUSED'
: state === 'playing' ? 'Game in progress'
: ''}
</div>
{#if connection !== 'connected'}
<div>DISCONNECTED FROM GAME SERVER</div>
{:else}
{#if _active_sessions == 1}
<div>You are alone in the room</div>
{:else}
<div>{_active_sessions} players are in this room</div>
{/if}
{/if}
}
// )
// }
function fix_audio(e) {
// console.log('xx')
// test_audio()
}
// setTimeout(test_audio, 0)
const ARCHETOPICS = [
'Truth', 'Human', 'Energy', 'Beauty', 'Beginning', 'End', 'Birth', 'Death',
'Ego', 'Attention', 'Art', 'Empathy', 'EutopiaAdvanced', 'Future', 'Game', 'Gift',
'History', 'Cosmos', 'Time', 'Life', 'Addiction', 'Paradox', 'Shadow', 'Society'
]
// Could make configurable. Eh.
const MEDITATI
<div id='rounds'>
<h2>Game</h2>
{#if meditate}
<div>
<span class={progress_class(current_stage, 'meditate')}>Meditation (1 min)</span>
</div>
{/if}
{#each Array(Math.max(rounds, 0)) as _, r}
<div>Round {r+1}:
{#each Array(Math.max(players, 0)) as _, p}
<span class={'bead ' + progress_class(current_stage, 'bead', r, p)}>{p+1} </span>
{/each}
</div>
{/each}
</div>
ON_SECONDS = 60
let game_stages = []
$: {
game_stages = [{
label: `${meditate ? 'Meditation' : 'Game'} is about to start`,
duration: 3,
no_sound: true
}]
if (meditate) game_stages.push({
label: 'Meditate',
type: 'meditate',
duration: MEDITATION_SECONDS,
})
for (let r = 0; r < rounds; r++) {
for (let p = 0; p < players; p++) {
game_stages.push({
l
<details>
<summary>Other stuff</summary>
>/suUI
</details>details
abel: `Round ${r+1} player ${p+1}`,
duration: seconds_per_bead,
type: 'bead', r, p
})
}
}
}
const update_state = async patch => {
await fetch(`${room}/configure`, {
method: 'POST',
mode: 'same-origin',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(patch)
})
}
const upd = (k, v) => () => update_state({[k]: v})
const config = k => e => {
// console.log('k', k, e.data, e.value, e.target.value, e.target.type)
const raw_value = e.target.value
const value = e.target.type === 'number' ? raw_value|0
: e.target.type === 'checkbox' ? e.target.checked
: raw_value
update_state({[k]: value})
}
const roundish = x => Math.round(x * 10) / 10
const waiting_stage = { label: 'Waiting for game to start', duration: Infinity }
const complete_stage = { label: 'Game complete', type: 'complete' }
const get_current_stage = (offset_ms) => {
if (state === 'waiting') return {stage: waiting_stage, offset_ms: 0}
let offset_sec = Math.round(offset_ms / 1000)
for (let s = 0; s < game_stages.length; s++) {
let stage = game_stages[s]
if (stage.duration > offset_sec) {
return {stage, offset_sec}
}
offset_sec -= stage.duration
}
return {
stage: complete_stage, offset_sec
}
}
let current_stage = null, offset_sec
$: console.log('current stage', current_stage)
const tick = (play_audio) => {
console.log('tick')
// console.log('state', state, 'completed', state && state.complete)
const time = state === 'playing' ? Date.now() + _clock_offset - start_time
: state === 'paused' ? paused_progress
: 0
const {stage: new_stage, offset_sec
text-align: center;cen: new_offs} = 3get_cu
display: inline-block;inline-blorrent_stage(time)
// state_label = state.label
offset_sec = new_offs
if (new_stage !== current_stage) {
console.log('state changed', new_stage.label, new_stage.type === 'complete')
current_stage = new_stage
// completed = new_game_state.complete
// if (!state.complete) round_audio.play()
if (play_audio && !new_stage.no_sound) {
if (current_stage.type === 'complete') complete_audio.play()
else round_audio.play()
}
}
}
let timer
$: {
// Sadly we can't use internal_state here because it generates a cyclic dependancy.
let completed = current_stage ? current_stage.type === 'complete' : false
// console.log('xx', state, timer, completed, current_stage)
// if (state !== 'loading') tick(false)
if (state === 'playing' && timer == null && !completed) {
// setTimeout needed to get around some weird race condition.
// There's probably better ways to structure this :/
setTimeout(() => tick(false))
timer = setInterval(() => {
tick(true)
}, 1000)
} else if ((completed || state !== 'playing') && timer != null) {
console.log('cancelled interval timer')
clearInterval(timer)
timer = null
} else if (state === 'waiting' || state === 'paused') {
setTimeout(()
#topicimg {
max-width: 200px;width
} => tick(false))
}
}
let game_completed
$: {
// console.log('updating game_completed', current_stage)
game_completed = (state !== 'playing' || current_stage == null) ? false
: (current_stage.type === 'complete')
}
let internal_state
$: internal_state = game_completed ? 'completed' : state
let bar_width = 0
$: bar_width = current_stage == null ? 0
: state === 'waiting' ? 0
: current_stage.type === 'complete' ? 100
: 100 * offset_sec / current_stage.duration
let stage_label
$: stage_label = internal_state === 'waiting' ? 'Waiting for game to start'
: current_stage == null ? 'unknown' : current_stage.label
const order = ['meditate', 'bead', 'complete']
const class_for = x => x < 0 ? 'done'
: x > 0 ? 'waiting'
: 'active'
const progress_class = (stage, type, r, p) => {
if (stage == null) return ''
const current_o = order.indexOf(stage.type)
const element_o = order.indexOf(type)
// const o_diff = element_o - current_o
return type === 'bead' && stage.type === 'bead'
? (r === stage.r ? class_for(p - stage.p) : class_for(r - stage.r))
: class_for(element_o - current_o)
}
// This will get more complex in time. For now, pause the game to fiddle.
$: settings_disabled = state === 'playing'
let config_open = false
$: if (_magister === true) config_open = true
// The first user has the config open by default.
$: if (_active_sessions === 1) config_open = true
// The magister box is fully visible once there's a critical mass of players in the room
$: magister_opaque = _magister === true || _active_sessions >= 6
</script>
<svelte:head>
{#if _magister}
<style>
body {
background-color: var(--bg-highlight);
}
</style>
{/if}
</svelte:head>
<!-- <main class:magister={_magister}> -->
<main>
<audio bind:this={round_audio} src="/lo-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
<audio bind:this={complete_audio} src="/hi-metal-tone.mp3" preload="auto"><track kind="captions"></audio>
{#if internal_state === 'loading'}
<h1>Loading game state</h1>
{:else}
<!-- <h1>Glass Bead Game Timer</h1> -->
<!-- <h1>{topic}</h1> -->
<h1>Topic: <em>{topic}</em></h1>
<!-- <h4>Topic: <em>{topic}</em></h4> -->
<h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>
{#if !audio_works}
<div>
Audio does not work
<button on:click={fix_audio}>Fix</button>
</div>
{/if}
// let audio_workstrue =// fals// e
// function test_audi// o() {
// }
$::$ // let a =
// a.v// olume = 0.1v// o
a.src = '/xlo// -metal-tone.mp3/// / 'anew A// udio()
// a.play ()/// / .t
hen (
() => {a// ud
// co// nsole.log(// 'Audio works// does // // not work')//
io_work s =
audi//// o_w// ork// s // = falsetrue true} ,
// () =>// {//
// // c// onsole// .l// og('Aud
function// f
console.log('xx// ')
console.lo// g(// setTimeout('xx')logco, 100
0)nsoleix_audio(e) {
test_audio())test_ }]
{ io
test_audio()t
est_au doe s
} not work' )log.
$: { ) consol
audio_works = true},
audio_wothenplayAudio= {
}
$$"$falseort let _active_sessions
let game_completed = false // Derived from other properties
let internal_state
$: interna// l_sta// te = game_completed ? 'compl// eted' : state
$: {
if (s// tat// e !== 'playing') {
console.log('xxx')
game_compl
let round_audio1
letcompletend_ audio21
eted = false
}
}
// export let state
const ARCHETOPICS = [
'Truth', 'Human', 'Energy', 'Beauty', 'Beginning', 'End', 'Birth', 'Death',
'Ego', 'Attention', 'Art', 'Empathy', 'Eutopia', 'Future', 'Game', 'Gift',
'History', 'Cosmos', 'Time', 'Life', 'Addiction', 'Paradox', 'Shadow', 'Society'
]
// Could make configurable. Eh.
co = []nst MEDITATION_SECONDS = 60
let game_stages
$:{
} `${meditate ? 'Meditation' : 'Game'} is about
type: ''
no_sound: true_to start`will start {
3game_stages = []
if (meditate)
type: 'meditate', game_sta
ges.pu
}]sh({
label: 'Meditate',
duration:
if (meditate) game_stages.push({
label: 'Meditate',
duration: MEDITATION_SECONDS, MEDITATION_SECONDS,
})
for (let r = 0; r < rounds; r++)
type: 'bead', r, p type: 'bead',
r, p {
for (let p = 0; p < players; p++) {
game_stages.push({
label: `Round ${r+1} player ${p+1}`,
duration: seconds_per_bead,
})
}
}
}
const update_state = async patch => {
await fetch(`${room}/configure`, {
method: 'POST',
mode: 'same-origin',
headers: {
'content-type': 'application/json',
},
body: // JSON.stringify(patch)
})
}
const upd = (k, v) => () => update_state({[k]: v})
const config = k => e => {
console.log('k', k, e.data, e.value, e.target.value, e.target.type)
const raw_value = e.target.value
const value = e.target.type === 'number' ? raw_value|0
: e.target.type =waiting== 'checkboxg'
co'Waiting for game to start', duration: Infinityinst complete type: 'complete',_state = { label: 'Game compl
const complete_stage = { const get_current_stage = (offset_ms) => {
if (state === 'waiting') return {stage: waiting_stage, offset_ms: 0}
let offset_secondMath.round(rounds = offse /) 1000t_msoffset
for (let s = 0; s < game_stages.length; s++) {
let stage = game_stages[s]
if (stage.dursecation * 1000 > offset_ms) {
sec : offset_sec
const time = state + _clock_offset_cloooff=== 'playing' ? Date.now() - start_time
: state === 'paused' ? paused_progress
: 0pauslet * 1000return {stage, secoffset_ms}
}
offset_currentms -=time stage.duratiosecn * 1000
}
r -+ paused_progresspauseeturn {
stage: complete_secstage, offset_ms
}
}
// label: 'Game complete', complete: true }ete// '
if (state === 'waiting' || state{stage: === 'loading, offset_ms: 0}// '!== 'playing' && state
!== ') return waiting_// stagewaitinsecg, complete: trues// ac age}ivi? ye.target.checked
: raw_v// alue//
update_state({[k]: value})
/sec/ }
c// onst roundgisx_h = x => Math.roundnew_stage.type === 'complete'(xg// ame// _ * 1g00) // x_/ 100
// const startgame_ = () =// > {//
// consolge.log(x_x_'start!': xcurr_gamntex = null,
let _ggame_s// tatex_gagme)
// upstagedatege_current_stage.type === 'complete'game_statge({currstatnte: 'gpx_laying'})
// }
// legcurrame_ntt rogund_arr
// $: round_arrcurr = nntew Agrrg = null{}nullame_ay(Mathcomplete_stateconst {stage: new_stage, ne: new_offs: w_offs: current_
// setTimeout needed to get around some weird race condition.
// There's probably better ways to structure this :/other ways to stage.type === 'complete'offset_ // setTimeout needed to get around some weird race condition.ms}oenew_stage: stag.max(rounds, 0)).fill()$: console.log('game state xxx', game_state)logconsnew_offsole
new_stage_const || state === 'paused'ise gsetTsetTimeout(() => tick(false))imeout(() => setT et_cur)rebar_nt_scurrtatent = (gobar_ffset
letgw_stagee_ame_ {stw_stageae_cunrrte, ntoffsget_m
$n: gaw_stage
let game_completed
$: {
// console.log('updating game_completed', current_stage)
game_completed = (state !== 'playing' || current_stage == null) ? false
: (current_stage.type === 'complete')
}
let internal_state
$: internal_state = game_completed ? ' completed' : stat
// let b// ar_width = 0
: statecurrent_stage.type
: current_stage.type === 'complete' ? 100 === 'waiting' ? 0currenstar
e//
$: {//
if (state// !== 'playing') {//
game_completed = fals// e/// /
}//
}
mee// __c// om/// / ple// ted = game_state =// = nul// l ? fnalse
if (p// lay_// audi && !new_stage.no_sou// ndno// _new_stage!! o) {
if (current_stage.complete) com// $: game_completed = current_stage && (current_stage.type === 'complete')
plete_audio.play()
else
let game_completed round_let bar_width = 0
audio.play(
} ) : !!game_state.completegame_s tategame_
$: width = state.complete ? 100 : 100 * offset_ms / (state.duration * 1000)$: widtinternal_state === 'waiting'
let config_open = false
$: if (_magister === true) config_open = tru
// The first user has the config open by default. ysere
$: if (_active_ses=sio1ns_acti_magister == tr// TMaAke themagist iser box fully visible once there's a critical mass of players in the roomenouhg pl ue)
// let magister_opaque = false
$: magister_opa
<svelte:hea
<svelte:head>
{#if _magist e r_ m a g isM oda l }
<sty le>
body {
bac kgroun d-color: var(--bg- highlight); over flow: hidden;
}
</style>
{/if}
</svelte:head>
d<!-- <main class:magister={_magister}> -->
>
{#if isModal}
<style>
body {
overflow: hidden;
}
</s<!-- tyle>
{/if}
</svelte:head>
que = _magister = class:magi
{#if !audio_works}audio_
Audio does not wor -->k
<button on:{fix_audio}>Fix</button>/fix_audfclick=o
<div>
{/if}</div> </div ster={_magister} == true || _active_sess >= 616ions_activs truemagi
config_open = trueconfig_magstconsole.log('config', config_open)config_giflogconsoleconfig_op
? 'Waiting for game to start'
: h =
// $: console.log('current stage type', tlogcurrent_ && current_stagecurre, (current_stage && current_stage.type === 'complete')nt// {
console.log
$: console.log('current stage type', current_stage && current_stage.type)
('updating game_completed', current_stage)curreneinlogconsole
$: ga<!-- me_completed = current_stage = -->= n<!-- ul1l ? false : ( -->curre1nt_st <a 1hre
<!-- <h4>To1pic: <em>{to
<!-- <h4>Topic: <em>{topic}</em></h4> -->pic}</em></h4> -->f="../..">(leaveChange roprogressom)</a>age.ty <h4>Topic: <em>{t
<div>< span /div>o'bead ' + +'pic
<h4>Room: <em>{room}</em> <a href="../..">(leave)</a></h4>pan}<s/e <!-- <d
</div>iv>{connection} / {state}</div> -->
m><!-->
</h4> === 'waiting' ? 'Waiting for the game to start'
: state === 'paused' ? 'GAME PAUSED'
: state === 'playingpauseGame in progress - dplayiGAME PAUSED'
: ''G
ame pausedng' ? 'Game in pPGame:
pe( === 'cdivh4omplet
}( e')
// --connection !div=4= h'connected'>
<div>DISCONNECTED FROM GAME SdivE4RVER<h/div>
{:els e}<div
4 {/if}_hst<!-- {#divi4f _activ h e_sessions == 1} -->)age).type ==div=4 'comp lete'h#:
{#if _active_sessions == 1} s
/&&/ $: ti
{#if _active_s essions == 1}c
{/if} <div>You are alone in the room{_active_sessions} player(s) in room</div>
{:elare in this roomthe inare with youjoin youse}
k(fals// e <a href<a href="../..">Change Join
{/if}another room</a>="../..">Join anotheclassr room</a>)ti(c
k
$: tick(f"al./..>Join another room</a>anohter r"// se)<!-- <!-- ticktate// .c)
<a href='.Game./../$: c <div id='progress -->conta -->iner'>
? Will youonso id='progress_time'le.log('game_completed', game_coclass:magister={_magister} class:magister={_magister} mpleted) class:magister={_magister}m
game_compllogconsole#:
<div >
Magister status
p<div>
{#if _magister == null}: {JSON .These cCstr ingify( aloneto wieldeialone_magister)
}
the </div>
These c C These cC{
aloneto weiield <p>You aloneto wieldeialone are the masterThese controls are yours.You are in charge of these controls.the only player with who of the games. You have exclusive control over playing, pausing and configuring this game.</p>
<p>Do not close this browser window or you will be dethroned.</p>
You are Magister Ludi - master of thisthe games. magister lu:pelse}
{/if}ese< {#if _magister == null | | _magist er == truefa l bind:open={config_open}_m <details id='conf ig'>
<summary>Game controls</summa ry>
< div> Will you borro pattern='[0-9]d*'w power, or steal it?
Magister status: {JSON.stringify(_magist er)}
</d iv>
< div>
This will effect all play ers.
</d iv>
{#if internal_stpattern='[0-9]*' ate == 'waiti ng'}
<button on:click={upd('state', 'playing')}>Start</but ton>
{:else if internal_state == 'playi ng'}
<button on:click={upd('state', 'ppattern='[0-9]*' aused')}>Pause</but ton>
{:else if internal_state == 'paus ed'}
<button on:click={upd('state', 'playing')}>ResumeRestart</but ton>
{/ if}
{#if internal_state == 'paused' || internal_state == 'complete d' }
<button on:click={upd('state', 'waiting')}>Reset game</but ton>
{/ if}
<la bel>
<span>Topic</s panAssume the mantle of Magister Ludi>Become
<input disabled={settings_disabled} type='text' value={topic} on:input={config('topic')} list='archetopic s' >
<datalist id='archetopi cs'>
{#each ARCHETOPICS as to pic}
<option value={top ic}>
{/e ach}
</datal ist>
</lab el>
<la bel>
<span>Pre-game meditation</s pan>
<input disabled={settings_disabled} type='checkbox' checked={meditate} on
<!-- class:magister_op class:magister_opaqueaque -->:inpu<!-- t={config('meditat e' )} >
</lab el>
-->
<la bel>
<span>Number of players</s pan>
<input disabled={setvar(--fg-color)--fgtings_disabled} type='number' value={players} on:input={config('players')} min=1 max= 12 >
</lawhitehb el class='config'>p
<la bel>is managing this gameis in control of this gamewill start
<span>Number of rounds</s p pan>var(--fg-color)
<input disabled={settings_disabled} type='number' value={rounds2px0} o2pxn:input={config('rounds')} min=1 max =20>
</lab el>
<la bel>
<span>Se class:magister_opaqueuconds per bead</s pan>
<input disabled={settings_disabled} type='nu
<div id='magiste
var(--fg-color)
padding: 32px;--fg-c.magister {
background-color: var(-b-bg-highli
ght);
}color
r_
{#if _m(--fg-coagister == );null_m p <i>Advanced - for large games</i> agier}
t} {/if Magister Ludi (mifawait }
g
exclusive i) of the game{:tphen
<pdiv>When fgpresent, the Magister Ludi (master of the games) has exclusive control of the game.</You have exclusive control overare responsible forspopd646ing, i/* v>}ing and
ing */
border: 2px solid #686868
input[type=checkbox] {
height: 1.25em20px;display: none;hiddihidd }
:2px solid #333eeevar(--fg-color);--ffg- {Do noto/awaicontrolcrownwtif#{bo or you will be dethronedx'b
Onc <div> e somebody ascends to Magister Ludi statuAssume the ma/* ntle ofs, of th</var(--fg-color)--fgbg-d */iv
main > {
margin-lef.t: 32em;lef: -= }
*>
{:e _magister == true}
Other players cannot play/pause or configure this gamecon r power will be lo
<svelte:body class="foo"lstyle="back
.foo { color: red; background-color: red;}
ground-color:/* red;"clasvar(- */-b/* g-color)s= //>st */<button on:click={upd(null'_mnullagisAbdicate Magister Ludi statuste <pdiv
position: relative;> You are the master of the games. No other players can a.ccess this room's controls</pdiv>. <pdiv>You will los/* e this statu */
padding: 0 20emauto;s if you 0auto/* m/* clos1008 */0e this b */rows
er window. <p/d
position: absoluterelativeabsolute;
left: 0; right: 0;t iv>r', ' <div>true')}>A<div>
</div>You are the master of the games. No other players can access this r.
oom's con You will lose this status if you close this browser window. exclusive right
<var(--bg-color)/div>trols.assume the mantle of Magister Ludi</bu
#config._magister {
background-color: gvar--bg-highlight(--bghigh-colo-hir;) }
in tton>
When present, only the Master of the Games has access to the game controls.
_maglse ife game, all ca
var(--bg-color) When pre
{/if}sent, only the Master of the Games has access to the game controlscan control the he t timerhis game.he Amesmn no l<!-- When theIf a master of the games is present, other pla cannot yers lo the games -->e
opacity: 40var(--bg-color)%;50cannot access the game's controls.F a ongera player start or stop the game/* . The
body {
background */
background-color:var(--bghighlight-color) white;
color: color: #33
0202;0c-colorbac
background-c, #magister_box:hover olor: red;ckcolor: red/* ; }
Magister Ludi will relinquish lo */scontrol thee oludotAscend to the throne ofher players are locked
Once somebody ascends to Magister Ludi status, other players can no longer start or stop the game.out of lousers mbecomesTheecome> mbe r' value={seconds_per_bead} on:input={config('seconds_per_
opacity: 40%;
transition: opacity 0.342 ease-outit
#magister_box.magister_opaque {
opacity: 1040%; }
in-out;1s; f
#magister_box:hover {
opacity: 100%; }
adeopaanimate:
opacity: 40%; opacity: 40%;bead ')}>
</div> </lab el>
<div style='margin-top:
#config {
margin-top: 2em;
} 1e m;'>
(Total game length: {round ish(
game_stages.reduce((x, s) => x + s.duration, 0) / 60
)} minu tes)
</d iv>
<button on:click={upd('_magister', 'true')}>Become Magister Ludi</but ton>
</details>
{:else}
<div>TOnly the Magisteris in control of this game</div>is in Ludi can Game controlled by WaiGamam,ag}
}
{/ifomplconst tick = (play_audio) => {/current_stage.type === 'complete'current/
c
<div>
JSON.stringify(stringify)_magistermagi Magister status: {}
</div> </div onso((internacontrolsorlsl_effectstat
<div>
This will drive the game for all players.
/***** Game config *****/
#config {
margin-top: 2em;
}
</div>
<div>
This will drive controlmodify thefor all players. game d
</div> e === 'playing' || internal_state === 'paused') && current_stagecur1
8#config {
margin-top: 2em;
}
/* **** Game config *****/ Conffi
#con
/* background-colo#1c0303r: white; */black;
background-bl
margin-top:
padding: 34px 0;3
padding: 200px auto;do;mar0;end-mode: da
mix-blend-mode: d
width: 100%;
margin: 0 2em;arken;blend-morken;blend-modefig {
margin-top: 2em;
}
rent_--interninterna font-size: 1080%;xcurrent_stage && !game_completed)game_complcomplinternal_internal_stateinternal_current_stage && !compl ? current_stag
#magister_box {
border: 1px dashed white;
margin: 1em 0;
padding:
max-width: 5300px;0.
#magister_box > button {
display: block;
}
> but52em;wdi2 }
.e.duration - le.lo// g( : '
<div>
This will drive
#config {
margin-top: 2em;
}
label {
display: block;
} the game for all players.
<
/div>
''tick')
con
'bead ' + roundsround' <div>{offset_secoffsets
<div id='pr
<span>ogres</span>scontainer'>tage_secs_remaining}</div> id=sole.log('x{statcurre', ntstasgte,agea// // // c 'ivi
if (state !== =.type === 'completeelte'loading== 'playing' || state === 'paused') tick(false)coympleted', sta
const order = ['mediPre-game meditationm tate', 'bead', 'complete']
const cla
<span>Meditatess_for = x =:> x < 0 ? 'done'
:x > 0 </span>? 'waiting'
: 'active'
(, t
tick(false)e// && ststage, ate.complete)
l || sstagetate === 'paused'et x = get_curren const curr
<span>ent_o = ortageder</span>.indexOf(scurrent_stage.type)
tconst ele
// if (m
const o_diff = element_o -< current_
type === 'bead' && stage.type === 'bead'
(r
<span>=== stage.r ? cl</span>ass_for(p - stage.p) : class_for(r - stage.r))class_forclass_for?
: class_for(elemen
// This will get more complex in time. For now,
<span>pause the game t</span>o fiddle.
$:let settings_disabledstate === 'playing' =
t_o - current_o)ty
relement_o !==- current_oeturn o_d style='margin-top: 1em;ing'iffelement_o - current_o !== 0 ? class_for(o_d
<button on:click={upd('_magisterxxsttrueyate', 'playBecome Magister Ludiing')}>Resume</button> iff)
: (class_for=== 0 )
return r === stage.r ? oo return r === stage.r ?
? ent_ocurrent_o = o
main { margin-bottom: 32em;
}
rdert
: class_for(0)class_forype.
if (element_o < current_o) return 'done'
else if (element_o > current if (type === 'bead') return r === stage.r ? =
elseo_o) return 'waiting'
else return 'active'picurrentelemcurren indexOf(current_stage.type)currenindeorderiif _ if cu
button {
font-size: 140%2
margin: 10px 0;00%; }
rrent_stagecurrenstate(Datcurre.nontw() g- start_time)
// state_label = state.label
offset_ms = x.off
// if (state !== 'load//
const progress_class = (type, r, p) => {
if (current_stage == null) return ''
current_gstate }
type
ing') tick(false)
tickset_ms
if (// x.game_
h1 {
margin-top: 12em;ri
ng }
#state !== game_state)
tick(false){
cocurrnsolnte.logg('
button {
font-size: 140%;
margin: 10px 0;
color: #330202; /* TODO: Use CSS variable for this */AS
}
state changed', x.game_state.lab15el, x.game_state.complete)
game_state = x.game_state
// completed// = new_game _statesec.complet
summary >margin-bottomtt3px label {
line-height: 2;lmargin }
{ e
min- //7 if 6(!state.
de435t
color: #330202;ails > :first-child:firmarginst-childb1ottom
input {
width: 103em
font-size: 16pxheight: 101.px3;;2emheight: }
: nt:fir > :first-child:f
display: inline-block;inlineirst lab25ellabelinpumin-width: 10em;width: t{
padding-top: 2em;
}
de/* tails {
padding- */top: 2em; }
{: ila
details > :not(:first-child):firfir* {
margin-left: 2em;efl }
margin: 25px;com
setTimeo
height: 5em;ut(se
position: relative;
border: 2px/* solid whit */e /* f
summary {
text-decoration: underlineu
cursor: pointergrabpointer;mopointer: nd;text-decorat }
ont-size: #330202; */
/* color: rgb(204,254,254); */
/* color: whit54er
margin-left: 5px1em;ef*/gb(204,254,254);
;font-sizeco60px;lor/* p positirgb(204,254,254)oin#330202r */gb(#330202: tTi
mix-blend-mode: differencediffere;mix-blenmeoutse
.bead {
margin-righ
padding: 40 2px;t: 12em43p
x21em;leftdisplay: block; }
t
Im mediate(()
} => ) setIm/* m
.waiting { */
color: #888;
e s)etTimetick(fals
font-s12ize: 60px;e)tickple
padding: 0 2px;-te) round_audio.play()
#progress_-tim {
position: absolute;
color: red; }
e
if (game_statecurrent_stage, .co class={progress_class('meditate')}mplete) {
// And play gong
// completed = true
// if (
i else if (=state === 'waiting') {
tick(false)tick }
f (stcurrate nt=== g'playcurri
lentt current_stage, tig class={progress_class('beadm, r, peditate')}mer
ng') tick(falsef)tickplay_audio) complete_audio.play()
}
curr}
}entte
g$: s
let last_state = null
const tick = (play_audio) => {
console.log('ti ck' )
consodisabled={settings_disabled}sett le.log('state', statxestate
/* && state.comple */
testate, 'completed', co
mp: new_offsget_ms leted)
let {game_statx.e: new_game_stax.te, offset_ms} = get_current_stcurrent_stage == null ? 0
: ate(
offset_ms = current_ne
w_offset_x.m
//
$: game_completed = curcurr disabled={settings_disabled}ent_rent_stage == null ? false : !!current_stage.complete
let bar_width = 0
$: bar_width =ge state.complete ? 100 : 100 disabled={settings_disabled} * offset_ms ge/ (state.duration * 1000)
g
let state_labegl
$: state_label = current_stage == null ? 'unknown' : current_stag disabled={settings_disabled}e.label
snew_ofDate.nnew_g.amex_state !== gow() - s tart_time )
// // x.setTimeout(() => state_la, n, 0)ew_game_state disabled={settings_disabled}.la1bel, new_gam con1st tick = (play_audio) => {
console.log('t ick ')
console.log('state', state, 'completed', state && state .compl ete)
let x = get_current_state(Date.no w() - sta rt_tim e)
// st ate _label = stat e.la be l
obar_ffset _ms = x.offs et _ms
if (x.game_sta te !== game_st ate) {
console.log('state ch anged', x.game_state.labe l , x.game_gstate.c omplete)
game_ state = x. game_state
// completed = new_game_st ate.complete
// if (!state. complete) round _ audio.text-decoration: line-throughlinestri;text-decplay(#888eee)
if (game_s tate.co border: 1px solid white; mplet e ) {
// And play gong
/ / comp leted = true
// if (play_aud io) c ompl ete_audio.play()
}
}
}e_state. game_state = new_game_state
complet
<!-- <div>
(Total game length: {roundish(
(rounds * players * seconds_per_bead + (meditate ? MEDITATION_SECONDS : 0)) / 60
)} minutes)
</div> -->enew_ga
.done { color: blue; }
.waiting { color: white; }
.activewaitingdonmagentamagpurplee { colwhiteor: blue; }lgame_stateabenew_gambnew_el =// s tate.labe l
if (last_stat e !== game_state) {
conso le.log('state changed'bar_ )
las
t_// state = game_stat e
game_state = new_g ame_stat e
com pleted = new_game_state.complet e
/
/ i
}f
(! state.complete ) round_audio.p lay2em( )
}
i // f (game_state.com// plete) ? {
// And&& : false play gon g
ga // complet ed = tr, game_stateu e
// // if (play_audio) complete_audio.play( )
}
}const tick = (play_audio) => {
}
!completed!
tate_l // Sadly we can't use internal_state here because it generates a cyclic dependancy.
let completed = game_state == null ? false : !!game_state.complete
abel = game_s'xx', tate == null, completed ? 'unknown' : game_state.labelgame_stateostate_l#: ? 100
: 100 * offset_ms / (sta
let completedgame_completed = game_state == null ? false : !!game_
consolecon state.completete.duration * 1000)#: coms}
_ms)ompletedc => {
for (let s =// 0; s < game_stag// es.le// ngth; s++) {
let state = // game_stages[s]
if (state.duration * 1000 > offset_ms) {
return {state, offset_ms// }
}
offset_ms -= state.duration * 1000
}
return {
state: // {
// We might// have l oadeompletedd wh
consolce.log('state', state, 'completed', game_completed)game_comp'timstatelogconso
completed = new_game_state.completenew_game_stategame_leetick = (play_audiono_soun d) => {
console.loggame_state: new_game_(' tick')
let {state, offset_ms} = get_current_state(Date.now() - star t_time)
//
game_state = new_game_statenew_gam state_label = stat e.labgame_ el
if (last_state !
ompleted game_state
console.log('cancelled interval timer')logconsoleo = new_game_statec== s tate) {
consolegame_.log('state ch anged')
// last_state = state
// if (game_!state.comompletedplete) round_audio .play()
}
c if (state.comp lete) {
// And pl ay gong
// game_completed = trif (play_audio) ue
complete_audio .play( )
tick(false)t ick } }
n the game i s a
tick(tr ue)ticklready finished. This is kin// da
// dirty, but I
const 'm special casing it to avoid audio playi// ng....
if (let {state} }
ci dirty, but Ifht gthTh.state.compl// ete) {
game_compl// eted // =
state_label = 'Game already finishe// d'. finishe// dstate_true else {gam e_// }
statee game m i
g// et
_current_state(Da// te.now() - start_time) l abel:round_ 'Game complete', complete: t// // rue }, offset_ms
}
}
let widt // h = 0
le// t timer
let state // _lab el
$: {
console.log(state, tim er)
// // Sadly we can't use internal_s// tate here b ecause it generates// a cyclic de pendanc// y.
if (state === 'playing ' && timer == nul// l &&
// audio1.play()aud
l// et last_state = null
// at e!game _completed// ) {
timer =
// // if (!sta// te.com p// lete// ) round_audio.pl
} ay(// )r // oundsetInterval(() => {
// consol // e.log('tick')
let // {state, offset_// ms}//
$: {
console.l// og(state, timer)
// Sadly we can't use internal_state here beca// use it generates a cyclic dependancy.
if (state === 'playing' && timer == // null && !game_completed) {
// We might have loaded when the // game is already finished. This is kinda
// dirty, but I'm special// casing it to avoid audio// playing.
if (get_current_state(Date.no// w() - start// _time).state.complete) {
// game_completed = true
stat// e_label = 'Game already f// inished'
} else {
let last_state = null
timer = setInterval(() // => {
console.log('tick')
l// et {state, offset_ms} = get_curre// nt_state(Date.now() - start_time)
state_label// = state.label
width = // state.complete ? 100
: 100 * o// ffset_ms / (state.durati// on * 1000)
if (last_state !== state) {
// consol// e.log('state changed')
// last_state = state
// // if (!state.complete)// round_audio.play()
}
// if (// state.comple// te) // {
// And play gong
game_completed = true
complete_audio.p// lay()
}
}, 100// 0)
}
} else // if // ((game_completed || state !== 'playing') && timer != null) {
clearInterval(timer)
timer = null
}
}= get_curr
completend_audio.play()end_aucompleent_state(Date.now() - start_time)
state_label = state.label
width = state.complete ? 100
: 100 * offset_ms / (
if (last_state !== state) {
console.log('state changed')
last_state = state
}
state.duration * 1000)
{
// And play gong
if (state.complete
} )
if (last_state && last_state !== state) {
console.log('state changed')
last_round_state = statestatelast_logconsole }
!= last_stlascomplett_ game_completed = true
}, 10end_00)
} else if ((game_completed || state !== 'playing') && timer != null) {
clearInterval(timer)
timer = null
}
}
$: {
if (internal_state =
<track kinaudio1{audio1}d b.lo./ind:this=="captions">
<audio id="audiotag1" srchi-metal-<trac
<audio bind:this={a2udio1} src="../hi-metal-tone.mp3" preload="auto"><track kind="captions"></audio>k kind="captions">tone.mp3_="audio/flute_c_long_01.
</track> </trackwav" preload="auto"></audi
o>== 'waiting') {
state_label = 'Waiting for game to start'
width = 0
}
}
$: {
console.log('internal state', internal_state)
}
// setInterval(() => width++, 1000)
{#if internal_state === 'loading'}
<h1>Loading game state from server</h1>int
{:else} } intern
</script>
<main>
<h1> Glass Bead Game Timer<details/h1>
<h4> R
<summary>Game sSettingsConfiguration</summary>>/summ
oom: <em>{room}</em></h4>
<h4>To pic: <em>{topic}</em></h4>
<div>{c onnection} / {state}</div>
<div>{_active_sessions } player(s) in room</div >
<h4>{state_label}</h4>
<d iv id='progresscontainer'>
<div id='progress' styl e='width: {width}%'></div>
</div>
<div id='rounds'>
<h2>Game struc ture</h2>
{#if meditate}
<di v>Medita tion (1 min)</div>
{/if}
{#each Array(Mat h.max(rounds, 0)) as _, r}
<div>Round {r+1}:
{#each Array(Math .max(players, 0)) as _, p }
<span >{p+1} </s pan>
{ /each}
</div>
{/each}
</div>
<div id=' config'>
<h2>Config</h2>
{#if in ternal_state == 'waiting'}
<button on:click={upd('state', 'playing')}>Start</button>
{:else if in ternal_state == 'playing'}
<button on:click={upd('state', 'paused')}>Pause</button>
{:els
e #if i internal_state == 'pausedcompleted' || nternal_state == 'paused'}
<button
{:else if internal_state == game'complete
{/if}d'} on:click={upd('state', ' playing')}>Resume</button>
{:els
<button on:click={upd('state', 'waiting')}>Reset</button>e if inte rnal_state == 'completed'}
<button on:click={upd('state', 'waiting' )}>Reset</butto n>
{/if}
<label>Topic
<input type='text' value={topic} on:input={config('top ic')} list='archetopics' >
<
<!-- <input type='range' value={players} on:submit={config('players')} min=1 max=12> --> datalist id='archetopics'<!-- >
{# each ARCHETOetailsPIdCS as topic}
<option value={topic}>
{/each}
<
<!-- <input type='range' value={players} on:sub -->mit={config('players')} min=1 max=12> -->/datalist>
</label>
<label>1 minute med itation before game starts
<input type='checkbox' checked={meditate} on
<!-- <input type='range' value={players} on:submit={config('players')} min=1 max=12> -->:i</style></style></style>n put={config('meditate')} >
<!-- <input type='range' value={players} on:submit={config('pl ayers')} min =1 max=12> -
<!-- <input type='range' value={players} on:submit={config('players')} min=1 max=12> -->->
</label>
<label>Number of players
<input type='number' value={players} on:input={config( 'players')} min=1 max=12 >
<!-- <input type='range' value={players} on:submit={config('pl ayers')} min =1 max=12> -->
</label>
<label>Number of rounds
<input type='number' value={rounds} on:input={confi g('rounds')} min=1 max=20>
<!-- <input type='range' value={players} on:submit={config('pl ayers')} min =1 max=12> -->
</label>
<label>Seconds per bead
<input type='number' value={seconds_per_bead} on:input={co nfig('seconds_per_bead')}>
<!-- <input type='range' value={players} on:submit={config('pl ayers')} min =1 max=1 2> -->
</label>
<div>
(Tot al game length: {roundish(
(rounds * players * seconds_per_bead + (meditate ? MED ITATION_SECOND
{/if}S : 0)) / 60
)} minutes)
</div>
<div>
(Tot al game length: {roundish(
game_stages.reduce((x, s) => x + s.durati on, 0) / 60
)} minutes)
</div>
</div>
</main>
<style>
#config {
margin-top: 2em;
}
label {
display: block;
}
#progresscontainer {
/* width: calc(100% - 50px); */
margin: 25px;
height: 5em;
border: 2px solid white;
}
#progress {
background-color: white;
/* width: 50%; */
height: 100%;
/* transition: width 1s linear; */
}
/* main {
text-align: center;
padding: 1em;
max-width: 240px;
margin: 0 auto;
}
h1 {
color: #ff3e00;
text-transform: uppercase;
font-size: 4em;
font-weight: 100;
}
@media (min-width: 640px) {
main {
max-width: none;
}
} */
</style><script>
import { start } from "repl";
import { arch } from "os";
import { kMa export let state
x// Length } frstart_timeom "buffer"
import { striroomn export let connarchetopicmeditate
export let meditateectionc
expor
let game_completedxxx = fa //Derived from other
// // let internal_sta// teintern $:properties
let internal_state = game_co$: {
if (state !== 'playing'{
console.log('xxx')logconsole
[
) game_completed =
} falsegame_ }
mpleted ? 'completed' : stategame_or lse
t let p export let rounds
layersonnectbead_rouned
beadgify
ex
const ARCHETOPICS = [
'Truth', 'Human', 'Energy', 'Beauty', 'Beginning', 'End', 'Birth', 'Death',
'Ego', 'Attention', 'Art', 'Empathy', 'Eutopia', 'Future', 'Game', 'Gift',
'History', 'Cosmos', 'Time', 'Life', 'Addiction', 'Paradox', 'Shadow', 'Society'
]
// Could make configurable. Eh.?
const MEDITATION_SECONDS = 60
let game_stagessata
$ game_stages = []
if (meditate) game_stages.push{
label: 'Meditate',
duration: MEDITATION_SECONDS,
})
for (let r = 0; r < rounds; r++) {
for (let prri = 0p; playersri < rou
+1 game_stages+1.push({
label: `Round $#{r} player$ p}`,#{'
duration: seconds_per_bead,second })
const get_current_statstarttime_of_msfsete = let game_durations = 0
while start_offset_msstart_ofstart;()
let fo let state.duration * 1000 > offset_ms) {
return {state, offset_ms}offs
offset_ms -= state.duration * 1000off
return {
state: { label: 'Game complete' }, offset_ms
} off state: {
label: 'Game complete',et
} offsestatestate = game_stages[s]
if (game_str (let s = 0; s < game_stages.length; s++) {
}
igamei=> {
}
pushisgame_
} ndps; ri++) {
}
for (let i = 0} rou MEDIebl(pushgamemedigame_: {
}
const get_porst
const ARCHETOPICS = [
'Truth', 'Human', 'Energy', 'Beauty', 'Beginning', 'End', 'Birth', 'Death',
'Ego', 'Attention', 'Art', 'Empathy', 'Eutopi
const get_current_state = (offset_ms) => {
for (let s = 0; s < game_stages.length; s++) {
let state = game_stages[s]
if (state.duration * 1000 > offset_ms) {
return {state, offset_ms}
}
offset_ms -= state.duration * 1000
}
return {
state: { lab0el: 'Game complete', complete// = 'Waiting for game to start'o host to start: true }, offset_ms
}
}
a', 'Future', 'Game', 'Gif
let state_label //gastate_progress
// Sadly we can't use internal_state here because it generates a cyclic dependancy.g// 0 to 1
let xxx = falsealme_state,intern && !game_completedgame_al_stateintern state_offset_msgame_, labelabelt',
'Histor!game_completedgame_comply', 'Cosmos', 'Tim && xxx === false& e', 'Life', 'Addiction', 'Paradox', 'Shadow', 'Society'
]
atetconnecte
export let colet ;nnectedd else else {
state_label = 'Waiting for game to start'
width }= 0
e: game_stategesgame_x
=}
}
waiting
$: {
if (internal_state !== 'playing') { 'pintern po: s state_label = 'Waiting for game to start'
width = 0
tate_offset_m// sstatestate.la1000bel
if (state.c state.(game_completed || completestate_label = 'Waiting for game to st
width = 0art'state_ ? 100
: || game_completed)game_&& sta
state_label= teomplete) state
width internal_stateinternal= 100 * ogame_completedfame_complf
g
if (state.complete) xxx = tr
$: {
console.log('internal state', internal_state)internlogconsole
} uestate set_ms / (game_completedsame_compltat(eg.duration * 1000) || xxx )offset25035_msoff_otate_labelffrt
sgame_state = state; state_offset_ms = offset_ms
offstate_oftlet {state, offset_ms} = get_current_state(Date.now( ) - start_time)
start_tnowDateget_curplayersvalue
exportrounds export let second = nulls_per_round
let value
const update_state = async patch => {
await await `${room}/configure`, '
le
<h4>{state_label}</h4>2state_gt timer
$: {
console.log(state, timer)statelogconsole
if (state === 'playing' && timer == null) {
timer = setInterconsointernal_le.log('tick')
let
val(() => {
console.log('tick')logconsole// else if (stateinternal_ !== 'playing' && timer != null) {
clearInterval(timer)
timer = nullclearInt }
}, 1internal_000)00
setInt// }
}
4:/{
method: 'POST',
mode: 'same-origin',
headers: {
internal_ 'contengame_completedt-type': 'application/json',
},
bowaitingd, v)y: JSOResetN.stringify(pat
{[k]: v}co
const config = k => e => {
console.log('k', k, {:else if state == 'paused'}
<button on:click={upd('state', 'playing')}>Resume</button>
{/if}
e, e.data, e.va, e.target.va lu e, e.target.typel
const raw_value = e.target.value+
co
{/if}nst value = e.target.type === 'number' ?raw
_ve.target.type ===
const roundish_tox => Math.round(roundx ) / 100
20(* 100 = ( 'checkbox' ? e.target.checked
: alue e.taraw_valueget.value|0 :r e.targvalueet.valueuedv
// update_state({target.value[k]: e.data})k, updaalue)logconsole }
(e)nst upd = (kk
// letconst _arr
// $: round_arrround_:$$:rounds = new Math.max(
let width = 50
// setInterval(() => width++, 1000)
letconst width
setInterval(() => width++
<h4>TopicRoom: <archetopicarchiem>{room}</em></h4>, 1000)se <div>{connection} / {state}</div>
tIn = 5_active_sessions_ player(s) in roomsesacti_0maxinr, 0)ounds).fill()fill.fill().mapfillArray(Arrayt
MaxLength, v)k(patc => () => update_state(patch)update> hpatc
<div id='progresscontainerpr
<div id='progress' style=':={{w: {width}i=dth: ' (1 min)50%'}}"></div>
ogress'></div>c> h = ch// )
) } stri
<h2>Game structure</h2>
{#if meditate}
<di
<div>
Total groundish(roundishame length: {((rounds * players +(meditate ? 1 : 0)) * )seconds_per_bead / 60} minutessecondmedit * ${ (includin
</div>v>Meditation</div>
{/if}[<medingifyhJSONd// ,sArray(Math.max(maxrou, 0)nds)Arraryademethd: each rround_ar {#each Array(Math.max(playersrounds, 0)pT) as
<spap+1} n>{</sp an>
{/each}>< _, i}r as _, i}
+1 :
<div>Round {i}</div>
{/each}ea fetch(
<div i
/* transition: width 10.21s lin */ear; d='rounds'>
{#f
</div> </div }//
(pat
update_state({state: 'play// ing'})updach)Archetopic =
const start = () => {
console.log list='archetopics'arch('
<datalist id='archetopics'>checked
{#each ARCH ETOPICS as topic}
<option value={topic}>
{/each}>/?ARCH<op
</datalist>cisstart!')logctextonsole }
archetopicarchetarch
e_activarchetopicpe_sessions
<h1>Glass Bead Game
Timers</h'1>seh',4cs_pe:r<em>_round</em
{#if state == 'waiting'
}
1 minute mMeditaionate before gam
<label>1 minute meditation before game starts
<input type='chec <div>
(Total game length: {roundish(
(rounds * players * seconds_per_bead + (meditate ? MEDITATION_SECONDS : 0)) / 60
)} minutes)
</div>
kbox' va (Total game length: {roundish(
game_stages.reduce((x, s) => x + s.duration, 0)game(rounds * players * seconds_per_bead + (meditate ? MEDITATION_SECONDS : 0)) / 60
)} minutes)
lue={meditate} on:input={config('meditate')} >
<!-- <input type='range' value={players} on:submit={config
('players')} min= + (meditate ? 1 : + + (meditateMEDITATION_SECONDSMEDI ? 1 :
0))0))1 max=12> -->
</l + MEDIabel>e stsart upd<butto'checkbox'nmeditate on:click={() => meditateupdate_'state,'({state: 'playing'})start}>Start</but {:else if state pd== 'playing'}
u<button on':cli,c'k={ <div>
<label>Number of players
<input type='number
'
<div>(
Total game length: {roundish((rounds * players + (meditate ? 1 : 0)) * seconds_per_bead / 60)} min)utes
</div>
v
<lab
el>Number of <label>Number of rounds
<input type='number' value={rounds} on:input={config('rounds')} min=1 max=2012>
<!-- <input type='range' value={players} on:submit={config('players')} min=1 max=12> -->
</label>
players
econds per beads S <input type='number' valuseconds_per_beadsecone={players} on:seconds_per_beadinput={config('players')} min=1 max=12 >
<!-- <input type='range' value={players} on:submit={config('players')} min=1 max=12> -->
</label>alue= {players} on:inputblur ={c onfig( min=1 max=12'players ')} min=1 max=12>
<!-- <input type='range' value={players} on:submit={config(' players')} min=1 max=12> -->
</label>
</div>
() => up
#progresscont/* ainer {
width: calc(100% */ - 520px);
height:25 3em;
border54em: 1px s
h2eight: 3
#progress {
background-color: white;
/* background- */color: white;
height: 100%;width: 50%;borderwidth: }
em;olid white;
marginpadding
border: 1px solid white;margin: 10px;: }
date_state({state:rounds 'rpause d'})}>P <!-- <input tyroundspenput='niumber' value={poundslablurryersubmits} on:inpu
label {
display: block; }
t={config('play min=1 max=12ers')}> -->
aus<!-- e<!-- </butto
range n>pd
{ value={submitplayers}:else if state == min=1 m -->ax=12oin'pausconfig( -->'players')in
configedpl on:input={()}ayinug'}
ton>
{:else if state == 'playing'}
playingresumResumee
{/ifii{upd(upd() =>
<div>
</div><label>Number of players<input type='n on:input=""on:inpbind:valuumber'></
</div>label>>/l update_state({state: 'pausedplayiPauseng'})fif#if>xport
<div> connected
:else if state == 'playing'#elif {else}elel# {c/onnecti}
{/ifon} sta
<div id='config'>
<h2>Config</h2>
<button on:click={start}>Start</button>/""on:cliu
</div><div id=bar te {st
#config {
margin-top: 2em;1m }
ate</div>>}spa let valueconnection
let state
<hh14>Ro state {state}om {
body {
background-color: black; }
ro/* om} connected {connected}</h1>;
evaluexpo// rt let state;
export let state;} from "querystring";
export let name;
export let statename;
</script>
<main>
<h1>Hello {name}!</<pre>{JSON.stringify(state)}</pre>stringifyJSONh1>
<p>Visit the */<a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p>
</main>
<style>
main {
text-align: center;
padding: 1em;
max-width: 240px;
margin: 0 auto;
}
h1 {
color: #ff3e00;
text-transform: uppercase;
font-size: 4em;
font-weight: 100;
}
@media (min-width: 640px) {
main {
max-width: none;
}
}
</style><script lang="ts">
import {onMount} from 'svelte';
let count: number = 0;
onMount(() => {
const interval = setInterval(() => count++, 1000);
return () => {
clearInterval(interval);
};
});
</script>
<style>
:global(body) {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
}
.App {
text-align: center;
}
.App code {
background: #0002;
padding: 4px 8px;
border-radius: 4px;
}
.App p {
margin: 0.4rem;
}
.App-header {
background-color: #f9f6f6;
color: #333;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
}
.App-link {
color: #ff3e00;
}
.App-logo {
height: 36vmin;
pointer-events: none;
margin-bottom: 3rem;
animation: App-logo-spin infinite 1.6s ease-in-out alternate;
}
@keyframes App-logo-spin {
from {
transform: scale(1);
}
to {
transform: scale(1.06);
}
}
</style>
<div class="App">
<header class="App-header">
<img src="/logo.svg" class="App-logo" alt="logo" />
<p>Edit <code>src/App.svelte</code> and save to reload.</p>
<p>Page has been open for <code>{count}</code> seconds.</p>
<p>
<a class="App-link" href="https://svelte.dev" target="_blank" rel="noopener noreferrer">
Learn Svelte
</a>
</p>
</header>
</div>
<script lang="ts">
import {onMount} from 'svelte';
let count = 0;
onMount(() => {
const interval = setInterval(() => count++, 1000);
return () => {
clearInterval(interval);
};
});
</script>
<style>
:global(body) {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
}
.App {
text-align: center;
}
.App code {
background: #0002;
padding: 4px 8px;
border-radius: 4px;
}
.App p {
margin: 0.4rem;
}
.App-header {
background-color: #f9f6f6;
color: #333;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
}
.App-link {
color: #ff3e00;
}
.App-logo {
height: 36vmin;
pointer-events: none;
margin-bottom: 3rem;
animation: App-logo-spin infinite 1.6s ease-in-out alternate;
}
@keyframes App-logo-spin {
from {
transform: scale(1);
}
to {
transform: scale(1.06);
}
}
</style>
<div class="App">
<header class="App-header">
<img src="/logo.svg" class="App-logo" alt="logo" />
<p>Edit <code>src/App.svelte</code> and save to reload.</p>
<p>Page has been open for <code>{count}</code> seconds.</p>
<p>
<a class="App-link" href="https://svelte.dev" target="_blank" rel="noopener noreferrer">
Learn Svelte
</a>
</p>
</header>
</div>