Most work done on single player game

This commit is contained in:
Lett Osprey 2026-04-12 21:19:28 +02:00
parent 584b1c9ef9
commit 09bf741997
8 changed files with 230 additions and 143 deletions

View file

@ -1,4 +1,7 @@
import components.{click_cell}
import components.{click_cell, click_cell_pair}
import gleam/bit_array
import gleam/crypto
import gleam/dynamic/decode
import gleam/erlang/process.{type Subject}
import gleam/int
import gleam/list
@ -9,13 +12,14 @@ import lustre/attribute.{class}
import lustre/effect.{type Effect}
import lustre/element.{type Element}
import lustre/element/html
import lustre/element/keyed
import lustre/event
import lustre/server_component
import shared/message.{type NotifyClient, type NotifyServer}
import web/components/shared.{
step_prompt, view_input, view_named_input, view_named_keyed_input, view_yes_no,
}
import web/components/shared
pub fn component() -> lustre.App(
#(List(#(Int, String)), message.ClientsServer),
#(List(#(String, String)), message.ClientsServer),
Model,
Msg,
) {
@ -26,14 +30,14 @@ pub opaque type Model {
Model(
state: Msg,
players: List(#(String, String)),
player_id: Option(String),
answers: List(#(Int, #(String, String))),
player: Option(#(String, String)),
answers: List(#(String, #(String, String))),
handler: Started(Subject(NotifyServer)),
)
}
fn init(
start_args: #(List(#(Int, String)), message.ClientsServer),
start_args: #(List(#(String, String)), message.ClientsServer),
) -> #(Model, Effect(Msg)) {
let #(answers, handlers) = start_args
let #(_registry, handler) = handlers
@ -42,11 +46,7 @@ fn init(
// "question number" -> #("question text", "users answer" array
// with blank user answers.
let initial_array =
list.filter(answers, fn(x) {
let #(i, _) = x
i <= 14 && i >= 0
})
|> list.map(fn(x) {
list.map(answers, fn(x) {
let #(a, b) = x
#(a, #(b, ""))
})
@ -65,68 +65,78 @@ fn init(
pub opaque type Msg {
Initial
PickedName(name: String)
PickedPlayer(player: Option(#(String, String)))
SharedMessage(message: NotifyClient)
ReceiveName(message: String)
AcceptName(accept: Option(String))
ReceiveName(name: Option(String))
AcceptPlayer(accept: Option(#(String, String)))
PickQuestion
PickedQuestion(question: String)
GiveAnswer(question: String, answer: String)
PickedQuestion(question: Option(#(String, String)))
GiveAnswer(question: #(String, String), answer: Option(String))
}
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
case msg {
Initial -> #(Model(..model, state: msg), effect.none())
PickedName(player_id) -> #(
Model(..model, player_id: Some(player_id), state: PickQuestion),
PickedPlayer(player) -> #(
case player {
Some(player) -> Model(..model, state: AcceptPlayer(Some(player)))
None -> Model(..model, state: ReceiveName(None))
},
effect.none(),
)
SharedMessage(_) -> #(model, effect.none())
AcceptName(None) -> #(
Model(Initial, model.players, None, [], model.handler),
effect.none(),
)
AcceptName(Some(player_id)) -> {
ReceiveName(Some(name)) -> {
let id =
bit_array.base64_encode(crypto.hash(crypto.Sha256, <<name:utf8>>), True)
#(Model(..model, state: AcceptPlayer(Some(#(id, name)))), effect.none())
}
AcceptPlayer(Some(player)) -> {
let #(_, player_name) = player
actor.send(model.handler.data, message.AddPlayer(player_name))
#(
Model(..model, player_id: Some(player_id), state: PickQuestion),
Model(..model, player: Some(player), state: PickQuestion),
effect.none(),
)
}
PickQuestion -> #(model, effect.none())
PickedQuestion(question) -> {
#(Model(..model, state: GiveAnswer(question, "")), effect.none())
PickedQuestion(Some(question)) -> {
#(
Model(..model, state: GiveAnswer(question:, answer: None)),
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())
GiveAnswer(question, Some(answer)) -> {
let #(question, _) = question
let assert Some(#(player_id, _)) = model.player
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(),
)
}
ReceiveName(_) -> #(Model(..model, state: msg), effect.none())
// Invalid states and "I want to start over" states|
GiveAnswer(_, None)
| AcceptPlayer(None)
| ReceiveName(None)
| PickedQuestion(None)
| PickQuestion -> #(
Model(Initial, model.players, None, model.answers, model.handler),
effect.none(),
)
}
}
@ -139,9 +149,10 @@ fn view(model: Model) -> Element(Msg) {
html.span([class("ml-8")], [
case model.state {
Initial -> html.text("STATUS: Please select player")
ReceiveName(_) -> html.text("STATUS: Please validate your name")
ReceiveName(_) -> html.text("STATUS: Please enter your name")
PickQuestion -> html.text("STATUS: Pick question to answer")
GiveAnswer(_, _) -> html.text("STATUS: Give your answer")
AcceptPlayer(_) -> html.text("STATUS: Validate player")
_ -> html.text("STATUS: Waiting for next question")
},
]),
@ -152,11 +163,29 @@ fn view(model: Model) -> Element(Msg) {
html.text("[ACTIVE TRANSMISSIONS]"),
]),
]),
case model.state {
Initial -> view_players(model.players)
PickQuestion -> view_questions(model.answers)
_ -> content_cell(#(10, #("Answer", "Answer question")))
},
html.div([class("participants-grid")], [
case model.state {
Initial ->
case model.players {
[] -> input_new_player()
_ -> view_players(model.players)
}
PickQuestion -> view_questions(model.answers)
ReceiveName(_) -> input_new_player()
AcceptPlayer(Some(player)) -> {
let #(_, player_name) = player
shared.confirm_cell(
Some("Join as this player: " <> player_name <> "?"),
player,
AcceptPlayer,
)
}
GiveAnswer(answer, None) -> input_new_answer(answer)
_ -> content_cell(#(10, #("Answer", "Answer question")))
},
]),
])
}
@ -166,18 +195,41 @@ fn view_players(players: List(#(String, String))) {
[],
list.append(
list.index_map(players, fn(item, index) {
click_cell(Some(int.to_string(index)), item, PickedName)
click_cell_pair(Some(int.to_string(index)), Some(item), PickedPlayer)
}),
[click_cell(Some("NEW"), #("new", "New Player!"), PickedName)],
[click_cell_pair(Some("ENTER NEW PLAYER"), None, PickedPlayer)],
),
),
])
}
fn view_questions(answers: List(#(Int, #(String, String)))) {
fn input_new_player() {
html.div([class("participant-box")], [
input_cell("Enter player name:", ReceiveName),
])
}
fn input_new_answer(question: #(String, String)) {
let #(question_id, question_text) = question
html.div([class("participant-box")], [
input_cell("Answer [" <> question_id <> "] " <> question_text, GiveAnswer(
question,
_,
)),
])
}
fn view_questions(answers: List(#(String, #(String, String)))) {
html.div(
[attribute.class("singles-grid")],
list.map(answers, fn(content) { content_cell(content) }),
[class("singles-grid")],
list.map(answers, fn(content) {
let #(number, #(question, answer)) = content
click_cell_pair(
Some(number <> " " <> answer),
Some(#(number, question)),
PickedQuestion,
)
}),
)
}
@ -197,3 +249,42 @@ fn content_cell(answer: #(Int, #(String, String))) -> Element(Msg) {
],
)
}
fn input_cell(
text: String,
on_submit handle_keydown: fn(Option(String)) -> msg,
) -> Element(msg) {
html.div([attribute.class("singles-grid")], [
html.div([], [html.text(text)]),
keyed.div([], [
#("inputheader", html.text("$>")),
#(
"input",
html.input([
attribute.type_("text"),
key_down(
fn(a: String) { decode.success(handle_keydown(Some(a))) },
fn() { decode.failure(handle_keydown(None), "") },
),
attribute.autofocus(True),
]),
),
]),
])
}
fn key_down(
success: fn(String) -> decode.Decoder(msg),
fail: fn() -> decode.Decoder(msg),
) {
event.on("keydown", {
use key <- decode.field("key", decode.string)
use value <- decode.subfield(["target", "value"], decode.string)
case key {
"Enter" if value != "" -> success(value)
_ -> fail()
}
})
|> server_component.include(["key", "target.value"])
}