Web app first pass
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
justthefrax
|
||||
132
client.go
Normal file
132
client.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
writeWait = 10 * time.Second // Time allowed to write a message to the peer.
|
||||
pongWait = 60 * time.Second // Time allowed to read the next pong message from the peer.
|
||||
pingPeriod = (pongWait * 9) / 10 // Send pings to peer with this period. Must be less than pongWait.
|
||||
maxMessageSize = 512 // Maximum message size allowed from peer.
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
type Clienter interface {
|
||||
fmt.Stringer
|
||||
getClient() *Client
|
||||
handleMessage(env GameMessage) error
|
||||
finish()
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
game *Game
|
||||
conn *websocket.Conn
|
||||
outQueue chan []byte
|
||||
}
|
||||
|
||||
func newClient(g *Game, conn *websocket.Conn) Client {
|
||||
return Client{g, conn, make(chan []byte, 256)}
|
||||
}
|
||||
|
||||
func (c *Client) send(data []byte) bool {
|
||||
select {
|
||||
case c.outQueue <- data:
|
||||
return true
|
||||
default:
|
||||
close(c.outQueue)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) closeQueue() {
|
||||
close(c.outQueue)
|
||||
}
|
||||
|
||||
func handleError(c Clienter, err error) {
|
||||
log.Printf("%s error: %v", c, err)
|
||||
errmsg := makeMessage("error", fmt.Sprintf("%v", err))
|
||||
c.getClient().send(errmsg)
|
||||
}
|
||||
|
||||
func readPump(c Clienter) {
|
||||
client := c.getClient()
|
||||
|
||||
defer func() {
|
||||
c.finish()
|
||||
client.conn.Close()
|
||||
}()
|
||||
|
||||
client.conn.SetReadLimit(maxMessageSize)
|
||||
client.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
client.conn.SetPongHandler(func(string) error {
|
||||
client.conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
return nil
|
||||
})
|
||||
|
||||
for {
|
||||
_, data, err := client.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
log.Printf("error: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
var envelope GameMessage
|
||||
if err := json.Unmarshal(data, &envelope); err != nil {
|
||||
handleError(c, fmt.Errorf("Unmarshal: %w", err))
|
||||
}
|
||||
|
||||
if err := c.handleMessage(envelope); err != nil {
|
||||
handleError(c, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writePump(c Clienter) {
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
client := c.getClient()
|
||||
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
client.conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-client.outQueue:
|
||||
client.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if !ok {
|
||||
// The game closed the channel.
|
||||
client.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
|
||||
w, err := client.conn.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(message)
|
||||
if err := w.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
client.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
if err := client.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
game.go
Normal file
118
game.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
players map[uint]*Player
|
||||
hosts map[*Host]bool
|
||||
|
||||
playerToken uint64 // Verification token for players
|
||||
hostToken uint64 // Verification token for the host
|
||||
|
||||
registerPlayer chan *Player // Register requests from players.
|
||||
unregisterPlayer chan *Player // Unregister requests from players.
|
||||
registerHost chan *Host // Register requests from hosts.
|
||||
unregisterHost chan *Host // Unregister requests from hosts.
|
||||
|
||||
activePlayer *Player
|
||||
}
|
||||
|
||||
func tokenize(token uint64) string {
|
||||
return strconv.FormatUint(token, 36)
|
||||
}
|
||||
|
||||
func newGame() *Game {
|
||||
return &Game{
|
||||
players: make(map[uint]*Player),
|
||||
hosts: make(map[*Host]bool),
|
||||
|
||||
playerToken: rand.Uint64(),
|
||||
hostToken: rand.Uint64(),
|
||||
|
||||
registerPlayer: make(chan *Player),
|
||||
unregisterPlayer: make(chan *Player),
|
||||
registerHost: make(chan *Host),
|
||||
unregisterHost: make(chan *Host),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) checkPlayerToken(token string) bool {
|
||||
return token == tokenize(g.playerToken)
|
||||
}
|
||||
|
||||
func (g *Game) broadcastHosts(message []byte) {
|
||||
for client := range g.hosts {
|
||||
if !client.send(message) {
|
||||
delete(g.hosts, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) broadcastPlayers(message []byte) {
|
||||
for id := range g.players {
|
||||
g.sendTo(id, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) sendTo(playerId uint, message []byte) error {
|
||||
client, ok := g.players[playerId]
|
||||
if !ok {
|
||||
return fmt.Errorf("No Player %d", playerId)
|
||||
}
|
||||
|
||||
if !client.send(message) {
|
||||
delete(g.players, playerId)
|
||||
return fmt.Errorf("Player %d disconnected", playerId)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Game) run() {
|
||||
for {
|
||||
select {
|
||||
|
||||
case player := <-g.registerPlayer:
|
||||
g.players[player.Id] = player
|
||||
log.Printf("Player %d (%s: %s) registered", player.Id, player.Team, player.Name)
|
||||
join := makeMessage("join", player)
|
||||
g.broadcastHosts(join)
|
||||
|
||||
case player := <-g.unregisterPlayer:
|
||||
id := player.Id
|
||||
if _, ok := g.players[id]; ok {
|
||||
delete(g.players, id)
|
||||
player.closeQueue()
|
||||
}
|
||||
log.Printf("Player %d (%s: %s) left", player.Id, player.Team, player.Name)
|
||||
join := makeMessage("part", player)
|
||||
g.broadcastHosts(join)
|
||||
|
||||
case host := <-g.registerHost:
|
||||
g.hosts[host] = true
|
||||
log.Println("Host joined")
|
||||
for _, player := range g.players {
|
||||
join := makeMessage("join", player)
|
||||
g.broadcastHosts(join)
|
||||
}
|
||||
|
||||
case host := <-g.unregisterHost:
|
||||
if _, ok := g.hosts[host]; ok {
|
||||
delete(g.hosts, host)
|
||||
host.closeQueue()
|
||||
}
|
||||
log.Println("Host left")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) reveal(value bool) {
|
||||
log.Printf("Revealing %v!", value)
|
||||
reveal := makeMessage("reveal", value)
|
||||
g.broadcastHosts(reveal)
|
||||
}
|
||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module github.com/justinian/justthefrax
|
||||
|
||||
go 1.22.4
|
||||
|
||||
require github.com/gorilla/websocket v1.5.3
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
58
home.html
Normal file
58
home.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>just the frax!</title>
|
||||
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--accent: #459991;
|
||||
--accent-hover: #65e5d9;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
form div {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
<script type="module">
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const token = params.get('token');
|
||||
const tokenInput = document.getElementById('token');
|
||||
tokenInput.value = token;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>just the frax!</h1>
|
||||
<p>A game of truth and lies.</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<h3>Join a Team</h3>
|
||||
<form action="/player">
|
||||
<input type="hidden" name="token" id="token"/>
|
||||
|
||||
<div>
|
||||
<label for="name">Your Name</label>
|
||||
<input type="text" name="name"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="team">Your Team</label>
|
||||
<select name="team">
|
||||
<option>Seattle</option>
|
||||
<option>SFBA</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="go"> </label>
|
||||
<button id="go">Go!</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
63
host.go
Normal file
63
host.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Host struct {
|
||||
Client
|
||||
}
|
||||
|
||||
func (h *Host) getClient() *Client { return &h.Client }
|
||||
func (h *Host) String() string { return "Host" }
|
||||
|
||||
func (h *Host) finish() {
|
||||
h.Client.game.unregisterHost <- h
|
||||
}
|
||||
|
||||
type ActivateMessage struct {
|
||||
Player uint `json:"player"`
|
||||
Story string `json:"story"`
|
||||
}
|
||||
|
||||
func (h *Host) handleMessage(env GameMessage) error {
|
||||
game := h.Client.game
|
||||
|
||||
switch env.Type {
|
||||
case "activate":
|
||||
m, err := parseSubMessage[ActivateMessage](env.Message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
player, ok := game.players[m.Player]
|
||||
if !ok {
|
||||
return fmt.Errorf("No Player %d", m.Player)
|
||||
}
|
||||
|
||||
player.activate(m.Story)
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Unknown message %s", env.Type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func serveHostWs(game *Game, w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
host := &Host{
|
||||
Client: newClient(game, conn),
|
||||
}
|
||||
game.registerHost <- host
|
||||
|
||||
go writePump(host)
|
||||
go readPump(host)
|
||||
}
|
||||
206
host.html
Normal file
206
host.html
Normal file
@@ -0,0 +1,206 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>just the frax!</title>
|
||||
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--accent: #459991;
|
||||
--accent-hover: #65e5d9;
|
||||
}
|
||||
|
||||
.block {
|
||||
border-radius: 10px;
|
||||
border: thin gray solid;
|
||||
margin: 10px;
|
||||
margin-bottom: 5px;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.warn {
|
||||
background: orange;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
#players {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#players .block {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.team-list {
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>just the frax!</h1>
|
||||
<p>A game of truth and lies.</p>
|
||||
</header>
|
||||
|
||||
<div class="block">
|
||||
<div class="title">Status</div>
|
||||
<div id="status-message"></div>
|
||||
</div>
|
||||
|
||||
<div class="block warn" id="warning">
|
||||
<div class="title">Error</div>
|
||||
<p id="warning-message"></p>
|
||||
</div>
|
||||
|
||||
<div id="players">
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="title">Send Story</div>
|
||||
<div>
|
||||
<label for="recipient">Recipient</label>
|
||||
<input type="text" id="recipient" readonly="true" value="None"/>
|
||||
|
||||
<textarea id="story-text"></textarea>
|
||||
<button type="button" id="send-button">Send</send>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
const teams = new Map();
|
||||
let recipient = null;
|
||||
|
||||
function updateRecipient(player) {
|
||||
recipient = player;
|
||||
const textarea = document.getElementById("recipient");
|
||||
if (player)
|
||||
textarea.value = player.name;
|
||||
else
|
||||
textarea.value = "None";
|
||||
}
|
||||
|
||||
function updateTeamsView() {
|
||||
const playersDiv = document.getElementById("players");
|
||||
playersDiv.innerHTML = "";
|
||||
teams.forEach((team, name) => {
|
||||
const teamDiv = document.createElement("div");
|
||||
teamDiv.classList.add("block");
|
||||
teamDiv.innerHTML = `<div class='title'>Team ${name}</div>`;
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.classList.add("team-list");
|
||||
|
||||
team.forEach(player => {
|
||||
const item = document.createElement("button");
|
||||
item.innerHTML = player.name;
|
||||
item.addEventListener("click", () => {
|
||||
updateRecipient(player);
|
||||
});
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
teamDiv.appendChild(list);
|
||||
playersDiv.appendChild(teamDiv);
|
||||
});
|
||||
}
|
||||
|
||||
function addPlayer(player) {
|
||||
if (!teams.has(player.team)) {
|
||||
teams.set(player.team, new Map());
|
||||
}
|
||||
const team = teams.get(player.team);
|
||||
team.set(player.id, player);
|
||||
updateTeamsView();
|
||||
}
|
||||
|
||||
function removePlayer(player) {
|
||||
if (!teams.has(player.team))
|
||||
return
|
||||
|
||||
const team = teams.get(player.team);
|
||||
team.delete(player.id);
|
||||
updateTeamsView();
|
||||
}
|
||||
|
||||
function sendMessage(conn, type, message) {
|
||||
const data = JSON.stringify({type, message});
|
||||
console.log("Sending:", data);
|
||||
conn.send(data);
|
||||
}
|
||||
|
||||
function handleMessage(data, conn) {
|
||||
console.warn("Got message data:", data);
|
||||
const msg = JSON.parse(data)
|
||||
switch (msg.type) {
|
||||
case "error":
|
||||
showError(msg.message);
|
||||
break;
|
||||
|
||||
case "join":
|
||||
addPlayer(msg.message);
|
||||
break;
|
||||
|
||||
case "part":
|
||||
removePlayer(msg.message);
|
||||
break;
|
||||
|
||||
case "reveal":
|
||||
break;
|
||||
|
||||
default:
|
||||
showError(`Unknown message type: ${msg.type}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function showStatus(message) {
|
||||
const statusDiv = document.getElementById('status-message');
|
||||
statusDiv.innerHTML = message;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const warnDiv = document.getElementById('warning');
|
||||
const warnMessage = document.getElementById('warning-message');
|
||||
warnMessage.innerHTML = message;
|
||||
warnDiv.style.display = "block";
|
||||
}
|
||||
|
||||
showStatus("Connecting...");
|
||||
const conn = new WebSocket("ws://" + document.location.host + "/host/ws");
|
||||
conn.addEventListener("close", e => { showStatus("Disconnected."); });
|
||||
conn.addEventListener("error", e => { showError("Websocket error."); });
|
||||
conn.addEventListener("message", e => { handleMessage(e.data, conn); });
|
||||
conn.addEventListener("open", e => { showStatus("Connected") });
|
||||
|
||||
const sendButton = document.getElementById('send-button');
|
||||
sendButton.addEventListener("click", () => {
|
||||
const textArea = document.getElementById('story-text');
|
||||
const story = textArea.value;
|
||||
|
||||
sendMessage(conn, "activate", { player: recipient.id, story });
|
||||
|
||||
textArea.value = "";
|
||||
updateRecipient(null);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
43
main.go
Normal file
43
main.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var addr = flag.String("addr", ":8080", "http server address")
|
||||
var host = flag.String("host", "localhost", "http server canonical hostname")
|
||||
|
||||
func servePages(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/":
|
||||
http.ServeFile(w, r, "home.html")
|
||||
case "/host":
|
||||
http.ServeFile(w, r, "host.html")
|
||||
case "/player":
|
||||
http.ServeFile(w, r, "player.html")
|
||||
default:
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
game := newGame()
|
||||
|
||||
log.Printf("Player link: http://%s%s/?token=%s", *host, *addr, tokenize(game.playerToken))
|
||||
log.Printf("Host link: http://%s%s/host?token=%s", *host, *addr, tokenize(game.hostToken))
|
||||
|
||||
go game.run()
|
||||
|
||||
http.HandleFunc("/", servePages)
|
||||
http.HandleFunc("/player/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
servePlayerWs(game, w, r)
|
||||
})
|
||||
http.HandleFunc("/host/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
serveHostWs(game, w, r)
|
||||
})
|
||||
|
||||
log.Fatal(http.ListenAndServe(*addr, nil))
|
||||
}
|
||||
25
message.go
Normal file
25
message.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type GameMessage struct {
|
||||
Type string `json:"type"`
|
||||
Message json.RawMessage `json:"message"`
|
||||
}
|
||||
|
||||
func parseSubMessage[T any](data json.RawMessage) (*T, error) {
|
||||
var message T
|
||||
if err := json.Unmarshal(data, &message); err != nil {
|
||||
return nil, fmt.Errorf("Unmarshal submessage: %w", err)
|
||||
}
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
func makeMessage[T any](name string, resp T) []byte {
|
||||
payload, _ := json.Marshal(&resp)
|
||||
data, _ := json.Marshal(&GameMessage{name, json.RawMessage(payload)})
|
||||
return data
|
||||
}
|
||||
108
player.go
Normal file
108
player.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var nextPlayerId uint = 1
|
||||
|
||||
type Player struct {
|
||||
Client
|
||||
|
||||
Name string `json:"name"`
|
||||
Team string `json:"team"`
|
||||
Id uint `json:"id"`
|
||||
}
|
||||
|
||||
func (p *Player) getClient() *Client { return &p.Client }
|
||||
func (p *Player) String() string { return fmt.Sprintf("Player %d", p.Id) }
|
||||
|
||||
func (p *Player) handleMessage(env GameMessage) error {
|
||||
game := p.Client.game
|
||||
|
||||
switch env.Type {
|
||||
|
||||
case "register":
|
||||
m, err := parseSubMessage[RegisterMessage](env.Message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.handleRegisterMessage(*m)
|
||||
|
||||
case "reveal":
|
||||
value, err := parseSubMessage[bool](env.Message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if game.activePlayer != p {
|
||||
return fmt.Errorf("You are not the active player")
|
||||
}
|
||||
game.activePlayer = nil
|
||||
game.reveal(*value)
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Unknown message")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (p *Player) activate(story string) {
|
||||
game := p.Client.game
|
||||
game.activePlayer = p
|
||||
|
||||
log.Printf("Activating player %s", p.Name)
|
||||
msg := makeMessage("card", story)
|
||||
p.send(msg)
|
||||
}
|
||||
|
||||
func (p *Player) finish() {
|
||||
p.Client.game.unregisterPlayer <- p
|
||||
}
|
||||
|
||||
type RegisterMessage struct {
|
||||
Name string `json: "name"`
|
||||
Team string `json: "team"`
|
||||
Token string `json: "token"`
|
||||
}
|
||||
|
||||
func (p *Player) handleRegisterMessage(m RegisterMessage) error {
|
||||
if p.Name != "" || p.Team != "" {
|
||||
return fmt.Errorf("Already registered as %s:%s", p.Team, p.Name)
|
||||
}
|
||||
|
||||
if !p.game.checkPlayerToken(m.Token) {
|
||||
return fmt.Errorf("Incorrect token")
|
||||
}
|
||||
|
||||
p.Name = m.Name
|
||||
p.Team = m.Team
|
||||
p.game.registerPlayer <- p
|
||||
|
||||
log.Printf("Player %d is %s:%s", p.Id, p.Team, p.Name)
|
||||
|
||||
msg := makeMessage("registered", p)
|
||||
p.send(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func servePlayerWs(game *Game, w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
id := nextPlayerId
|
||||
nextPlayerId++
|
||||
player := &Player{
|
||||
Client: newClient(game, conn),
|
||||
Id: id,
|
||||
}
|
||||
|
||||
go writePump(player)
|
||||
go readPump(player)
|
||||
}
|
||||
152
player.html
Normal file
152
player.html
Normal file
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>just the frax!</title>
|
||||
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--accent: #459991;
|
||||
--accent-hover: #65e5d9;
|
||||
}
|
||||
|
||||
.block {
|
||||
border-radius: 10px;
|
||||
border: thin gray solid;
|
||||
margin: 10px;
|
||||
margin-bottom: 5px;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.warn {
|
||||
background: orange;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.block .title {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#card {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
color: white;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
#reveal-false {
|
||||
background: #99454D;
|
||||
}
|
||||
|
||||
#warning {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>just the frax!</h1>
|
||||
<p>A game of truth and lies.</p>
|
||||
</header>
|
||||
|
||||
<div class="block">
|
||||
<div class="title">Status</div>
|
||||
<div id="status-message"></div>
|
||||
</div>
|
||||
|
||||
<div class="block warn" id="warning">
|
||||
<div class="title">Error</div>
|
||||
<p id="warning-message"></p>
|
||||
</div>
|
||||
|
||||
<div class="block" id="card">
|
||||
<div class="title">Story Card</div>
|
||||
<p>Read this part aloud verbatim, then answer questions:</p>
|
||||
<blockquote id="card-content"></blockquote>
|
||||
|
||||
<p>When the host tells you to reveal, use the correct button:</p>
|
||||
<button type="button" id="reveal-true">This is THE TRUTH</button>
|
||||
<button type="button" id="reveal-false">This is A LIE</button>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
function sendMessage(conn, type, message) {
|
||||
const data = JSON.stringify({type, message});
|
||||
console.log("Sending:", data);
|
||||
conn.send(data);
|
||||
}
|
||||
|
||||
function register(conn) {
|
||||
showStatus("Registering...");
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const name = params.get('name');
|
||||
const team = params.get('team');
|
||||
const token = params.get('token');
|
||||
sendMessage(conn, "register", {name, team, token});
|
||||
}
|
||||
|
||||
function handleMessage(data, conn) {
|
||||
console.warn("Got message data:", data);
|
||||
const msg = JSON.parse(data)
|
||||
switch (msg.type) {
|
||||
case "error":
|
||||
showError(msg.message);
|
||||
break;
|
||||
|
||||
case "registered":
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const name = params.get('name');
|
||||
const team = params.get('team');
|
||||
showStatus(`Signed in as ${name} [Team: ${team}]`)
|
||||
break;
|
||||
|
||||
case "card":
|
||||
const cardDiv = document.getElementById("card");
|
||||
const cardContent = document.getElementById("card-content");
|
||||
cardContent.innerHTML = msg.message;
|
||||
cardDiv.style.display = "block";
|
||||
break;
|
||||
|
||||
default:
|
||||
showError(`Unknown message type: ${msg.type}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function showStatus(message) {
|
||||
const statusDiv = document.getElementById('status-message');
|
||||
statusDiv.innerHTML = message;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const warnDiv = document.getElementById('warning');
|
||||
const warnMessage = document.getElementById('warning-message');
|
||||
warnMessage.innerHTML = message;
|
||||
warnDiv.style.display = "block";
|
||||
}
|
||||
|
||||
showStatus("Connecting...");
|
||||
const conn = new WebSocket("ws://" + document.location.host + "/player/ws");
|
||||
conn.addEventListener("close", e => { showStatus("Disconnected."); });
|
||||
conn.addEventListener("error", e => { showError("Websocket error."); });
|
||||
conn.addEventListener("message", e => { handleMessage(e.data, conn); });
|
||||
conn.addEventListener("open", e => { register(conn); });
|
||||
|
||||
const revealTrue = document.getElementById('reveal-true');
|
||||
const revealFalse = document.getElementById('reveal-false');
|
||||
[revealTrue, revealFalse].forEach(button => {
|
||||
button.addEventListener("click", e => {
|
||||
const value = (button === revealTrue);
|
||||
const cardDiv = document.getElementById("card");
|
||||
cardDiv.style.display = "none";
|
||||
sendMessage(conn, "reveal", value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user