Trading card city builder game?

continuing building game state watcher

eldridge.cam 73692137 8c36b7cc

Waiting for spindle ...
+551 -431
+47 -51
api/src/cartography_api/game_state.gleam
··· 1 - import cartography_api/internal/repr 2 1 import gleam/dynamic/decode 3 2 import gleam/json.{type Json} 4 3 import gleam/list ··· 27 26 } 28 27 29 28 pub type Deck { 30 - Deck(cards: List(Card)) 29 + Deck(tiles: List(Tile), citizens: List(Citizen)) 31 30 } 32 31 33 - pub type Card { 32 + pub type Tile { 34 33 Tile(id: CardId, tile_type_id: CardTypeId, name: String) 34 + } 35 + 36 + pub type Citizen { 35 37 Citizen( 36 38 id: CardId, 37 39 species_id: CardTypeId, ··· 41 43 } 42 44 43 45 pub type Field { 44 - Field(tiles: List(FieldTile), citizens: List(FieldCitizen)) 46 + Field(name: String, tiles: List(FieldTile), citizens: List(FieldCitizen)) 45 47 } 46 48 47 49 pub type FieldTile { ··· 52 54 FieldCitizen(id: CardId, x: Int, y: Int) 53 55 } 54 56 55 - pub fn new() { 56 - GameState(deck: Deck(cards: []), field: Field(tiles: [], citizens: [])) 57 - } 58 - 59 57 pub fn to_json(game_state: GameState) -> Json { 60 58 json.object([ 61 59 #( 62 60 "deck", 63 61 json.object([ 64 62 #( 65 - "cards", 66 - game_state.deck.cards 63 + "tiles", 64 + game_state.deck.tiles 67 65 |> list.map(fn(card) { 68 66 case card { 69 67 Tile(TileId(id), TileTypeId(tile_type_id), name) -> ··· 72 70 #("tile_type_id", json.string(tile_type_id)), 73 71 #("name", json.string(name)), 74 72 ]) 75 - |> repr.struct("Tile") 73 + _ -> panic as "unreachable" 74 + } 75 + }) 76 + |> json.preprocessed_array(), 77 + ), 78 + #( 79 + "citizens", 80 + game_state.deck.citizens 81 + |> list.map(fn(card) { 82 + case card { 76 83 Citizen( 77 84 CitizenId(id), 78 85 SpeciesId(species_id), ··· 88 95 json.nullable(home_tile_id, fn(id) { json.int(id.id) }), 89 96 ), 90 97 ]) 91 - |> repr.struct("Citizen") 92 98 _ -> panic as "unreachable" 93 99 } 94 100 }) ··· 99 105 #( 100 106 "field", 101 107 json.object([ 108 + #("name", json.string(game_state.field.name)), 102 109 #( 103 110 "tiles", 104 111 game_state.field.tiles ··· 136 143 137 144 pub fn decoder() -> decode.Decoder(GameState) { 138 145 use deck <- decode.field("deck", { 139 - use cards <- decode.field( 140 - "cards", 146 + use tiles <- decode.field( 147 + "tiles", 141 148 decode.list({ 142 - use tag <- repr.struct_tag(Tile( 143 - id: TileId(0), 144 - tile_type_id: TileTypeId(""), 145 - name: "", 146 - )) 147 - case tag { 148 - "Tile" -> { 149 - use id <- decode.field("id", decode.map(decode.int, TileId)) 150 - use tile_type_id <- decode.field( 151 - "tile_type_id", 152 - decode.map(decode.string, TileTypeId), 153 - ) 154 - use name <- decode.field("name", decode.string) 155 - decode.success(Tile(id:, tile_type_id:, name:)) 156 - } 157 - "Citizen" -> { 158 - use id <- decode.field("id", decode.map(decode.int, CitizenId)) 159 - use species_id <- decode.field( 160 - "species_id", 161 - decode.map(decode.string, SpeciesId), 162 - ) 163 - use name <- decode.field("name", decode.string) 164 - use home_tile_id <- decode.field( 165 - "home_tile_id", 166 - decode.optional(decode.map(decode.int, TileId)), 167 - ) 168 - decode.success(Citizen(id:, species_id:, name:, home_tile_id:)) 169 - } 170 - _ -> { 171 - decode.failure( 172 - Tile(id: TileId(0), tile_type_id: TileTypeId(""), name: ""), 173 - "card", 174 - ) 175 - } 176 - } 149 + use id <- decode.field("id", decode.map(decode.int, TileId)) 150 + use tile_type_id <- decode.field( 151 + "tile_type_id", 152 + decode.map(decode.string, TileTypeId), 153 + ) 154 + use name <- decode.field("name", decode.string) 155 + decode.success(Tile(id:, tile_type_id:, name:)) 156 + }), 157 + ) 158 + use citizens <- decode.field( 159 + "citizens", 160 + decode.list({ 161 + use id <- decode.field("id", decode.map(decode.int, CitizenId)) 162 + use species_id <- decode.field( 163 + "species_id", 164 + decode.map(decode.string, SpeciesId), 165 + ) 166 + use name <- decode.field("name", decode.string) 167 + use home_tile_id <- decode.field( 168 + "home_tile_id", 169 + decode.optional(decode.map(decode.int, TileId)), 170 + ) 171 + decode.success(Citizen(id:, species_id:, name:, home_tile_id:)) 177 172 }), 178 173 ) 179 - decode.success(Deck(cards:)) 174 + decode.success(Deck(tiles:, citizens:)) 180 175 }) 181 176 use field <- decode.field("field", { 177 + use name <- decode.field("name", decode.string) 182 178 use tiles <- decode.field( 183 179 "tiles", 184 180 decode.list({ ··· 197 193 decode.success(FieldCitizen(id:, x:, y:)) 198 194 }), 199 195 ) 200 - decode.success(Field(tiles:, citizens:)) 196 + decode.success(Field(name:, tiles:, citizens:)) 201 197 }) 202 198 decode.success(GameState(deck:, field:)) 203 199 }
-8
api/test/cartography_api_test.gleam
··· 3 3 pub fn main() -> Nil { 4 4 gleeunit.main() 5 5 } 6 - 7 - // gleeunit test functions end in `_test` 8 - pub fn hello_world_test() { 9 - let name = "Joe" 10 - let greeting = "Hello, " <> name <> "!" 11 - 12 - assert greeting == "Hello, Joe!" 13 - }
+52
api/test/game_state_test.gleam
··· 1 + import cartography_api/game_state 2 + import gleam/json 3 + import gleam/option 4 + 5 + pub fn game_state_round_trip_test() { 6 + let game_state = 7 + game_state.GameState( 8 + deck: game_state.Deck( 9 + tiles: [ 10 + game_state.Tile( 11 + id: game_state.TileId(3), 12 + tile_type_id: game_state.TileTypeId("bakery"), 13 + name: "Bakery 1", 14 + ), 15 + game_state.Tile( 16 + id: game_state.TileId(4), 17 + tile_type_id: game_state.TileTypeId("house"), 18 + name: "House 1", 19 + ), 20 + ], 21 + citizens: [ 22 + game_state.Citizen( 23 + id: game_state.CitizenId(1), 24 + species_id: game_state.SpeciesId("cat"), 25 + name: "Panda", 26 + home_tile_id: option.None, 27 + ), 28 + game_state.Citizen( 29 + id: game_state.CitizenId(2), 30 + species_id: game_state.SpeciesId("cat"), 31 + name: "Natto", 32 + home_tile_id: option.Some(game_state.TileId(4)), 33 + ), 34 + ], 35 + ), 36 + field: game_state.Field( 37 + name: "The Field", 38 + tiles: [ 39 + game_state.FieldTile(id: game_state.TileId(3), x: 1, y: 1), 40 + game_state.FieldTile(id: game_state.TileId(4), x: 2, y: 1), 41 + ], 42 + citizens: [ 43 + game_state.FieldCitizen(id: game_state.CitizenId(2), x: 2, y: 1), 44 + ], 45 + ), 46 + ) 47 + 48 + let json = game_state.to_string(game_state) 49 + let assert Ok(game_state_again) = json.parse(json, game_state.decoder()) 50 + 51 + assert game_state == game_state_again 52 + }
+68
generated/schema.sql
··· 184 184 185 185 186 186 -- 187 + -- Name: field_citizens; Type: TABLE; Schema: public; Owner: - 188 + -- 189 + 190 + CREATE TABLE public.field_citizens ( 191 + citizen_id bigint NOT NULL, 192 + account_id public.citext NOT NULL, 193 + field_id bigint NOT NULL, 194 + grid_x integer NOT NULL, 195 + grid_y integer NOT NULL 196 + ); 197 + 198 + 199 + -- 200 + -- Name: TABLE field_citizens; Type: COMMENT; Schema: public; Owner: - 201 + -- 202 + 203 + COMMENT ON TABLE public.field_citizens IS 'Tracks the state of citizens currently deployed on the field.'; 204 + 205 + 206 + -- 187 207 -- Name: field_tiles; Type: TABLE; Schema: public; Owner: - 188 208 -- 189 209 ··· 529 549 530 550 531 551 -- 552 + -- Name: field_citizens field_citizens_pkey; Type: CONSTRAINT; Schema: public; Owner: - 553 + -- 554 + 555 + ALTER TABLE ONLY public.field_citizens 556 + ADD CONSTRAINT field_citizens_pkey PRIMARY KEY (citizen_id); 557 + 558 + 559 + -- 532 560 -- Name: field_tiles field_tiles_pkey; Type: CONSTRAINT; Schema: public; Owner: - 533 561 -- 534 562 ··· 701 729 702 730 ALTER TABLE ONLY public.citizens 703 731 ADD CONSTRAINT citizens_species_id_fkey FOREIGN KEY (species_id) REFERENCES public.species(id) ON UPDATE CASCADE ON DELETE CASCADE; 732 + 733 + 734 + -- 735 + -- Name: field_citizens field_citizens_account_id_citizen_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 736 + -- 737 + 738 + ALTER TABLE ONLY public.field_citizens 739 + ADD CONSTRAINT field_citizens_account_id_citizen_id_fkey FOREIGN KEY (account_id, citizen_id) REFERENCES public.card_accounts(account_id, card_id) ON UPDATE CASCADE ON DELETE CASCADE; 740 + 741 + 742 + -- 743 + -- Name: field_citizens field_citizens_account_id_field_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 744 + -- 745 + 746 + ALTER TABLE ONLY public.field_citizens 747 + ADD CONSTRAINT field_citizens_account_id_field_id_fkey FOREIGN KEY (account_id, field_id) REFERENCES public.fields(account_id, id) ON UPDATE CASCADE ON DELETE CASCADE; 748 + 749 + 750 + -- 751 + -- Name: field_citizens field_citizens_account_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 752 + -- 753 + 754 + ALTER TABLE ONLY public.field_citizens 755 + ADD CONSTRAINT field_citizens_account_id_fkey FOREIGN KEY (account_id) REFERENCES public.accounts(id) ON UPDATE CASCADE ON DELETE CASCADE; 756 + 757 + 758 + -- 759 + -- Name: field_citizens field_citizens_citizen_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 760 + -- 761 + 762 + ALTER TABLE ONLY public.field_citizens 763 + ADD CONSTRAINT field_citizens_citizen_id_fkey FOREIGN KEY (citizen_id) REFERENCES public.citizens(id) ON UPDATE CASCADE ON DELETE CASCADE; 764 + 765 + 766 + -- 767 + -- Name: field_citizens field_citizens_field_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 768 + -- 769 + 770 + ALTER TABLE ONLY public.field_citizens 771 + ADD CONSTRAINT field_citizens_field_id_fkey FOREIGN KEY (field_id) REFERENCES public.fields(id) ON UPDATE CASCADE ON DELETE CASCADE; 704 772 705 773 706 774 --
+16
migrations/committed/000014.sql
··· 1 + --! Previous: sha1:6899eea812121a36fcdc877f6c5c8111b43e6fd7 2 + --! Hash: sha1:e1df9dc29a55d976f7f130d613fc1ba172e2df1b 3 + 4 + DROP TABLE IF EXISTS field_citizens; 5 + 6 + CREATE TABLE field_citizens ( 7 + citizen_id BIGINT PRIMARY KEY REFERENCES citizens (id) ON DELETE CASCADE ON UPDATE CASCADE, 8 + account_id CITEXT NOT NULL REFERENCES accounts (id) ON DELETE CASCADE ON UPDATE CASCADE, 9 + field_id BIGINT NOT NULL REFERENCES fields (id) ON DELETE CASCADE ON UPDATE CASCADE, 10 + grid_x INTEGER NOT NULL, 11 + grid_y INTEGER NOT NULL, 12 + FOREIGN KEY (account_id, field_id) REFERENCES fields (account_id, id) ON DELETE CASCADE ON UPDATE CASCADE, 13 + FOREIGN KEY (account_id, citizen_id) REFERENCES card_accounts (account_id, card_id) ON DELETE CASCADE ON UPDATE CASCADE 14 + ); 15 + 16 + COMMENT ON TABLE field_citizens IS 'Tracks the state of citizens currently deployed on the field.';
+27 -5
server/src/actor/game_state_watcher.gleam
··· 1 1 import bus 2 2 import cartography_api/game_state 3 + import db/game_state as db_game_state 3 4 import gleam/erlang/process 4 5 import gleam/otp/actor 6 + import gleam/result 5 7 import mist 8 + import pog 6 9 import youid/uuid 7 10 8 11 pub type Init { 9 12 Init( 10 - bus: bus.Bus, 11 13 conn: mist.WebsocketConnection, 12 14 message_id: uuid.Uuid, 13 15 account_id: String, ··· 16 18 } 17 19 18 20 type State { 19 - State(init: Init, game_state: game_state.GameState) 21 + State( 22 + conn: mist.WebsocketConnection, 23 + db: process.Name(pog.Message), 24 + message_id: uuid.Uuid, 25 + account_id: String, 26 + field_id: game_state.FieldId, 27 + game_state: game_state.GameState, 28 + ) 20 29 } 21 30 22 31 pub type Message { ··· 24 33 Stop 25 34 } 26 35 27 - pub fn start(init: Init) { 36 + pub fn start(db: process.Name(pog.Message), bus: bus.Bus, init: Init) { 28 37 actor.new_with_initialiser(50, fn(sub) { 29 38 let selector = 30 39 process.new_selector() 31 40 |> process.select(sub) 32 41 |> process.select_map( 33 - bus.on_card_account(init.bus, init.account_id), 42 + bus.on_card_account(bus, init.account_id), 34 43 CardCreated, 35 44 ) 36 - State(init:, game_state: game_state.new()) 45 + 46 + use game_state <- result.try(db_game_state.load( 47 + pog.named_connection(db), 48 + init.field_id, 49 + )) 50 + 51 + State( 52 + conn: init.conn, 53 + field_id: init.field_id, 54 + message_id: init.message_id, 55 + account_id: init.account_id, 56 + db:, 57 + game_state:, 58 + ) 37 59 |> actor.initialised() 38 60 |> actor.selecting(selector) 39 61 |> actor.returning(sub)
+101
server/src/db/game_state.gleam
··· 1 + import cartography_api/game_state 2 + import db/rows 3 + import db/sql 4 + import gleam/list 5 + import gleam/option 6 + import gleam/pair 7 + import gleam/result 8 + import gleam/string 9 + import pog 10 + 11 + fn to_tile(tile: sql.GetPlayerDeckRow) -> game_state.Tile { 12 + case tile { 13 + sql.GetPlayerDeckRow( 14 + id, 15 + name, 16 + tile_type_id: option.Some(tile_type_id), 17 + species_id: option.None, 18 + home_tile_id: option.None, 19 + ) -> 20 + game_state.Tile( 21 + id: game_state.TileId(id), 22 + tile_type_id: game_state.TileTypeId(tile_type_id), 23 + name:, 24 + ) 25 + _ -> panic as "unreachable" 26 + } 27 + } 28 + 29 + fn to_citizen(citizen: sql.GetPlayerDeckRow) -> game_state.Citizen { 30 + case citizen { 31 + sql.GetPlayerDeckRow( 32 + id, 33 + name, 34 + tile_type_id: option.None, 35 + species_id: option.Some(species_id), 36 + home_tile_id:, 37 + ) -> 38 + game_state.Citizen( 39 + id: game_state.CitizenId(id), 40 + species_id: game_state.SpeciesId(species_id), 41 + name:, 42 + home_tile_id: option.map(home_tile_id, game_state.TileId), 43 + ) 44 + _ -> panic as "unreachable" 45 + } 46 + } 47 + 48 + fn to_field_tile(tile: sql.GetFieldTilesRow) -> game_state.FieldTile { 49 + game_state.FieldTile( 50 + id: game_state.TileId(tile.tile_id), 51 + x: tile.grid_x, 52 + y: tile.grid_y, 53 + ) 54 + } 55 + 56 + fn to_field_citizen(citizen: sql.GetFieldCitizensRow) -> game_state.FieldCitizen { 57 + game_state.FieldCitizen( 58 + id: game_state.CitizenId(citizen.citizen_id), 59 + x: citizen.grid_x, 60 + y: citizen.grid_y, 61 + ) 62 + } 63 + 64 + pub fn load(db: pog.Connection, field_id: game_state.FieldId) { 65 + use field <- result.try( 66 + db 67 + |> sql.get_field_by_id(field_id.id) 68 + |> result.map_error(string.inspect), 69 + ) 70 + use field <- rows.one_or(field, "field not found") 71 + use #(tiles, citizens) <- result.try( 72 + sql.get_player_deck(db, field.account_id) 73 + |> result.map(fn(result) { 74 + list.partition(result.rows, fn(card) { option.is_some(card.tile_type_id) }) 75 + |> pair.map_first(list.map(_, to_tile)) 76 + |> pair.map_second(list.map(_, to_citizen)) 77 + }) 78 + |> result.map_error(string.inspect), 79 + ) 80 + 81 + use field_tiles <- result.try( 82 + sql.get_field_tiles(db, field_id.id) 83 + |> result.map(fn(result) { list.map(result.rows, to_field_tile) }) 84 + |> result.map_error(string.inspect), 85 + ) 86 + 87 + use field_citizens <- result.try( 88 + sql.get_field_citizens(db, field_id.id) 89 + |> result.map(fn(result) { list.map(result.rows, to_field_citizen) }) 90 + |> result.map_error(string.inspect), 91 + ) 92 + 93 + Ok(game_state.GameState( 94 + deck: game_state.Deck(tiles:, citizens:), 95 + field: game_state.Field( 96 + name: field.name, 97 + tiles: field_tiles, 98 + citizens: field_citizens, 99 + ), 100 + )) 101 + }
+12
server/src/db/rows.gleam
··· 33 33 } 34 34 } 35 35 36 + pub fn one_or( 37 + result: pog.Returned(t), 38 + error: e, 39 + with_row: fn(t) -> Result(u, e), 40 + ) -> Result(u, e) { 41 + case result.rows { 42 + [] -> Error(error) 43 + [row] -> with_row(row) 44 + _ -> Error(error) 45 + } 46 + } 47 + 36 48 pub fn execute( 37 49 query: pog.Query(t), 38 50 database: pog.Connection,
+170
server/src/db/sql.gleam
··· 5 5 //// 6 6 7 7 import gleam/dynamic/decode 8 + import gleam/option.{type Option} 8 9 import pog 9 10 10 11 /// A row you get from running the `create_account` query ··· 329 330 " 330 331 |> pog.query 331 332 |> pog.parameter(pog.int(arg_1)) 333 + |> pog.returning(decoder) 334 + |> pog.execute(db) 335 + } 336 + 337 + /// A row you get from running the `get_field_citizens` query 338 + /// defined in `./src/db/sql/get_field_citizens.sql`. 339 + /// 340 + /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 341 + /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 342 + /// 343 + pub type GetFieldCitizensRow { 344 + GetFieldCitizensRow( 345 + citizen_id: Int, 346 + account_id: String, 347 + field_id: Int, 348 + grid_x: Int, 349 + grid_y: Int, 350 + ) 351 + } 352 + 353 + /// Runs the `get_field_citizens` query 354 + /// defined in `./src/db/sql/get_field_citizens.sql`. 355 + /// 356 + /// > 🐿️ This function was generated automatically using v4.6.0 of 357 + /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 358 + /// 359 + pub fn get_field_citizens( 360 + db: pog.Connection, 361 + arg_1: Int, 362 + ) -> Result(pog.Returned(GetFieldCitizensRow), pog.QueryError) { 363 + let decoder = { 364 + use citizen_id <- decode.field(0, decode.int) 365 + use account_id <- decode.field(1, decode.string) 366 + use field_id <- decode.field(2, decode.int) 367 + use grid_x <- decode.field(3, decode.int) 368 + use grid_y <- decode.field(4, decode.int) 369 + decode.success(GetFieldCitizensRow( 370 + citizen_id:, 371 + account_id:, 372 + field_id:, 373 + grid_x:, 374 + grid_y:, 375 + )) 376 + } 377 + 378 + "SELECT 379 + * 380 + FROM 381 + field_citizens 382 + WHERE 383 + field_citizens.field_id = $1; 384 + " 385 + |> pog.query 386 + |> pog.parameter(pog.int(arg_1)) 387 + |> pog.returning(decoder) 388 + |> pog.execute(db) 389 + } 390 + 391 + /// A row you get from running the `get_field_tiles` query 392 + /// defined in `./src/db/sql/get_field_tiles.sql`. 393 + /// 394 + /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 395 + /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 396 + /// 397 + pub type GetFieldTilesRow { 398 + GetFieldTilesRow( 399 + tile_id: Int, 400 + account_id: String, 401 + field_id: Int, 402 + grid_x: Int, 403 + grid_y: Int, 404 + ) 405 + } 406 + 407 + /// Runs the `get_field_tiles` query 408 + /// defined in `./src/db/sql/get_field_tiles.sql`. 409 + /// 410 + /// > 🐿️ This function was generated automatically using v4.6.0 of 411 + /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 412 + /// 413 + pub fn get_field_tiles( 414 + db: pog.Connection, 415 + arg_1: Int, 416 + ) -> Result(pog.Returned(GetFieldTilesRow), pog.QueryError) { 417 + let decoder = { 418 + use tile_id <- decode.field(0, decode.int) 419 + use account_id <- decode.field(1, decode.string) 420 + use field_id <- decode.field(2, decode.int) 421 + use grid_x <- decode.field(3, decode.int) 422 + use grid_y <- decode.field(4, decode.int) 423 + decode.success(GetFieldTilesRow( 424 + tile_id:, 425 + account_id:, 426 + field_id:, 427 + grid_x:, 428 + grid_y:, 429 + )) 430 + } 431 + 432 + "SELECT 433 + * 434 + FROM 435 + field_tiles 436 + WHERE 437 + field_tiles.field_id = $1; 438 + " 439 + |> pog.query 440 + |> pog.parameter(pog.int(arg_1)) 441 + |> pog.returning(decoder) 442 + |> pog.execute(db) 443 + } 444 + 445 + /// A row you get from running the `get_player_deck` query 446 + /// defined in `./src/db/sql/get_player_deck.sql`. 447 + /// 448 + /// > 🐿️ This type definition was generated automatically using v4.6.0 of the 449 + /// > [squirrel package](https://github.com/giacomocavalieri/squirrel). 450 + /// 451 + pub type GetPlayerDeckRow { 452 + GetPlayerDeckRow( 453 + id: Int, 454 + name: String, 455 + tile_type_id: Option(String), 456 + species_id: Option(String), 457 + home_tile_id: Option(Int), 458 + ) 459 + } 460 + 461 + /// Runs the `get_player_deck` query 462 + /// defined in `./src/db/sql/get_player_deck.sql`. 463 + /// 464 + /// > 🐿️ This function was generated automatically using v4.6.0 of 465 + /// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). 466 + /// 467 + pub fn get_player_deck( 468 + db: pog.Connection, 469 + arg_1: String, 470 + ) -> Result(pog.Returned(GetPlayerDeckRow), pog.QueryError) { 471 + let decoder = { 472 + use id <- decode.field(0, decode.int) 473 + use name <- decode.field(1, decode.string) 474 + use tile_type_id <- decode.field(2, decode.optional(decode.string)) 475 + use species_id <- decode.field(3, decode.optional(decode.string)) 476 + use home_tile_id <- decode.field(4, decode.optional(decode.int)) 477 + decode.success(GetPlayerDeckRow( 478 + id:, 479 + name:, 480 + tile_type_id:, 481 + species_id:, 482 + home_tile_id:, 483 + )) 484 + } 485 + 486 + "SELECT 487 + cards.id as id, 488 + coalesce(tiles.name, citizens.name) as name, 489 + tiles.tile_type_id, 490 + citizens.species_id, 491 + citizens.home_tile_id 492 + FROM 493 + cards 494 + INNER JOIN card_accounts ON card_accounts.card_id = cards.id 495 + LEFT OUTER JOIN citizens ON cards.id = citizens.id 496 + LEFT OUTER JOIN tiles ON cards.id = tiles.id 497 + WHERE 498 + card_accounts.account_id = $1; 499 + " 500 + |> pog.query 501 + |> pog.parameter(pog.text(arg_1)) 332 502 |> pog.returning(decoder) 333 503 |> pog.execute(db) 334 504 }
+6
server/src/db/sql/get_field_citizens.sql
··· 1 + SELECT 2 + * 3 + FROM 4 + field_citizens 5 + WHERE 6 + field_citizens.field_id = $1;
+6
server/src/db/sql/get_field_tiles.sql
··· 1 + SELECT 2 + * 3 + FROM 4 + field_tiles 5 + WHERE 6 + field_tiles.field_id = $1;
+13
server/src/db/sql/get_player_deck.sql
··· 1 + SELECT 2 + cards.id as id, 3 + coalesce(tiles.name, citizens.name) as name, 4 + tiles.tile_type_id, 5 + citizens.species_id, 6 + citizens.home_tile_id 7 + FROM 8 + cards 9 + INNER JOIN card_accounts ON card_accounts.card_id = cards.id 10 + LEFT OUTER JOIN citizens ON cards.id = citizens.id 11 + LEFT OUTER JOIN tiles ON cards.id = tiles.id 12 + WHERE 13 + card_accounts.account_id = $1;
+2 -3
server/src/handlers/authenticate_handler.gleam
··· 16 16 account_id: String, 17 17 ) -> Result(mist.Next(state.State, _msg), String) { 18 18 { 19 - let assert Ok(acc) = sql.create_account(state.db_connection(st), account_id) 19 + let assert Ok(acc) = sql.create_account(state.db(st), account_id) 20 20 use acc <- rows.one_or_none(acc) 21 21 let assert Ok(account_id) = case acc { 22 22 option.Some(acc) -> Ok(acc.id) 23 23 option.None -> { 24 - let assert Ok(acc) = 25 - sql.get_account(state.db_connection(st), account_id) 24 + let assert Ok(acc) = sql.get_account(state.db(st), account_id) 26 25 use acc <- rows.one(acc) 27 26 Ok(acc.id) 28 27 }
+3 -3
server/src/handlers/debug_add_card_handler.gleam
··· 14 14 use account_id <- state.account_id(st) 15 15 { 16 16 let assert Ok(card_type) = 17 - state.db_connection(st) 17 + state.db(st) 18 18 |> sql.get_card_type(card_type_id.id) 19 19 use card_type <- rows.one(card_type) 20 20 21 21 use card_id <- result.try(case card_type.class { 22 22 sql.Citizen -> { 23 23 let assert Ok(citizen) = 24 - state.db_connection(st) 24 + state.db(st) 25 25 |> sql.create_citizen(account_id, card_type.id) 26 26 use citizen <- rows.one(citizen) 27 27 Ok(game_state.CitizenId(citizen.card_id)) 28 28 } 29 29 sql.Tile -> { 30 30 let assert Ok(tile) = 31 - state.db_connection(st) 31 + state.db(st) 32 32 |> sql.create_tile(account_id, card_type.id) 33 33 use tile <- rows.one(tile) 34 34 Ok(game_state.TileId(tile.card_id))
+1 -1
server/src/handlers/list_fields_handler.gleam
··· 17 17 use account_id <- state.account_id(st) 18 18 { 19 19 let assert Ok(result) = 20 - sql.list_fields_for_account(state.db_connection(st), account_id) 20 + sql.list_fields_for_account(state.db(st), account_id) 21 21 let message = 22 22 result.rows 23 23 |> list.map(fn(row) {
+1 -4
server/src/handlers/watch_field_handler.gleam
··· 17 17 { 18 18 use field_watcher <- result.try(state.start_game_state_watcher( 19 19 st, 20 - conn, 21 - message_id, 22 - account_id, 23 - field_id, 20 + game_state_watcher.Init(conn:, message_id:, account_id:, field_id:), 24 21 )) 25 22 26 23 st
-9
server/src/models/account.gleam
··· 1 - import gleam/json 2 - 3 - pub type Account { 4 - Account(id: String) 5 - } 6 - 7 - pub fn to_json(account: Account) { 8 - json.object([#("id", json.string(account.id))]) 9 - }
-12
server/src/models/card.gleam
··· 1 - import gleam/json 2 - 3 - pub type Card { 4 - Card(id: Int, card_type_id: String) 5 - } 6 - 7 - pub fn to_json(citizen: Card) { 8 - json.object([ 9 - #("id", json.int(citizen.id)), 10 - #("card_type_id", json.string(citizen.card_type_id)), 11 - ]) 12 - }
-19
server/src/models/card_account.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - 4 - pub type CardAccount { 5 - CardAccount(card_id: Int, account_id: String) 6 - } 7 - 8 - pub fn to_json(field_card: CardAccount) { 9 - json.object([ 10 - #("card_id", json.int(field_card.card_id)), 11 - #("account_id", json.string(field_card.account_id)), 12 - ]) 13 - } 14 - 15 - pub fn from_json() { 16 - use card_id <- decode.field("card_id", decode.int) 17 - use account_id <- decode.field("account_id", decode.string) 18 - decode.success(CardAccount(card_id:, account_id:)) 19 - }
-22
server/src/models/card_class.gleam
··· 1 - import gleam/dynamic/decode 2 - 3 - pub type CardClass { 4 - Tile 5 - Citizen 6 - } 7 - 8 - pub fn decoder() { 9 - use value <- decode.then(decode.string) 10 - case value { 11 - "tile" -> decode.success(Tile) 12 - "citizen" -> decode.success(Citizen) 13 - _ -> decode.failure(Tile, "card class value") 14 - } 15 - } 16 - 17 - pub fn to_string(class: CardClass) { 18 - case class { 19 - Tile -> "tile" 20 - Citizen -> "citizen" 21 - } 22 - }
-34
server/src/models/card_set.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - import gleam/string 4 - import gleam/time/calendar 5 - import gleam/time/timestamp 6 - 7 - pub type CardSet { 8 - CardSet(id: String, release_date: timestamp.Timestamp) 9 - } 10 - 11 - pub fn to_json(card_set: CardSet) { 12 - json.object([ 13 - #("id", json.string(card_set.id)), 14 - #( 15 - "release_date", 16 - card_set.release_date 17 - |> timestamp.to_rfc3339(calendar.utc_offset) 18 - |> json.string(), 19 - ), 20 - ]) 21 - } 22 - 23 - pub fn from_json() { 24 - use id <- decode.field("id", decode.string) 25 - use release_date <- decode.field("release_date", decode.string) 26 - case timestamp.parse_rfc3339(release_date) { 27 - Ok(release_date) -> decode.success(CardSet(id:, release_date:)) 28 - Error(error) -> 29 - decode.failure( 30 - CardSet(id:, release_date: timestamp.unix_epoch), 31 - string.inspect(error), 32 - ) 33 - } 34 - }
-13
server/src/models/card_type.gleam
··· 1 - import gleam/json 2 - import models/card_class 3 - 4 - pub type CardType { 5 - CardType(id: String, class: card_class.CardClass) 6 - } 7 - 8 - pub fn to_json(card_type: CardType) { 9 - json.object([ 10 - #("id", json.string(card_type.id)), 11 - #("class", json.string(card_class.to_string(card_type.class))), 12 - ]) 13 - }
-20
server/src/models/citizen.gleam
··· 1 - import gleam/json 2 - import gleam/option 3 - 4 - pub type Citizen { 5 - Citizen( 6 - id: Int, 7 - species_id: String, 8 - home_tile_id: option.Option(Int), 9 - name: String, 10 - ) 11 - } 12 - 13 - pub fn to_json(citizen: Citizen) { 14 - json.object([ 15 - #("id", json.int(citizen.id)), 16 - #("species_id", json.string(citizen.species_id)), 17 - #("home_tile_id", json.nullable(citizen.home_tile_id, json.int)), 18 - #("name", json.string(citizen.name)), 19 - ]) 20 - }
-30
server/src/models/field.gleam
··· 1 - import db/sql 2 - import gleam/dynamic/decode 3 - import gleam/json 4 - 5 - pub type Field { 6 - Field(id: Int, name: String, account_id: String) 7 - } 8 - 9 - pub fn to_json(field: Field) { 10 - json.object([ 11 - #("id", json.int(field.id)), 12 - #("name", json.string(field.name)), 13 - #("account_id", json.string(field.account_id)), 14 - ]) 15 - } 16 - 17 - pub fn from_json() { 18 - use id <- decode.field("id", decode.int) 19 - use name <- decode.field("name", decode.string) 20 - use account_id <- decode.field("account_id", decode.string) 21 - decode.success(Field(id:, name:, account_id:)) 22 - } 23 - 24 - pub fn from_list_fields_for_account(row: sql.ListFieldsForAccountRow) { 25 - Field(id: row.id, name: row.name, account_id: row.account_id) 26 - } 27 - 28 - pub fn from_get_field_by_id(row: sql.GetFieldByIdRow) { 29 - Field(id: row.id, name: row.name, account_id: row.account_id) 30 - }
-31
server/src/models/field_tile.gleam
··· 1 - import gleam/dynamic/decode 2 - import gleam/json 3 - 4 - pub type FieldTile { 5 - FieldTile( 6 - tile_id: Int, 7 - account_id: String, 8 - field_id: Int, 9 - grid_x: Int, 10 - grid_y: Int, 11 - ) 12 - } 13 - 14 - pub fn to_json(field_tile: FieldTile) { 15 - json.object([ 16 - #("tile_id", json.int(field_tile.tile_id)), 17 - #("account_id", json.string(field_tile.account_id)), 18 - #("field_id", json.int(field_tile.field_id)), 19 - #("grid_x", json.int(field_tile.grid_x)), 20 - #("grid_y", json.int(field_tile.grid_y)), 21 - ]) 22 - } 23 - 24 - pub fn from_json() { 25 - use tile_id <- decode.field("tile_id", decode.int) 26 - use account_id <- decode.field("account_id", decode.string) 27 - use field_id <- decode.field("field_id", decode.int) 28 - use grid_x <- decode.field("grid_x", decode.int) 29 - use grid_y <- decode.field("grid_y", decode.int) 30 - decode.success(FieldTile(tile_id:, account_id:, field_id:, grid_x:, grid_y:)) 31 - }
-11
server/src/models/resource.gleam
··· 1 - import gleam/json 2 - 3 - pub type Resource { 4 - Resource(id: String) 5 - } 6 - 7 - pub fn to_json(resource: Resource) { 8 - json.object([ 9 - #("id", json.string(resource.id)), 10 - ]) 11 - }
-11
server/src/models/species.gleam
··· 1 - import gleam/json 2 - 3 - pub type Species { 4 - Species(id: String) 5 - } 6 - 7 - pub fn to_json(species: Species) { 8 - json.object([ 9 - #("id", json.string(species.id)), 10 - ]) 11 - }
-13
server/src/models/species_need.gleam
··· 1 - import gleam/json 2 - 3 - pub type SpeciesNeed { 4 - SpeciesNeed(species_id: String, resource_id: String, quantity: Int) 5 - } 6 - 7 - pub fn to_json(need: SpeciesNeed) { 8 - json.object([ 9 - #("species_id", json.string(need.species_id)), 10 - #("resource_id", json.string(need.resource_id)), 11 - #("quantity", json.int(need.quantity)), 12 - ]) 13 - }
-13
server/src/models/tile.gleam
··· 1 - import gleam/json 2 - 3 - pub type Tile { 4 - Tile(id: Int, tile_type_id: String, name: String) 5 - } 6 - 7 - pub fn to_json(tile: Tile) { 8 - json.object([ 9 - #("id", json.int(tile.id)), 10 - #("tile_type_id", json.string(tile.tile_type_id)), 11 - #("name", json.string(tile.name)), 12 - ]) 13 - }
-34
server/src/models/tile_category.gleam
··· 1 - import gleam/dynamic/decode 2 - 3 - pub type TileCategory { 4 - Residential 5 - Production 6 - Amenity 7 - Source 8 - Trade 9 - Transportation 10 - } 11 - 12 - pub fn decoder() { 13 - use value <- decode.then(decode.string) 14 - case value { 15 - "residential" -> decode.success(Residential) 16 - "production" -> decode.success(Production) 17 - "amenity" -> decode.success(Amenity) 18 - "source" -> decode.success(Source) 19 - "trade" -> decode.success(Trade) 20 - "transportation" -> decode.success(Transportation) 21 - _ -> decode.failure(Residential, "card category value") 22 - } 23 - } 24 - 25 - pub fn to_string(category: TileCategory) { 26 - case category { 27 - Residential -> "residential" 28 - Production -> "production" 29 - Amenity -> "amenity" 30 - Source -> "source" 31 - Trade -> "trade" 32 - Transportation -> "transportation" 33 - } 34 - }
-20
server/src/models/tile_type.gleam
··· 1 - import gleam/json 2 - import models/tile_category 3 - 4 - pub type TileType { 5 - TileType( 6 - id: String, 7 - category: tile_category.TileCategory, 8 - houses: Int, 9 - employs: Int, 10 - ) 11 - } 12 - 13 - pub fn to_json(card_type: TileType) { 14 - json.object([ 15 - #("id", json.string(card_type.id)), 16 - #("category", json.string(tile_category.to_string(card_type.category))), 17 - #("houses", json.int(card_type.houses)), 18 - #("employs", json.int(card_type.employs)), 19 - ]) 20 - }
-13
server/src/models/tile_type_consume.gleam
··· 1 - import gleam/json 2 - 3 - pub type TileTypeConsume { 4 - TileTypeConsume(tile_type_id: String, resource_id: String, quantity: Int) 5 - } 6 - 7 - pub fn to_json(consume: TileTypeConsume) { 8 - json.object([ 9 - #("tile_type_id", json.string(consume.tile_type_id)), 10 - #("resource_id", json.string(consume.resource_id)), 11 - #("quantity", json.int(consume.quantity)), 12 - ]) 13 - }
-13
server/src/models/tile_type_produce.gleam
··· 1 - import gleam/json 2 - 3 - pub type TileTypeProduce { 4 - TileTypeProduce(tile_type_id: String, resource_id: String, quantity: Int) 5 - } 6 - 7 - pub fn to_json(produce: TileTypeProduce) { 8 - json.object([ 9 - #("tile_type_id", json.string(produce.tile_type_id)), 10 - #("resource_id", json.string(produce.resource_id)), 11 - #("quantity", json.int(produce.quantity)), 12 - ]) 13 - }
+7 -13
server/src/server.gleam
··· 25 25 |> result.try(int.parse) 26 26 |> result.unwrap(12_000) 27 27 28 - let db_name = process.new_name("database") 29 28 let assert Ok(database_url) = envoy.get("DATABASE_URL") 30 - let assert Ok(db_config) = pog.url_config(db_name, database_url) 29 + let db = process.new_name("database") 30 + let assert Ok(db_config) = pog.url_config(db, database_url) 31 31 let database = 32 32 db_config 33 33 |> pog.pool_size(10) 34 34 |> pog.supervised() 35 35 36 - let #(bus_process, bus_handles) = bus.supervised() 36 + let #(bus_process, bus) = bus.supervised() 37 37 38 - let game_state_watcher_supervisor_name = 39 - process.new_name("game_state_watcher_supervisor") 38 + let game_state_watchers = process.new_name("game_state_watchers") 40 39 let factory = 41 - factory_supervisor.worker_child(game_state_watcher.start) 42 - |> factory_supervisor.named(game_state_watcher_supervisor_name) 40 + factory_supervisor.worker_child(game_state_watcher.start(db, bus, _)) 41 + |> factory_supervisor.named(game_state_watchers) 43 42 |> factory_supervisor.supervised() 44 43 45 - let context = 46 - context.Context( 47 - db_name, 48 - bus_handles, 49 - game_state_watchers: game_state_watcher_supervisor_name, 50 - ) 44 + let context = context.Context(db:, bus:, game_state_watchers:) 51 45 let server = 52 46 mist.new(router.handler(_, context)) 53 47 |> mist.port(port)
+10
server/src/server/context.gleam
··· 16 16 ), 17 17 ) 18 18 } 19 + 20 + pub fn db(ctx: Context) -> pog.Connection { 21 + pog.named_connection(ctx.db) 22 + } 23 + 24 + pub fn start_game_state_watcher(ctx: Context, init: game_state_watcher.Init) { 25 + ctx.game_state_watchers 26 + |> factory_supervisor.get_by_name() 27 + |> factory_supervisor.start_child(init) 28 + }
+5 -21
server/src/websocket/state.gleam
··· 1 1 import actor/game_state_watcher 2 2 import bus 3 - import cartography_api/game_state 4 3 import gleam/dict.{type Dict} 5 4 import gleam/option.{type Option} 6 - import gleam/otp/factory_supervisor 7 - import mist 8 5 import pog 9 6 import server/context.{type Context} 10 7 import youid/uuid.{type Uuid} ··· 35 32 State(..state, account_id: option.Some(account_id)) 36 33 } 37 34 38 - pub fn db_connection(state: State) -> pog.Connection { 39 - pog.named_connection(state.context.db) 35 + pub fn db(state: State) -> pog.Connection { 36 + context.db(state.context) 40 37 } 41 38 42 39 pub fn bus(state: State) -> bus.Bus { ··· 58 55 State(..state, subscriptions: dict.delete(state.subscriptions, id)) 59 56 } 60 57 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 - )) 58 + pub fn start_game_state_watcher(state: State, init: game_state_watcher.Init) { 59 + state.context 60 + |> context.start_game_state_watcher(init) 77 61 }
+4 -4
src/lib/appserver/socket/SocketV1Protocol.ts
··· 115 115 }); 116 116 export type Citizen = StaticDecode<typeof Citizen>; 117 117 118 - export const Card = Type.Union([Struct("Tile", Tile), Struct("Citizen", Citizen)]); 119 - export type Card = StaticDecode<typeof Card>; 120 - 121 118 export const FieldTile = Type.Object({ id: TileId, x: Type.Integer(), y: Type.Integer() }); 122 119 export type FieldTile = StaticDecode<typeof FieldTile>; 123 120 ··· 137 134 export type GameStateField = StaticDecode<typeof GameStateField>; 138 135 139 136 export const GameState = Type.Object({ 140 - deck: Type.Array(Card), 137 + deck: Type.Object({ 138 + tiles: Type.Array(Tile), 139 + citizens: Type.Array(Citizen), 140 + }), 141 141 field: GameStateField, 142 142 }); 143 143 export type GameState = StaticDecode<typeof GameState>;