Single player game close to finished

This commit is contained in:
Lett Osprey 2026-04-11 14:11:11 +02:00
parent 3385118b14
commit 584b1c9ef9
12 changed files with 179 additions and 6454 deletions

View file

@ -12,3 +12,4 @@ lustre = ">= 5.3.5 and < 6.0.0"
gleam_crypto = ">= 1.5.1 and < 2.0.0"
group_registry = ">= 1.0.0 and < 2.0.0"
wisp = ">= 2.2.1 and < 3.0.0"
shared = { path = "../shared" }

View file

@ -22,6 +22,7 @@ packages = [
{ name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" },
{ name = "mist", version = "6.0.2", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "6B03DEEA38A02F276333CB27B53B16D3D45BD741B89599085A601BAF635F2006" },
{ name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" },
{ name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_json", "gleam_stdlib", "lustre"], source = "local", path = "../shared" },
{ name = "simplifile", version = "2.4.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "7C18AFA4FED0B4CE1FA5B0B4BAC1FA1744427054EA993565F6F3F82E5453170D" },
{ name = "wisp", version = "2.2.2", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "5FF5F1E288C3437252ABB93D8F9CF42FF652CE7AD54480CFE736038DC09C4F22" },
]
@ -36,4 +37,5 @@ gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
group_registry = { version = ">= 1.0.0 and < 2.0.0" }
lustre = { version = ">= 5.3.5 and < 6.0.0" }
mist = { version = ">= 6.0.0 and < 7.0.0" }
shared = { path = "../shared" }
wisp = { version = ">= 2.2.1 and < 3.0.0" }

View file

@ -1,3 +1,4 @@
import components.{click_cell}
import gleam/erlang/process.{type Subject}
import gleam/int
import gleam/list
@ -24,6 +25,8 @@ pub fn component() -> lustre.App(
pub opaque type Model {
Model(
state: Msg,
players: List(#(String, String)),
player_id: Option(String),
answers: List(#(Int, #(String, String))),
handler: Started(Subject(NotifyServer)),
)
@ -48,122 +51,136 @@ fn init(
#(a, #(b, ""))
})
#(Model(Initial, initial_array, handler), effect.none())
#(
Model(
Initial,
actor.call(handler.data, 1000, message.FetchPlayers),
None,
initial_array,
handler,
),
effect.none(),
)
}
pub opaque type Msg {
Initial
PickedName(name: String)
SharedMessage(message: NotifyClient)
ReceiveName(message: String)
AcceptName(accept: Option(String))
GiveQuestion(name: String, question: String)
GiveAnswer(name: String, question: Int, answer: String)
PickQuestion
PickedQuestion(question: String)
GiveAnswer(question: String, answer: String)
}
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
case msg {
Initial | SharedMessage(_) -> #(model, effect.none())
AcceptName(None) -> #(Model(Initial, [], model.handler), effect.none())
AcceptName(Some(name)) -> {
#(Model(..model, state: GiveQuestion(name, "")), effect.none())
}
GiveQuestion(name, question) ->
case int.parse(question) {
Ok(question) if question >= 1 && question <= 14 -> #(
Model(..model, state: GiveAnswer(name:, question:, answer: "")),
effect.none(),
)
_ -> #(
Model(..model, state: GiveQuestion(name:, question: "")),
effect.none(),
)
}
GiveAnswer(id, question, answer) -> {
actor.send(
model.handler.data,
message.GiveSingleAnswer(id:, question:, answer:),
)
let new_value = case list.key_find(model.answers, question) {
Ok(pair) -> {
let #(a, _) = pair
#(a, answer)
}
Error(_) -> #("", answer)
}
Initial -> #(Model(..model, state: msg), effect.none())
PickedName(player_id) -> #(
Model(..model, player_id: Some(player_id), state: PickQuestion),
effect.none(),
)
SharedMessage(_) -> #(model, effect.none())
AcceptName(None) -> #(
Model(Initial, model.players, None, [], model.handler),
effect.none(),
)
AcceptName(Some(player_id)) -> {
#(
Model(
..model,
state: GiveQuestion(id, ""),
answers: list.key_set(model.answers, question, new_value),
),
Model(..model, player_id: Some(player_id), state: PickQuestion),
effect.none(),
)
}
PickQuestion -> #(model, effect.none())
PickedQuestion(question) -> {
#(Model(..model, state: GiveAnswer(question, "")), effect.none())
}
GiveAnswer(question, answer) -> {
let assert Some(player_id) = model.player_id
case int.parse(question) {
Ok(question) -> {
actor.send(
model.handler.data,
message.GiveSingleAnswer(id: player_id, question:, answer:),
)
let new_value = case list.key_find(model.answers, question) {
Ok(pair) -> {
let #(a, _) = pair
#(a, answer)
}
Error(_) -> #("", answer)
}
#(
Model(
..model,
state: PickQuestion,
answers: list.key_set(model.answers, question, new_value),
),
effect.none(),
)
}
_ -> {
echo "bad index"
#(model, effect.none())
}
}
}
ReceiveName(_) -> #(Model(..model, state: msg), effect.none())
}
}
fn view(model: Model) -> Element(Msg) {
element.fragment([
html.div([attribute.class("terminal-prompt")], [
case model.state {
Initial ->
step_prompt(
"Hello stranger. To join the quiz, I need to know your name",
fn() { view_input(ReceiveName) },
)
ReceiveName(name) ->
step_prompt(
"Your name is " <> name <> "? Are you absolutely sure???",
fn() { view_yes_no(name, AcceptName) },
)
GiveQuestion(name, _) ->
step_prompt(
"Enter the number of the question you want to answer",
fn() { view_named_input(name, GiveQuestion) },
)
GiveAnswer(name, question, _) ->
step_prompt(
"Enter the answer to question number " <> int.to_string(question),
fn() { view_named_keyed_input(question, name, GiveAnswer) },
)
_ -> html.h3([], [html.text("Waiting for next question")])
},
]),
html.div([class("terminal-header")], [
html.div([class("terminal-status")], [
html.span([class("status-blink")], [html.text("")]),
html.text(" SYSTEM READY"),
html.span([class("ml-8")], [
case model.state {
Initial -> html.text("STATUS: Please input your name")
Initial -> html.text("STATUS: Please select player")
ReceiveName(_) -> html.text("STATUS: Please validate your name")
GiveQuestion(_, _) -> html.text("STATUS: Pick question to answer")
GiveAnswer(_, _, _) -> html.text("STATUS: Give your answer")
PickQuestion -> html.text("STATUS: Pick question to answer")
GiveAnswer(_, _) -> html.text("STATUS: Give your answer")
_ -> html.text("STATUS: Waiting for next question")
},
]),
]),
]),
terminal_section(model.answers, "[ACTIVE TRANSMISSIONS]", fn(answer) {
content_cell(answer)
}),
html.div([attribute.class("terminal-section")], [
html.div([attribute.class("terminal-label mb-4")], [
html.text("[ACTIVE TRANSMISSIONS]"),
]),
]),
case model.state {
Initial -> view_players(model.players)
PickQuestion -> view_questions(model.answers)
_ -> content_cell(#(10, #("Answer", "Answer question")))
},
])
}
fn terminal_section(
answers: List(#(Int, #(String, String))),
header: String,
extract: fn(#(Int, #(String, String))) -> Element(Msg),
) {
html.div([attribute.class("terminal-section")], [
html.div([attribute.class("terminal-label mb-4")], [
html.text(header),
]),
html.div([attribute.class("singles-grid")], list.map(answers, extract)),
fn view_players(players: List(#(String, String))) {
html.div([], [
html.div(
[],
list.append(
list.index_map(players, fn(item, index) {
click_cell(Some(int.to_string(index)), item, PickedName)
}),
[click_cell(Some("NEW"), #("new", "New Player!"), PickedName)],
),
),
])
}
fn view_questions(answers: List(#(Int, #(String, String)))) {
html.div(
[attribute.class("singles-grid")],
list.map(answers, fn(content) { content_cell(content) }),
)
}
fn content_cell(answer: #(Int, #(String, String))) -> Element(Msg) {
let #(question, #(question_text, answer)) = answer
html.div(

View file

@ -1,9 +1,9 @@
import shared/message
import gleam/int
import gleam/json
import lustre/attribute.{class}
import lustre/element
import lustre/element/html.{body, div, head, html, link, meta, script, title}
import shared/message.{RoomInfo}
import wisp.{type Response}
pub fn main_html(rooms: List(#(String, message.RoomInfo))) -> Response {
@ -24,19 +24,16 @@ pub fn main_html(rooms: List(#(String, message.RoomInfo))) -> Response {
[
attribute.id("model"),
attribute.type_("application/json"),
attribute.src("/client.js"),
],
TODO: CREATE TEXT STRING FROM ROOM LIST!
"
[
{
\"id\": \"1234\",
\"name\": \"Team A\",
\"key\": \"1234\"
}
]
",
json.array(rooms, fn(room) {
let #(id, RoomInfo(name, pin_enc)) = room
json.object([
#("id", json.string(id)),
#("name", json.string(name)),
#("key", json.string(pin_enc)),
])
})
|> json.to_string,
),
link([
attribute.rel("stylesheet"),

View file

@ -151,7 +151,7 @@ fn add_player(
fn add_room(room_handler: Started(Subject(RoomControl)), json) {
let decode_room = {
use id <- decode.field("id", decode.string)
use pin_enc <- decode.field("pin", decode.string)
use pin_enc <- decode.field("pin_enc", decode.string)
use name <- decode.field("name", decode.string)
decode.success(message.CreateRoom(
id:,