tangled
alpha
login
or
join now
eldridge.cam
/
cartography
0
fork
atom
Trading card city builder game?
0
fork
atom
overview
issues
pulls
pipelines
build out more fully some api interface
eldridge.cam
1 month ago
16eb3a66
583b9f68
+341
-35
9 changed files
expand all
collapse all
unified
split
Justfile
api
gleam.toml
manifest.toml
src
game_state.gleam
repr.gleam
request.gleam
response.gleam
server
gleam.toml
manifest.toml
+2
Justfile
···
48
48
[group: "dev"]
49
49
get:
50
50
npm install
51
51
+
cd server && gleam deps download
52
52
+
cd api && gleam deps download
51
53
52
54
[group: "docker"]
53
55
up: && migrate
+1
-1
api/gleam.toml
···
20
20
[dependencies]
21
21
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
22
22
gleam_json = ">= 3.1.0 and < 4.0.0"
23
23
-
glepack = { git = "https://github.com/Lemorz56/glepack", ref = "main" }
23
23
+
squirtle = ">= 1.4.0 and < 2.0.0"
24
24
25
25
[dev-dependencies]
26
26
gleeunit = ">= 1.0.0 and < 2.0.0"
+2
-2
api/manifest.toml
···
5
5
{ name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
6
6
{ name = "gleam_stdlib", version = "0.68.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F7FAEBD8EF260664E86A46C8DBA23508D1D11BB3BCC6EE1B89B3BC3E5C83FF1E" },
7
7
{ name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
8
8
-
{ name = "glepack", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "git", repo = "https://github.com/Lemorz56/glepack", commit = "7a7df549845ccec8f92c757adbfb9c76e6c70692" },
8
8
+
{ name = "squirtle", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "squirtle", source = "hex", outer_checksum = "B4DCF7ED3E829E1DB6163F3E18677EF0862DF3F4CE076D98628EBB8D9BC974BC" },
9
9
]
10
10
11
11
[requirements]
12
12
gleam_json = { version = ">= 3.1.0 and < 4.0.0" }
13
13
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
14
14
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
15
15
-
glepack = { git = "https://github.com/Lemorz56/glepack", ref = "main" }
15
15
+
squirtle = { version = ">= 1.4.0 and < 2.0.0" }
+227
api/src/game_state.gleam
···
1
1
+
import gleam/dynamic/decode
2
2
+
import gleam/json.{type Json}
3
3
+
import gleam/list
4
4
+
import gleam/option.{type Option}
5
5
+
import repr
6
6
+
import squirtle
7
7
+
8
8
+
/// Defines a shared game state data model, which the server manages on behalf of a client
9
9
+
/// as a definitive source of truth.
10
10
+
pub type GameState {
11
11
+
GameState(deck: Deck, field: Field)
12
12
+
}
13
13
+
14
14
+
pub type CardId {
15
15
+
TileId(id: Int)
16
16
+
CitizenId(id: Int)
17
17
+
}
18
18
+
19
19
+
pub type CardTypeId {
20
20
+
TileTypeId(id: String)
21
21
+
SpeciesId(id: String)
22
22
+
}
23
23
+
24
24
+
pub type FieldId {
25
25
+
FieldId(id: Int)
26
26
+
}
27
27
+
28
28
+
pub type Deck {
29
29
+
Deck(cards: List(Card))
30
30
+
}
31
31
+
32
32
+
pub type Card {
33
33
+
Tile(id: CardId, tile_type_id: CardTypeId, name: String)
34
34
+
Citizen(
35
35
+
id: CardId,
36
36
+
species_id: CardTypeId,
37
37
+
name: String,
38
38
+
home_tile_id: Option(CardId),
39
39
+
)
40
40
+
}
41
41
+
42
42
+
pub type Field {
43
43
+
Field(id: FieldId, tiles: List(FieldTile), citizens: List(FieldCitizen))
44
44
+
}
45
45
+
46
46
+
pub type FieldTile {
47
47
+
FieldTile(id: CardId, x: Int, y: Int)
48
48
+
}
49
49
+
50
50
+
pub type FieldCitizen {
51
51
+
FieldCitizen(id: CardId, x: Int, y: Int)
52
52
+
}
53
53
+
54
54
+
pub fn to_json(game_state: GameState) -> Json {
55
55
+
json.object([
56
56
+
#(
57
57
+
"deck",
58
58
+
json.object([
59
59
+
#(
60
60
+
"cards",
61
61
+
game_state.deck.cards
62
62
+
|> list.map(fn(card) {
63
63
+
case card {
64
64
+
Tile(TileId(id), TileTypeId(tile_type_id), name) ->
65
65
+
json.object([
66
66
+
#("id", json.int(id)),
67
67
+
#("tile_type_id", json.string(tile_type_id)),
68
68
+
#("name", json.string(name)),
69
69
+
])
70
70
+
|> repr.struct("Tile")
71
71
+
Citizen(
72
72
+
CitizenId(id),
73
73
+
SpeciesId(species_id),
74
74
+
name,
75
75
+
home_tile_id,
76
76
+
) ->
77
77
+
json.object([
78
78
+
#("id", json.int(id)),
79
79
+
#("species_id", json.string(species_id)),
80
80
+
#("name", json.string(name)),
81
81
+
#(
82
82
+
"home_tile_id",
83
83
+
json.nullable(home_tile_id, fn(id) { json.int(id.id) }),
84
84
+
),
85
85
+
])
86
86
+
_ -> panic as "unreachable"
87
87
+
}
88
88
+
})
89
89
+
|> json.preprocessed_array(),
90
90
+
),
91
91
+
]),
92
92
+
),
93
93
+
#(
94
94
+
"field",
95
95
+
json.object([
96
96
+
#("id", json.int(game_state.field.id.id)),
97
97
+
#(
98
98
+
"tiles",
99
99
+
game_state.field.tiles
100
100
+
|> list.map(fn(tile) {
101
101
+
json.object([
102
102
+
#("id", json.int(tile.id.id)),
103
103
+
#("x", json.int(tile.x)),
104
104
+
#("y", json.int(tile.y)),
105
105
+
])
106
106
+
})
107
107
+
|> json.preprocessed_array(),
108
108
+
),
109
109
+
#(
110
110
+
"citizens",
111
111
+
game_state.field.citizens
112
112
+
|> list.map(fn(citizen) {
113
113
+
json.object([
114
114
+
#("id", json.int(citizen.id.id)),
115
115
+
#("x", json.int(citizen.x)),
116
116
+
#("y", json.int(citizen.y)),
117
117
+
])
118
118
+
})
119
119
+
|> json.preprocessed_array(),
120
120
+
),
121
121
+
]),
122
122
+
),
123
123
+
])
124
124
+
}
125
125
+
126
126
+
pub fn to_string(game_state: GameState) -> String {
127
127
+
game_state
128
128
+
|> to_json()
129
129
+
|> json.to_string()
130
130
+
}
131
131
+
132
132
+
pub fn decoder() -> decode.Decoder(GameState) {
133
133
+
use deck <- decode.field("deck", {
134
134
+
use cards <- decode.field(
135
135
+
"cards",
136
136
+
decode.list({
137
137
+
use tag <- repr.struct_tag(Tile(
138
138
+
id: TileId(0),
139
139
+
tile_type_id: TileTypeId(""),
140
140
+
name: "",
141
141
+
))
142
142
+
case tag {
143
143
+
"Tile" -> {
144
144
+
use id <- decode.field("id", decode.map(decode.int, TileId))
145
145
+
use tile_type_id <- decode.field(
146
146
+
"tile_type_id",
147
147
+
decode.map(decode.string, TileTypeId),
148
148
+
)
149
149
+
use name <- decode.field("name", decode.string)
150
150
+
decode.success(Tile(id:, tile_type_id:, name:))
151
151
+
}
152
152
+
"Citizen" -> {
153
153
+
use id <- decode.field("id", decode.map(decode.int, CitizenId))
154
154
+
use species_id <- decode.field(
155
155
+
"species_id",
156
156
+
decode.map(decode.string, SpeciesId),
157
157
+
)
158
158
+
use name <- decode.field("name", decode.string)
159
159
+
use home_tile_id <- decode.field(
160
160
+
"home_tile_id",
161
161
+
decode.optional(decode.map(decode.int, TileId)),
162
162
+
)
163
163
+
decode.success(Citizen(id:, species_id:, name:, home_tile_id:))
164
164
+
}
165
165
+
_ -> {
166
166
+
decode.failure(
167
167
+
Tile(id: TileId(0), tile_type_id: TileTypeId(""), name: ""),
168
168
+
"card",
169
169
+
)
170
170
+
}
171
171
+
}
172
172
+
}),
173
173
+
)
174
174
+
decode.success(Deck(cards:))
175
175
+
})
176
176
+
use field <- decode.field("field", {
177
177
+
use id <- decode.field("id", decode.map(decode.int, FieldId))
178
178
+
use tiles <- decode.field(
179
179
+
"tiles",
180
180
+
decode.list({
181
181
+
use id <- decode.field("id", decode.map(decode.int, TileId))
182
182
+
use x <- decode.field("x", decode.int)
183
183
+
use y <- decode.field("y", decode.int)
184
184
+
decode.success(FieldTile(id:, x:, y:))
185
185
+
}),
186
186
+
)
187
187
+
use citizens <- decode.field(
188
188
+
"citizens",
189
189
+
decode.list({
190
190
+
use id <- decode.field("id", decode.map(decode.int, CitizenId))
191
191
+
use x <- decode.field("x", decode.int)
192
192
+
use y <- decode.field("y", decode.int)
193
193
+
decode.success(FieldCitizen(id:, x:, y:))
194
194
+
}),
195
195
+
)
196
196
+
decode.success(Field(id:, tiles:, citizens:))
197
197
+
})
198
198
+
decode.success(GameState(deck:, field:))
199
199
+
}
200
200
+
201
201
+
pub fn diff(previous: GameState, next: GameState) -> List(squirtle.Patch) {
202
202
+
let assert Ok(previous) =
203
203
+
previous
204
204
+
|> to_string()
205
205
+
|> squirtle.json_value_parse()
206
206
+
let assert Ok(next) =
207
207
+
next
208
208
+
|> to_string()
209
209
+
|> squirtle.json_value_parse()
210
210
+
squirtle.diff(previous, next)
211
211
+
}
212
212
+
213
213
+
pub fn patch(
214
214
+
previous: GameState,
215
215
+
patches: List(squirtle.Patch),
216
216
+
) -> Result(GameState, List(decode.DecodeError)) {
217
217
+
let assert Ok(previous) =
218
218
+
previous
219
219
+
|> to_string()
220
220
+
|> squirtle.json_value_parse()
221
221
+
let assert Ok(next) =
222
222
+
// NOTE: this one might fail if the patches are bad, should handle that better,
223
223
+
// but realistically if there are bad patches, it means we have lost the server.
224
224
+
previous
225
225
+
|> squirtle.patch(patches)
226
226
+
squirtle.json_value_decode(next, decoder())
227
227
+
}
+30
api/src/repr.gleam
···
1
1
+
import gleam/dynamic/decode.{type Decoder}
2
2
+
import gleam/json.{type Json}
3
3
+
4
4
+
pub fn struct(payload: Json, tag: String) -> Json {
5
5
+
json.object([
6
6
+
#("#type", json.string("struct")),
7
7
+
#("#tag", json.string(tag)),
8
8
+
#("#payload", payload),
9
9
+
])
10
10
+
}
11
11
+
12
12
+
pub fn struct_tag(fallback: t, cb: fn(String) -> Decoder(t)) -> Decoder(t) {
13
13
+
use ty <- decode.field("#type", decode.string)
14
14
+
case ty {
15
15
+
"struct" -> {
16
16
+
use tag <- decode.field("#tag", decode.string)
17
17
+
cb(tag)
18
18
+
}
19
19
+
_ -> {
20
20
+
decode.failure(fallback, "struct")
21
21
+
}
22
22
+
}
23
23
+
}
24
24
+
25
25
+
pub fn struct_payload(
26
26
+
decoder: Decoder(f),
27
27
+
cb: fn(f) -> Decoder(t),
28
28
+
) -> Decoder(t) {
29
29
+
decode.field("#payload", decoder, cb)
30
30
+
}
+21
-31
api/src/request.gleam
···
1
1
import gleam/dynamic/decode
2
2
import gleam/json
3
3
+
import repr
3
4
5
5
+
/// A request is sent from the client to the server.
6
6
+
///
7
7
+
/// A response does not necessarily respond to something, it might just be a pushed notification.
4
8
pub opaque type Request {
5
9
Authenticate(auth_token: String)
6
10
DebugAddCard(card_id: String)
7
11
}
8
12
9
9
-
pub fn to_text(request: Request) -> String {
13
13
+
pub fn to_string(request: Request) -> String {
10
14
json.to_string(case request {
11
15
Authenticate(auth_token) ->
12
12
-
json.object([
13
13
-
#("#type", json.string("struct")),
14
14
-
#("#tag", json.string("Authenticate")),
15
15
-
#("#payload", json.string(auth_token)),
16
16
-
])
16
16
+
json.string(auth_token)
17
17
+
|> repr.struct("Authenticate")
17
18
DebugAddCard(card_id) ->
18
18
-
json.object([
19
19
-
#("#type", json.string("struct")),
20
20
-
#("#tag", json.string("DebugAddCard")),
21
21
-
#("#payload", json.string(card_id)),
22
22
-
])
19
19
+
json.string(card_id)
20
20
+
|> repr.struct("DebugAddCard")
23
21
})
24
22
}
25
23
26
26
-
pub fn from_text(text: String) -> Result(Request, json.DecodeError) {
27
27
-
json.parse(text, {
28
28
-
use ty <- decode.field("#type", decode.string)
29
29
-
case ty {
30
30
-
"struct" -> {
31
31
-
use tag <- decode.field("#tag", decode.string)
32
32
-
case tag {
33
33
-
"Authenticate" -> {
34
34
-
use payload <- decode.field("#payload", decode.string)
35
35
-
decode.success(Authenticate(payload))
36
36
-
}
37
37
-
"DebugAddCard" -> {
38
38
-
use payload <- decode.field("#payload", decode.string)
39
39
-
decode.success(DebugAddCard(payload))
40
40
-
}
41
41
-
_ -> {
42
42
-
decode.failure(Authenticate(""), "valid #tag")
43
43
-
}
44
44
-
}
24
24
+
pub fn from_string(string: String) -> Result(Request, json.DecodeError) {
25
25
+
json.parse(string, {
26
26
+
use tag <- repr.struct_tag(Authenticate(""))
27
27
+
case tag {
28
28
+
"Authenticate" -> {
29
29
+
use payload <- repr.struct_payload(decode.string)
30
30
+
decode.success(Authenticate(payload))
31
31
+
}
32
32
+
"DebugAddCard" -> {
33
33
+
use payload <- repr.struct_payload(decode.string)
34
34
+
decode.success(DebugAddCard(payload))
45
35
}
46
36
_ -> {
47
47
-
decode.failure(Authenticate(""), "#type == 'struct'")
37
37
+
decode.failure(Authenticate(""), "valid #tag")
48
38
}
49
39
}
50
40
})
+54
api/src/response.gleam
···
1
1
+
import game_state.{type GameState}
2
2
+
import gleam/dynamic/decode
3
3
+
import gleam/json
4
4
+
import gleam/list
5
5
+
import repr
6
6
+
import squirtle.{type Patch}
1
7
8
8
+
/// A response is sent from the server to the client.
9
9
+
pub opaque type Response {
10
10
+
PutData(GameState)
11
11
+
PatchData(List(Patch))
12
12
+
}
13
13
+
14
14
+
pub fn to_string(response: Response) -> String {
15
15
+
json.to_string(case response {
16
16
+
PutData(game_state) ->
17
17
+
game_state.to_json(game_state)
18
18
+
|> repr.struct("PutData")
19
19
+
PatchData(patches) ->
20
20
+
patches
21
21
+
|> list.map(squirtle.patch_to_json_value)
22
22
+
|> list.map(squirtle.json_value_to_gleam_json)
23
23
+
|> json.preprocessed_array()
24
24
+
|> repr.struct("PatchData")
25
25
+
})
26
26
+
}
27
27
+
28
28
+
pub fn from_string(string: String) -> Result(Response, json.DecodeError) {
29
29
+
json.parse(string, {
30
30
+
use tag <- repr.struct_tag(PatchData([]))
31
31
+
case tag {
32
32
+
"PutData" -> {
33
33
+
use payload <- repr.struct_payload(game_state.decoder())
34
34
+
decode.success(PutData(payload))
35
35
+
}
36
36
+
"PatchData" -> {
37
37
+
use payload <- repr.struct_payload(
38
38
+
decode.list(squirtle.patch_decoder()),
39
39
+
)
40
40
+
decode.success(PatchData(payload))
41
41
+
}
42
42
+
_ -> {
43
43
+
decode.failure(PatchData([]), "valid #tag")
44
44
+
}
45
45
+
}
46
46
+
})
47
47
+
}
48
48
+
49
49
+
pub fn put_data(game_state: GameState) -> Response {
50
50
+
PutData(game_state)
51
51
+
}
52
52
+
53
53
+
pub fn patch_data(patches: List(Patch)) -> Response {
54
54
+
PatchData(patches)
55
55
+
}
+1
server/gleam.toml
···
25
25
palabres = ">= 1.0.3 and < 2.0.0"
26
26
squirrel = ">= 4.6.0 and < 5.0.0"
27
27
cartography_api = { path = "../api" }
28
28
+
squirtle = ">= 1.4.0 and < 2.0.0"
28
29
29
30
[dev-dependencies]
30
31
gleeunit = ">= 1.0.0 and < 2.0.0"
+3
-1
server/manifest.toml
···
4
4
packages = [
5
5
{ name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
6
6
{ name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" },
7
7
-
{ name = "cartography_api", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "glepack"], source = "local", path = "../api" },
7
7
+
{ name = "cartography_api", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "squirtle"], source = "local", path = "../api" },
8
8
{ name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" },
9
9
{ name = "eval", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "eval", source = "hex", outer_checksum = "264DAF4B49DF807F303CA4A4E4EBC012070429E40BE384C58FE094C4958F9BDA" },
10
10
{ name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" },
···
40
40
{ name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" },
41
41
{ name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" },
42
42
{ name = "squirrel", version = "4.6.0", build_tools = ["gleam"], requirements = ["argv", "envoy", "eval", "filepath", "glam", "gleam_community_ansi", "gleam_crypto", "gleam_json", "gleam_regexp", "gleam_stdlib", "gleam_time", "glexer", "justin", "mug", "non_empty_list", "pog", "simplifile", "term_size", "tom", "tote", "youid"], otp_app = "squirrel", source = "hex", outer_checksum = "0ED10A868BDD1A5D4B68D99CD1C72DC3F23C6E36E16D33454C5F0C31BAC9CB1E" },
43
43
+
{ name = "squirtle", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "squirtle", source = "hex", outer_checksum = "B4DCF7ED3E829E1DB6163F3E18677EF0862DF3F4CE076D98628EBB8D9BC974BC" },
43
44
{ name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" },
44
45
{ name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
45
46
{ name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" },
···
61
62
palabres = { version = ">= 1.0.3 and < 2.0.0" }
62
63
pog = { version = ">= 4.1.0 and < 5.0.0" }
63
64
squirrel = { version = ">= 4.6.0 and < 5.0.0" }
65
65
+
squirtle = { version = ">= 1.4.0 and < 2.0.0" }