tangled
alpha
login
or
join now
eldridge.cam
/
cartography
0
fork
atom
Trading card city builder game?
0
fork
atom
overview
issues
pulls
pipelines
set up concept of game state watcher
eldridge.cam
1 month ago
8c36b7cc
66e78c1c
+183
-34
10 changed files
expand all
collapse all
unified
split
api
src
cartography_api
game_state.gleam
docker-compose.yml
server
src
actor
game_state_watcher.gleam
handlers
unsubscribe_handler.gleam
watch_field_handler.gleam
server
context.gleam
server.gleam
websocket
handler.gleam
state.gleam
src
lib
appserver
socket
SocketV1.svelte.ts
+6
-4
api/src/cartography_api/game_state.gleam
···
41
41
}
42
42
43
43
pub type Field {
44
44
-
Field(id: FieldId, tiles: List(FieldTile), citizens: List(FieldCitizen))
44
44
+
Field(tiles: List(FieldTile), citizens: List(FieldCitizen))
45
45
}
46
46
47
47
pub type FieldTile {
···
50
50
51
51
pub type FieldCitizen {
52
52
FieldCitizen(id: CardId, x: Int, y: Int)
53
53
+
}
54
54
+
55
55
+
pub fn new() {
56
56
+
GameState(deck: Deck(cards: []), field: Field(tiles: [], citizens: []))
53
57
}
54
58
55
59
pub fn to_json(game_state: GameState) -> Json {
···
95
99
#(
96
100
"field",
97
101
json.object([
98
98
-
#("id", json.int(game_state.field.id.id)),
99
102
#(
100
103
"tiles",
101
104
game_state.field.tiles
···
176
179
decode.success(Deck(cards:))
177
180
})
178
181
use field <- decode.field("field", {
179
179
-
use id <- decode.field("id", decode.map(decode.int, FieldId))
180
182
use tiles <- decode.field(
181
183
"tiles",
182
184
decode.list({
···
195
197
decode.success(FieldCitizen(id:, x:, y:))
196
198
}),
197
199
)
198
198
-
decode.success(Field(id:, tiles:, citizens:))
200
200
+
decode.success(Field(tiles:, citizens:))
199
201
})
200
202
decode.success(GameState(deck:, field:))
201
203
}
+1
-1
docker-compose.yml
···
25
25
image: glitchtip/glitchtip
26
26
container_name: glitchtip
27
27
depends_on:
28
28
-
- postgres
28
28
+
- postgres
29
29
ports:
30
30
- "8000:8000"
31
31
environment:
+54
server/src/actor/game_state_watcher.gleam
···
1
1
+
import bus
2
2
+
import cartography_api/game_state
3
3
+
import gleam/erlang/process
4
4
+
import gleam/otp/actor
5
5
+
import mist
6
6
+
import youid/uuid
7
7
+
8
8
+
pub type Init {
9
9
+
Init(
10
10
+
bus: bus.Bus,
11
11
+
conn: mist.WebsocketConnection,
12
12
+
message_id: uuid.Uuid,
13
13
+
account_id: String,
14
14
+
field_id: game_state.FieldId,
15
15
+
)
16
16
+
}
17
17
+
18
18
+
type State {
19
19
+
State(init: Init, game_state: game_state.GameState)
20
20
+
}
21
21
+
22
22
+
pub type Message {
23
23
+
CardCreated(card_id: game_state.CardId)
24
24
+
Stop
25
25
+
}
26
26
+
27
27
+
pub fn start(init: Init) {
28
28
+
actor.new_with_initialiser(50, fn(sub) {
29
29
+
let selector =
30
30
+
process.new_selector()
31
31
+
|> process.select(sub)
32
32
+
|> process.select_map(
33
33
+
bus.on_card_account(init.bus, init.account_id),
34
34
+
CardCreated,
35
35
+
)
36
36
+
State(init:, game_state: game_state.new())
37
37
+
|> actor.initialised()
38
38
+
|> actor.selecting(selector)
39
39
+
|> actor.returning(sub)
40
40
+
|> Ok()
41
41
+
})
42
42
+
|> actor.on_message(handle_message)
43
43
+
|> actor.start()
44
44
+
}
45
45
+
46
46
+
fn handle_message(state: State, message: Message) -> actor.Next(State, Message) {
47
47
+
case message {
48
48
+
CardCreated(_card_id) -> {
49
49
+
// TODO: include the card in the deck?
50
50
+
actor.continue(state)
51
51
+
}
52
52
+
Stop -> actor.stop()
53
53
+
}
54
54
+
}
+12
server/src/handlers/unsubscribe_handler.gleam
···
1
1
+
import mist
2
2
+
import websocket/state
3
3
+
import youid/uuid
4
4
+
5
5
+
pub fn handle(
6
6
+
st: state.State,
7
7
+
message_id: uuid.Uuid,
8
8
+
) -> Result(mist.Next(state.State, _msg), String) {
9
9
+
state.unsubscribe(st, message_id)
10
10
+
|> mist.continue()
11
11
+
|> Ok()
12
12
+
}
+34
server/src/handlers/watch_field_handler.gleam
···
1
1
+
import actor/game_state_watcher
2
2
+
import cartography_api/game_state
3
3
+
import gleam/erlang/process
4
4
+
import gleam/result
5
5
+
import gleam/string
6
6
+
import mist
7
7
+
import websocket/state
8
8
+
import youid/uuid
9
9
+
10
10
+
pub fn handle(
11
11
+
st: state.State,
12
12
+
conn: mist.WebsocketConnection,
13
13
+
message_id: uuid.Uuid,
14
14
+
field_id: game_state.FieldId,
15
15
+
) -> Result(mist.Next(state.State, _msg), String) {
16
16
+
use account_id <- state.account_id(st)
17
17
+
{
18
18
+
use field_watcher <- result.try(state.start_game_state_watcher(
19
19
+
st,
20
20
+
conn,
21
21
+
message_id,
22
22
+
account_id,
23
23
+
field_id,
24
24
+
))
25
25
+
26
26
+
st
27
27
+
|> state.add_subscription(message_id, fn() {
28
28
+
process.send(field_watcher.data, game_state_watcher.Stop)
29
29
+
})
30
30
+
|> mist.continue()
31
31
+
|> Ok()
32
32
+
}
33
33
+
|> result.map_error(string.inspect)
34
34
+
}
+17
-2
server/src/server.gleam
···
1
1
+
import actor/game_state_watcher
1
2
import bus
2
3
import envoy
3
4
import gleam/erlang/process
4
5
import gleam/int
6
6
+
import gleam/otp/factory_supervisor
5
7
import gleam/otp/static_supervisor
6
8
import gleam/result
7
9
import mist
···
21
23
let port =
22
24
envoy.get("PORT")
23
25
|> result.try(int.parse)
24
24
-
|> result.unwrap(12000)
26
26
+
|> result.unwrap(12_000)
25
27
26
28
let db_name = process.new_name("database")
27
29
let assert Ok(database_url) = envoy.get("DATABASE_URL")
···
33
35
34
36
let #(bus_process, bus_handles) = bus.supervised()
35
37
36
36
-
let context = context.Context(db_name, bus_handles)
38
38
+
let game_state_watcher_supervisor_name =
39
39
+
process.new_name("game_state_watcher_supervisor")
40
40
+
let factory =
41
41
+
factory_supervisor.worker_child(game_state_watcher.start)
42
42
+
|> factory_supervisor.named(game_state_watcher_supervisor_name)
43
43
+
|> factory_supervisor.supervised()
44
44
+
45
45
+
let context =
46
46
+
context.Context(
47
47
+
db_name,
48
48
+
bus_handles,
49
49
+
game_state_watchers: game_state_watcher_supervisor_name,
50
50
+
)
37
51
let server =
38
52
mist.new(router.handler(_, context))
39
53
|> mist.port(port)
···
43
57
static_supervisor.new(static_supervisor.OneForOne)
44
58
|> static_supervisor.add(database)
45
59
|> static_supervisor.add(bus_process)
60
60
+
|> static_supervisor.add(factory)
46
61
|> static_supervisor.add(server)
47
62
|> static_supervisor.start()
48
63
+13
-2
server/src/server/context.gleam
···
1
1
+
import actor/game_state_watcher
1
2
import bus
2
2
-
import gleam/erlang/process.{type Name}
3
3
+
import gleam/erlang/process.{type Name, type Subject}
4
4
+
import gleam/otp/factory_supervisor
3
5
import pog
4
6
5
7
pub type Context {
6
6
-
Context(db: Name(pog.Message), bus: bus.Bus)
8
8
+
Context(
9
9
+
db: Name(pog.Message),
10
10
+
bus: bus.Bus,
11
11
+
game_state_watchers: Name(
12
12
+
factory_supervisor.Message(
13
13
+
game_state_watcher.Init,
14
14
+
Subject(game_state_watcher.Message),
15
15
+
),
16
16
+
),
17
17
+
)
7
18
}
+5
-6
server/src/websocket/handler.gleam
···
8
8
import handlers/authenticate_handler
9
9
import handlers/debug_add_card_handler
10
10
import handlers/list_fields_handler
11
11
+
import handlers/unsubscribe_handler
12
12
+
import handlers/watch_field_handler
11
13
import json_websocket
12
14
import mist.{type WebsocketConnection}
13
15
import palabres
···
24
26
request.Authenticate(id) ->
25
27
authenticate_handler.handle(state, conn, message.id, id)
26
28
request.ListFields -> list_fields_handler.handle(state, conn, message.id)
27
27
-
request.WatchField(_) -> {
28
28
-
todo
29
29
-
}
29
29
+
request.WatchField(field_id) ->
30
30
+
watch_field_handler.handle(state, conn, message.id, field_id)
30
31
request.DebugAddCard(card_id) ->
31
32
debug_add_card_handler.handle(state, card_id)
32
32
-
request.Unsubscribe -> {
33
33
-
todo
34
34
-
}
33
33
+
request.Unsubscribe -> unsubscribe_handler.handle(state, message.id)
35
34
}
36
35
}
37
36
case response {
+40
-18
server/src/websocket/state.gleam
···
1
1
+
import actor/game_state_watcher
1
2
import bus
2
2
-
import gleam/dict
3
3
-
import gleam/option
3
3
+
import cartography_api/game_state
4
4
+
import gleam/dict.{type Dict}
5
5
+
import gleam/option.{type Option}
6
6
+
import gleam/otp/factory_supervisor
7
7
+
import mist
4
8
import pog
5
5
-
import server/context
9
9
+
import server/context.{type Context}
10
10
+
import youid/uuid.{type Uuid}
6
11
7
12
pub opaque type State {
8
13
State(
9
9
-
context: context.Context,
10
10
-
account_id: option.Option(String),
11
11
-
listeners: dict.Dict(String, fn() -> Nil),
14
14
+
context: Context,
15
15
+
account_id: Option(String),
16
16
+
subscriptions: Dict(Uuid, fn() -> Nil),
12
17
)
13
18
}
14
19
15
20
pub fn new(context: context.Context) -> State {
16
16
-
State(context:, account_id: option.None, listeners: dict.new())
21
21
+
State(context:, account_id: option.None, subscriptions: dict.new())
17
22
}
18
23
19
24
pub fn account_id(
···
38
43
state.context.bus
39
44
}
40
45
41
41
-
pub fn add_listener(
42
42
-
state: State,
43
43
-
channel: String,
44
44
-
unsubscribe: fn() -> Nil,
45
45
-
) -> State {
46
46
-
State(..state, listeners: dict.insert(state.listeners, channel, unsubscribe))
46
46
+
pub fn add_subscription(state: State, id: Uuid, subscription: fn() -> Nil) {
47
47
+
State(
48
48
+
..state,
49
49
+
subscriptions: dict.insert(state.subscriptions, id, subscription),
50
50
+
)
47
51
}
48
52
49
49
-
pub fn remove_listener(state: State, channel: String) -> State {
50
50
-
case dict.get(state.listeners, channel) {
51
51
-
Ok(unsub) -> unsub()
52
52
-
Error(Nil) -> Nil
53
53
+
pub fn unsubscribe(state: State, id: Uuid) {
54
54
+
case dict.get(state.subscriptions, id) {
55
55
+
Ok(sub) -> sub()
56
56
+
Error(_) -> Nil
53
57
}
54
54
-
State(..state, listeners: dict.delete(state.listeners, channel))
58
58
+
State(..state, subscriptions: dict.delete(state.subscriptions, id))
59
59
+
}
60
60
+
61
61
+
pub fn start_game_state_watcher(
62
62
+
state: State,
63
63
+
conn: mist.WebsocketConnection,
64
64
+
message_id: Uuid,
65
65
+
account_id: String,
66
66
+
field_id: game_state.FieldId,
67
67
+
) {
68
68
+
state.context.game_state_watchers
69
69
+
|> factory_supervisor.get_by_name()
70
70
+
|> factory_supervisor.start_child(game_state_watcher.Init(
71
71
+
bus: state.context.bus,
72
72
+
conn:,
73
73
+
message_id:,
74
74
+
account_id:,
75
75
+
field_id:,
76
76
+
))
55
77
}
+1
-1
src/lib/appserver/socket/SocketV1.svelte.ts
···
55
55
try {
56
56
const message = Value.Decode(ResponseMessage, JSON.parse(data));
57
57
this.dispatchEvent(new MessageEvent(message));
58
58
-
} catch (error) {
58
58
+
} catch {
59
59
this.#socket.close(4000, "Invalid JSON received");
60
60
this.dispatchEvent(new Event("error"));
61
61
}