Trading card city builder game?

set up concept of game state watcher

+183 -34
+6 -4
api/src/cartography_api/game_state.gleam
··· 41 41 } 42 42 43 43 pub type Field { 44 - Field(id: FieldId, tiles: List(FieldTile), citizens: List(FieldCitizen)) 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 + } 54 + 55 + pub fn new() { 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 - #("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 - 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 - decode.success(Field(id:, tiles:, citizens:)) 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 - - postgres 28 + - postgres 29 29 ports: 30 30 - "8000:8000" 31 31 environment:
+54
server/src/actor/game_state_watcher.gleam
··· 1 + import bus 2 + import cartography_api/game_state 3 + import gleam/erlang/process 4 + import gleam/otp/actor 5 + import mist 6 + import youid/uuid 7 + 8 + pub type Init { 9 + Init( 10 + bus: bus.Bus, 11 + conn: mist.WebsocketConnection, 12 + message_id: uuid.Uuid, 13 + account_id: String, 14 + field_id: game_state.FieldId, 15 + ) 16 + } 17 + 18 + type State { 19 + State(init: Init, game_state: game_state.GameState) 20 + } 21 + 22 + pub type Message { 23 + CardCreated(card_id: game_state.CardId) 24 + Stop 25 + } 26 + 27 + pub fn start(init: Init) { 28 + actor.new_with_initialiser(50, fn(sub) { 29 + let selector = 30 + process.new_selector() 31 + |> process.select(sub) 32 + |> process.select_map( 33 + bus.on_card_account(init.bus, init.account_id), 34 + CardCreated, 35 + ) 36 + State(init:, game_state: game_state.new()) 37 + |> actor.initialised() 38 + |> actor.selecting(selector) 39 + |> actor.returning(sub) 40 + |> Ok() 41 + }) 42 + |> actor.on_message(handle_message) 43 + |> actor.start() 44 + } 45 + 46 + fn handle_message(state: State, message: Message) -> actor.Next(State, Message) { 47 + case message { 48 + CardCreated(_card_id) -> { 49 + // TODO: include the card in the deck? 50 + actor.continue(state) 51 + } 52 + Stop -> actor.stop() 53 + } 54 + }
+12
server/src/handlers/unsubscribe_handler.gleam
··· 1 + import mist 2 + import websocket/state 3 + import youid/uuid 4 + 5 + pub fn handle( 6 + st: state.State, 7 + message_id: uuid.Uuid, 8 + ) -> Result(mist.Next(state.State, _msg), String) { 9 + state.unsubscribe(st, message_id) 10 + |> mist.continue() 11 + |> Ok() 12 + }
+34
server/src/handlers/watch_field_handler.gleam
··· 1 + import actor/game_state_watcher 2 + import cartography_api/game_state 3 + import gleam/erlang/process 4 + import gleam/result 5 + import gleam/string 6 + import mist 7 + import websocket/state 8 + import youid/uuid 9 + 10 + pub fn handle( 11 + st: state.State, 12 + conn: mist.WebsocketConnection, 13 + message_id: uuid.Uuid, 14 + field_id: game_state.FieldId, 15 + ) -> Result(mist.Next(state.State, _msg), String) { 16 + use account_id <- state.account_id(st) 17 + { 18 + use field_watcher <- result.try(state.start_game_state_watcher( 19 + st, 20 + conn, 21 + message_id, 22 + account_id, 23 + field_id, 24 + )) 25 + 26 + st 27 + |> state.add_subscription(message_id, fn() { 28 + process.send(field_watcher.data, game_state_watcher.Stop) 29 + }) 30 + |> mist.continue() 31 + |> Ok() 32 + } 33 + |> result.map_error(string.inspect) 34 + }
+17 -2
server/src/server.gleam
··· 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 + 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 - |> result.unwrap(12000) 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 - let context = context.Context(db_name, bus_handles) 38 + let game_state_watcher_supervisor_name = 39 + process.new_name("game_state_watcher_supervisor") 40 + let factory = 41 + factory_supervisor.worker_child(game_state_watcher.start) 42 + |> factory_supervisor.named(game_state_watcher_supervisor_name) 43 + |> factory_supervisor.supervised() 44 + 45 + let context = 46 + context.Context( 47 + db_name, 48 + bus_handles, 49 + game_state_watchers: game_state_watcher_supervisor_name, 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 + |> 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 + import actor/game_state_watcher 1 2 import bus 2 - import gleam/erlang/process.{type Name} 3 + import gleam/erlang/process.{type Name, type Subject} 4 + import gleam/otp/factory_supervisor 3 5 import pog 4 6 5 7 pub type Context { 6 - Context(db: Name(pog.Message), bus: bus.Bus) 8 + Context( 9 + db: Name(pog.Message), 10 + bus: bus.Bus, 11 + game_state_watchers: Name( 12 + factory_supervisor.Message( 13 + game_state_watcher.Init, 14 + Subject(game_state_watcher.Message), 15 + ), 16 + ), 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 + import handlers/unsubscribe_handler 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 - request.WatchField(_) -> { 28 - todo 29 - } 29 + request.WatchField(field_id) -> 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 - request.Unsubscribe -> { 33 - todo 34 - } 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 + import actor/game_state_watcher 1 2 import bus 2 - import gleam/dict 3 - import gleam/option 3 + import cartography_api/game_state 4 + import gleam/dict.{type Dict} 5 + import gleam/option.{type Option} 6 + import gleam/otp/factory_supervisor 7 + import mist 4 8 import pog 5 - import server/context 9 + import server/context.{type Context} 10 + import youid/uuid.{type Uuid} 6 11 7 12 pub opaque type State { 8 13 State( 9 - context: context.Context, 10 - account_id: option.Option(String), 11 - listeners: dict.Dict(String, fn() -> Nil), 14 + context: Context, 15 + account_id: Option(String), 16 + subscriptions: Dict(Uuid, fn() -> Nil), 12 17 ) 13 18 } 14 19 15 20 pub fn new(context: context.Context) -> State { 16 - State(context:, account_id: option.None, listeners: dict.new()) 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 - pub fn add_listener( 42 - state: State, 43 - channel: String, 44 - unsubscribe: fn() -> Nil, 45 - ) -> State { 46 - State(..state, listeners: dict.insert(state.listeners, channel, unsubscribe)) 46 + pub fn add_subscription(state: State, id: Uuid, subscription: fn() -> Nil) { 47 + State( 48 + ..state, 49 + subscriptions: dict.insert(state.subscriptions, id, subscription), 50 + ) 47 51 } 48 52 49 - pub fn remove_listener(state: State, channel: String) -> State { 50 - case dict.get(state.listeners, channel) { 51 - Ok(unsub) -> unsub() 52 - Error(Nil) -> Nil 53 + pub fn unsubscribe(state: State, id: Uuid) { 54 + case dict.get(state.subscriptions, id) { 55 + Ok(sub) -> sub() 56 + Error(_) -> Nil 53 57 } 54 - State(..state, listeners: dict.delete(state.listeners, channel)) 58 + State(..state, subscriptions: dict.delete(state.subscriptions, id)) 59 + } 60 + 61 + pub fn start_game_state_watcher( 62 + state: State, 63 + conn: mist.WebsocketConnection, 64 + message_id: Uuid, 65 + account_id: String, 66 + field_id: game_state.FieldId, 67 + ) { 68 + state.context.game_state_watchers 69 + |> factory_supervisor.get_by_name() 70 + |> factory_supervisor.start_child(game_state_watcher.Init( 71 + bus: state.context.bus, 72 + conn:, 73 + message_id:, 74 + account_id:, 75 + field_id:, 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 - } catch (error) { 58 + } catch { 59 59 this.#socket.close(4000, "Invalid JSON received"); 60 60 this.dispatchEvent(new Event("error")); 61 61 }