Skip to content

Commit 2f56632

Browse files
committed
add seeds and icons
1 parent 35e7a8f commit 2f56632

9 files changed

Lines changed: 230 additions & 17 deletions

File tree

via-ui/package-lock.json

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

via-ui/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
},
1717
"dependencies": {
1818
"@duckdb/duckdb-wasm": "^1.33.1-dev45.0",
19+
"@fortawesome/fontawesome-svg-core": "^7.2.0",
20+
"@fortawesome/free-solid-svg-icons": "^7.2.0",
21+
"@fortawesome/vue-fontawesome": "^3.2.0",
1922
"@tailwindcss/vite": "^4.3.0",
2023
"d3": "^7.9.0",
2124
"tailwindcss": "^4.3.0",

via-ui/src/api/game/manager.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface GameState {
2424
currentDistance: number | null
2525
shapePreview: ShapePoint[]
2626
stopPreview: Stop | null
27+
seed: String | null
2728
}
2829

2930
const state = ref<GameState>({
@@ -37,6 +38,7 @@ const state = ref<GameState>({
3738
currentDistance: null,
3839
shapePreview: [],
3940
stopPreview: null,
41+
seed: null,
4042
})
4143

4244
const service = new Service('/via/data')
@@ -67,14 +69,25 @@ function computeDistanceToTarget(stop: Stop) {
6769
return Math.round(Math.sqrt(x * x + y * y))
6870
}
6971

72+
function generateSeed(): string {
73+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
74+
let s = ''
75+
for (let i = 0; i < 6; i++) s += chars[Math.floor(Math.random() * chars.length)]
76+
return s
77+
}
78+
79+
7080
export function useGameManager() {
7181
const gameState = readonly(state)
7282

73-
async function startGame() {
83+
async function startGame(gameSeed?: string) {
84+
state.value.seed = gameSeed ?? generateSeed()
85+
const startSeed = state.value.seed + ':start'
86+
const stopSeed = state.value.seed + ':stop'
7487
await service.init()
7588
state.value.currentTime = START_DATE
76-
state.value.startStation = await service.getRandomStop()
77-
state.value.targetStation = await service.getRandomStop()
89+
state.value.startStation = await service.getRandomStop(startSeed)
90+
state.value.targetStation = await service.getRandomStop(stopSeed)
7891
state.value.currentStop = state.value.startStation
7992
state.value.departures = await service.getUpcomingDepartures(
8093
state.value.startStation.stop_id,

via-ui/src/api/gtfs/service.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,43 @@ export class Service {
4040
// --------------------------------------------------------------------------
4141

4242
/** Picks a random physical stop (excludes parent stations and entrances). */
43-
async getRandomStop(): Promise<Stop> {
43+
async getRandomStop(seed?: string): Promise<Stop> {
4444
await this.init()
45-
const rows = await this.select<Stop>(`
45+
46+
const whereClause = `
47+
WHERE COALESCE(NULLIF(TRIM(location_type), ''), '0') = '0'
48+
AND CAST(stop_lat AS DOUBLE) BETWEEN 45.8 AND 47.8
49+
AND CAST(stop_lon AS DOUBLE) BETWEEN 5.9 AND 10.5
50+
`
51+
52+
if (seed === undefined) {
53+
const rows = await this.select<Stop>(`
4654
SELECT
47-
stop_id,
48-
stop_name,
55+
stop_id, stop_name,
4956
CAST(stop_lat AS DOUBLE) AS stop_lat,
5057
CAST(stop_lon AS DOUBLE) AS stop_lon
5158
FROM stops
52-
WHERE COALESCE(NULLIF(TRIM(location_type), ''), '0') = '0'
53-
AND CAST(stop_lat AS DOUBLE) BETWEEN 45.8 AND 47.8
54-
AND CAST(stop_lon AS DOUBLE) BETWEEN 5.9 AND 10.5
59+
${whereClause}
5560
ORDER BY RANDOM()
56-
LIMIT 1
61+
LIMIT 1
5762
`)
58-
return rows[0]!!
63+
return rows[0]!
64+
}
65+
66+
const rows = await this.prepareAndQuery<Stop>(
67+
`
68+
SELECT
69+
stop_id, stop_name,
70+
CAST(stop_lat AS DOUBLE) AS stop_lat,
71+
CAST(stop_lon AS DOUBLE) AS stop_lon
72+
FROM stops
73+
${whereClause}
74+
ORDER BY hash(stop_id || ?)
75+
LIMIT 1
76+
`,
77+
[seed],
78+
)
79+
return rows[0]!
5980
}
6081

6182
/** Looks up a single stop by id. Returns null if not found. */

via-ui/src/components/Departure.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ onUnmounted(() => {
7474

7575
<div class="collapse-content text-sm z-1">
7676
<button class="btn btn-primary btn-outline w-full" @click="departureSelected()">
77+
<FontAwesomeIcon icon="arrow-right-to-bracket" class="w-5 h-5" />
7778
Board this train
7879
</button>
7980
</div>

via-ui/src/components/SideBar.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ watch(departureChosen, async () => {
3939

4040
<h2 v-else class="text-2xl font-bold">
4141
<p class="text-xs tracking-wide uppercase opacity-60 font-semibold mb-1 truncate">
42+
<FontAwesomeIcon icon="location-dot" class="w-5 h-5" />
4243
Current location
4344
</p>
4445

@@ -50,6 +51,7 @@ watch(departureChosen, async () => {
5051

5152
<div v-if="departureChosen" class="w-full min-w-0">
5253
<p class="text-xs tracking-wide uppercase opacity-60 font-semibold mb-1 truncate">
54+
<FontAwesomeIcon icon="train" class="w-5 h-5" />
5355
Current train
5456
</p>
5557
<div class="text-lg font-bold flex items-center">

via-ui/src/components/Stop.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ onUnmounted(() => {
6666

6767
<div class="collapse-content text-sm z-1">
6868
<button class="btn btn-outline btn-secondary w-full" @click="stopSelected()">
69+
<FontAwesomeIcon icon="arrow-right-from-bracket" class="w-5 h-5" />
6970
Get off here
7071
</button>
7172
</div>

via-ui/src/components/TopBar.vue

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,31 @@ import { ref } from 'vue'
55
66
const { gameState, startGame } = useGameManager()
77
const loading = ref(false)
8+
const seedInput = ref('')
9+
const copied = ref(false)
810
911
async function start() {
1012
loading.value = true
1113
await startGame()
1214
loading.value = false
1315
}
16+
17+
async function copySeed() {
18+
if (!gameState.value.seed) return
19+
await navigator.clipboard.writeText(String(gameState.value.seed))
20+
copied.value = true
21+
setTimeout(() => (copied.value = false), 1200)
22+
}
23+
24+
async function applySeed() {
25+
const newSeed = seedInput.value.trim()
26+
if (!newSeed) return
27+
;(document.getElementById('multiplayerModal') as HTMLDialogElement)?.close()
28+
seedInput.value = ''
29+
loading.value = true
30+
await startGame(newSeed)
31+
loading.value = false
32+
}
1433
</script>
1534

1635
<template>
@@ -20,10 +39,78 @@ async function start() {
2039
<p class="py-4">Here, you will find explanation on how to play the game</p>
2140
<div class="modal-action">
2241
<form method="dialog">
23-
<button class="btn btn-soft btn-primary">Close</button>
42+
<button class="btn btn-soft btn-primary">
43+
<FontAwesomeIcon icon="xmark" class="w-5 h-5" />
44+
Close
45+
</button>
46+
</form>
47+
</div>
48+
</div>
49+
</dialog>
50+
51+
<dialog id="multiplayerModal" class="modal">
52+
<div class="modal-box">
53+
<h3 class="text-lg font-bold">Multiplayer</h3>
54+
<p class="py-2 text-sm opacity-70">
55+
Every game has a seed. Two players using the same seed get the same start station and the
56+
same target, so you can race a friend on identical journeys. Share your seed to challenge
57+
someone, or enter theirs below to join their game.
58+
</p>
59+
60+
<!-- Share your current seed -->
61+
<div class="mt-4">
62+
<label class="label pb-1">
63+
<span class="label-text font-bold">Your current seed</span>
64+
</label>
65+
<div class="join w-full">
66+
<input
67+
type="text"
68+
class="input input-bordered join-item flex-1 font-mono text-center"
69+
:value="gameState.seed ?? '—'"
70+
readonly
71+
aria-label="Your current seed"
72+
/>
73+
<button class="btn join-item" @click="copySeed" :disabled="!gameState.seed">
74+
<FontAwesomeIcon :icon="copied ? 'check' : 'copy'" class="w-5 h-5" />
75+
{{ copied ? 'Copied' : 'Copy' }}
76+
</button>
77+
</div>
78+
</div>
79+
80+
<div class="mt-4">
81+
<label class="label pb-1">
82+
<span class="label-text font-bold">Join a game</span>
83+
</label>
84+
<input
85+
v-model="seedInput"
86+
type="text"
87+
class="input input-bordered w-full font-mono"
88+
placeholder="Enter a seed"
89+
@keyup.enter="applySeed"
90+
/>
91+
<label class="label pt-1">
92+
<span class="label-text-alt opacity-60">
93+
Applying restarts the game with this seed.
94+
</span>
95+
</label>
96+
</div>
97+
98+
<div class="modal-action">
99+
<form method="dialog">
100+
<button class="btn btn-joined">
101+
<FontAwesomeIcon icon="xmark" class="w-5 h-5" />
102+
Cancel
103+
</button>
24104
</form>
105+
<button class="btn btn-primary" @click="applySeed" :disabled="!seedInput.trim()">
106+
<FontAwesomeIcon icon="check" class="w-5 h-5" />
107+
Apply & restart
108+
</button>
25109
</div>
26110
</div>
111+
<form method="dialog" class="modal-backdrop">
112+
<button>close</button>
113+
</form>
27114
</dialog>
28115

29116
<div
@@ -60,22 +147,29 @@ async function start() {
60147
<div
61148
class="w-full lg:w-auto flex flex-row gap-2 justify-center lg:justify-end max-w-md lg:max-w-none mx-auto lg:mx-0 shrink-0"
62149
>
150+
<button
151+
class="btn btn-secondary btn-soft flex-1 lg:flex-initial font-bold lg:px-6"
152+
onclick="multiplayerModal.showModal()"
153+
>
154+
<FontAwesomeIcon icon="users" class="w-5 h-5" />
155+
Multiplayer
156+
</button>
63157
<button
64158
class="btn btn-secondary btn-soft flex-1 lg:flex-initial font-bold lg:px-6"
65159
onclick="helpModal.showModal()"
66160
>
67-
How to play?
161+
<FontAwesomeIcon icon="circle-question" class="w-5 h-5" />
162+
How to play
68163
</button>
69164
<button
70165
class="btn btn-primary btn-soft flex-1 lg:flex-initial font-bold lg:px-6"
71166
@click="start()"
72167
:disabled="loading"
73168
>
74169
<span v-if="loading" class="loading loading-spinner loading-xs"></span>
170+
<FontAwesomeIcon icon="arrow-rotate-left" class="w-5 h-5" />
75171
Restart
76172
</button>
77173
</div>
78174
</div>
79175
</template>
80-
81-
<style scoped></style>

via-ui/src/main.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,40 @@
11
import { createApp } from 'vue'
22
import App from './App.vue'
33
import './style.css'
4+
import { library } from '@fortawesome/fontawesome-svg-core'
5+
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
6+
import {
7+
faCopy,
8+
faCheck,
9+
faPenToSquare,
10+
faUsers,
11+
faXmark,
12+
faArrowRightToBracket,
13+
faArrowRightFromBracket,
14+
faTrain,
15+
faLocationDot,
16+
faCircleQuestion,
17+
faArrowRotateLeft,
18+
} from '@fortawesome/free-solid-svg-icons'
19+
20+
library.add(
21+
faCopy,
22+
faCheck,
23+
faPenToSquare,
24+
faUsers,
25+
faXmark,
26+
faArrowRightToBracket,
27+
faArrowRightFromBracket,
28+
faTrain,
29+
faLocationDot,
30+
faCircleQuestion,
31+
faArrowRotateLeft,
32+
)
33+
34+
const app = createApp(App)
35+
app.component('FontAwesomeIcon', FontAwesomeIcon)
36+
app.mount('#app')
437

5-
createApp(App).mount('#app')
638
//import { GtfsService } from '@/api/gtfs/gtfsService.ts'
739
//
840
// const gtfs = new GtfsService('/via/data')

0 commit comments

Comments
 (0)