Most work done on single player game
This commit is contained in:
parent
584b1c9ef9
commit
09bf741997
8 changed files with 230 additions and 143 deletions
|
|
@ -1,3 +1,5 @@
|
|||
import gleam/bit_array
|
||||
import gleam/crypto
|
||||
import gleam/erlang/process.{type Subject}
|
||||
import gleam/int
|
||||
import gleam/list
|
||||
|
|
@ -15,7 +17,7 @@ type State {
|
|||
State(
|
||||
question_number: Int,
|
||||
// id, (name (question#, answer_attempt)
|
||||
slow_answers: List(#(String, #(String, List(#(Int, String))))),
|
||||
slow_answers: List(#(String, #(String, List(#(String, String))))),
|
||||
// int in #pair: ping counted since response back.
|
||||
name_answers: List(#(String, #(Int, AnswerStatus))),
|
||||
hide_answers: Bool,
|
||||
|
|
@ -92,22 +94,25 @@ pub fn initialize(
|
|||
fetch_players(state.slow_answers, subject)
|
||||
state
|
||||
}
|
||||
message.AddPlayer(name:, subject:) -> {
|
||||
let _ = add_player(name, subject)
|
||||
state
|
||||
}
|
||||
message.AddPlayer(name) ->
|
||||
State(..state, slow_answers: add_player(name, state.slow_answers))
|
||||
}
|
||||
|> actor.continue()
|
||||
})
|
||||
|> actor.start
|
||||
}
|
||||
|
||||
fn add_player(_name: String, _subject: Subject(Result(String, String))) {
|
||||
#(200, "ok", "ok")
|
||||
fn add_player(name: String, players: List(#(String, #(String, List(#(_, _)))))) {
|
||||
let id =
|
||||
bit_array.base64_encode(crypto.hash(crypto.Sha256, <<name:utf8>>), True)
|
||||
case list.key_find(players, id) {
|
||||
Error(_) -> [#(id, #(name, [])), ..players]
|
||||
Ok(_) -> players
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_players(
|
||||
players: List(#(String, #(String, List(#(Int, String))))),
|
||||
players: List(#(String, #(String, List(#(_, String))))),
|
||||
subject: Subject(List(#(String, String))),
|
||||
) {
|
||||
actor.send(
|
||||
|
|
@ -246,7 +251,7 @@ fn combine_lists(state: State) {
|
|||
let #(_, #(name, answers)) = name_answers
|
||||
list.filter_map(answers, fn(number_answer) {
|
||||
let #(answer_number, answer) = number_answer
|
||||
case state.question_number == answer_number {
|
||||
case int.to_string(state.question_number) == answer_number {
|
||||
True -> {
|
||||
Ok(
|
||||
User(name, 0, case state.hide_answers {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ pub fn serve(
|
|||
|
||||
pub fn serve_slow(
|
||||
request: Request(Connection),
|
||||
component: lustre.App(#(List(#(Int, String)), message.ClientsServer), model, msg),
|
||||
component: lustre.App(#(List(#(String, String)), message.ClientsServer), model, msg),
|
||||
id: String,
|
||||
roomhandler: actor.Started(Subject(message.RoomControl)),
|
||||
statehandler: actor.Started(Subject(message.StateControl)),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import gleam/int
|
||||
import gleam/list
|
||||
import gleam/option.{type Option, None, Some}
|
||||
import gleam/otp/actor
|
||||
|
|
@ -63,7 +64,7 @@ pub fn initialize() {
|
|||
subject,
|
||||
list.map(state.questions, fn(x) {
|
||||
let #(i, #(q, _)) = x
|
||||
#(i, q)
|
||||
#(int.to_string(i), q)
|
||||
}),
|
||||
)
|
||||
state
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ pub type NotifyServer {
|
|||
PurgePlayers
|
||||
GiveName(name: String)
|
||||
GiveAnswer(name: String, answer: Option(String))
|
||||
GiveSingleAnswer(id: String, question: Int, answer: String)
|
||||
GiveSingleAnswer(id: String, question: String, answer: String)
|
||||
FetchPlayers(subject: Subject(List(#(String, String))))
|
||||
AddPlayer(name: String, subject: Subject(Result(String, String)))
|
||||
AddPlayer(String)
|
||||
}
|
||||
|
||||
pub type StateControl {
|
||||
|
|
@ -24,7 +24,7 @@ pub type StateControl {
|
|||
SetAnswer(id: Int, answer: String)
|
||||
SetInfo(url: String)
|
||||
FetchQuestion(id: Int, subject: Subject(Option(String)))
|
||||
FetchQuestions(subject: Subject(List(#(Int, String))))
|
||||
FetchQuestions(subject: Subject(List(#(String, String))))
|
||||
}
|
||||
|
||||
pub type Room {
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ pub fn step_prompt(text: String, fetch: fn() -> Element(a)) {
|
|||
]),
|
||||
])
|
||||
}
|
||||
|
||||
//fn xiew_player_list(items: List(String)) -> Element(Msg) {
|
||||
// layout("Select or enter your player", None, case items {
|
||||
// [] -> [html.text("No items in your list yet.")]
|
||||
|
|
@ -115,3 +116,22 @@ pub fn step_prompt(text: String, fetch: fn() -> Element(a)) {
|
|||
// }
|
||||
// })
|
||||
//
|
||||
pub fn confirm_cell(
|
||||
title: Option(String),
|
||||
accepted: #(String, String),
|
||||
on_submit handle_button: fn(Option(#(String, String))) -> msg,
|
||||
) -> Element(msg) {
|
||||
html.div([attribute.class("participant-login")], [
|
||||
html.div([attribute.class("participant-name")], [
|
||||
case title {
|
||||
Some(title) -> html.div([], [html.text(title)])
|
||||
None -> element.none()
|
||||
},
|
||||
html.button([event.on_click(handle_button(Some(accepted)))], [
|
||||
html.text(" <Yes> "),
|
||||
]),
|
||||
html.text(" - "),
|
||||
html.button([event.on_click(handle_button(None))], [html.text(" <No> ")]),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import gleam/dynamic/decode
|
|||
import gleam/erlang/process.{type Subject}
|
||||
import gleam/http
|
||||
import gleam/int
|
||||
import gleam/json
|
||||
import gleam/list
|
||||
import gleam/option
|
||||
import gleam/otp/actor.{type Started}
|
||||
import shared/message.{type RoomControl, type StateControl}
|
||||
import web/handlers/serve.{html_404}
|
||||
|
|
@ -21,7 +19,6 @@ pub fn handle_request(
|
|||
case wisp.path_segments(req) {
|
||||
[] | ["index.html"] -> serve.main_html(fetch_rooms(room_handler))
|
||||
["api", "room"] -> handle_room(room_handler, req)
|
||||
["api", "room", id, ..path] -> handle_players(room_handler, req, id, path)
|
||||
["api", ..path] -> handle_admin_api(state_handler, req, path)
|
||||
_ -> html_404()
|
||||
}
|
||||
|
|
@ -37,29 +34,6 @@ fn handle_room(room_handler: Started(Subject(RoomControl)), req: Request) {
|
|||
|> serve.create_json_response
|
||||
}
|
||||
|
||||
fn handle_players(
|
||||
room_handler: Started(Subject(RoomControl)),
|
||||
req: Request,
|
||||
id: String,
|
||||
path: List(String),
|
||||
) {
|
||||
use json <- wisp.require_json(req)
|
||||
|
||||
case actor.call(room_handler.data, 1000, message.FetchRoom(id, _)) {
|
||||
option.Some(#(_, player_handler)) ->
|
||||
case req.method, path {
|
||||
http.Post, ["player"] -> add_player(player_handler, json)
|
||||
_, _ -> #(404, "bad api path", "Resource not found")
|
||||
}
|
||||
option.None -> #(
|
||||
404,
|
||||
"Room not found, or key invalid",
|
||||
"resource not found",
|
||||
)
|
||||
}
|
||||
|> serve.create_json_response
|
||||
}
|
||||
|
||||
fn handle_admin_api(
|
||||
actor: Started(Subject(StateControl)),
|
||||
req: Request,
|
||||
|
|
@ -117,37 +91,6 @@ fn decode_info(
|
|||
}
|
||||
}
|
||||
|
||||
fn add_player(
|
||||
player_handler: Started(Subject(message.NotifyServer)),
|
||||
json_string: decode.Dynamic,
|
||||
) {
|
||||
let decode_player = {
|
||||
use name <- decode.field("name", decode.string)
|
||||
decode.success(name)
|
||||
}
|
||||
case decode.run(json_string, decode_player) {
|
||||
Ok(player) -> {
|
||||
case actor.call(player_handler.data, 1000, message.AddPlayer(player, _)) {
|
||||
Ok(id) -> #(
|
||||
200,
|
||||
"Added player with name [" <> player <> "] given id [" <> id <> "]",
|
||||
json.to_string(json.object([#("id", json.string(id))])),
|
||||
)
|
||||
Error(msg) -> #(
|
||||
400,
|
||||
"Unable to add player [" <> msg <> "]",
|
||||
"player not added",
|
||||
)
|
||||
}
|
||||
}
|
||||
Error(_msg) -> #(
|
||||
400,
|
||||
"Could not parse player [decoding error]",
|
||||
"bad request",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn add_room(room_handler: Started(Subject(RoomControl)), json) {
|
||||
let decode_room = {
|
||||
use id <- decode.field("id", decode.string)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue