Compare commits
5 Commits
1e42085ce6
...
0bade0bd4f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bade0bd4f | ||
|
|
85fdfac888 | ||
|
|
d3689e36a7 | ||
|
|
91357afb1f | ||
|
|
73e79212a9 |
BIN
cmd/server/.games.go.swp
Normal file
BIN
cmd/server/.games.go.swp
Normal file
Binary file not shown.
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"io"
|
"io"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/glebarez/sqlite"
|
_ "github.com/glebarez/sqlite"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
type DB struct {
|
type DB struct {
|
||||||
scheme string
|
scheme string
|
||||||
conn string
|
conn string
|
||||||
|
rw *sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDB(ctx context.Context, scheme, conn string) (DB, error) {
|
func NewDB(ctx context.Context, scheme, conn string) (DB, error) {
|
||||||
@@ -21,6 +23,7 @@ func NewDB(ctx context.Context, scheme, conn string) (DB, error) {
|
|||||||
db := DB{
|
db := DB{
|
||||||
scheme: scheme,
|
scheme: scheme,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
|
rw: &sync.RWMutex{},
|
||||||
}
|
}
|
||||||
|
|
||||||
sql, err := db.dial(ctx)
|
sql, err := db.dial(ctx)
|
||||||
@@ -29,26 +32,58 @@ func NewDB(ctx context.Context, scheme, conn string) (DB, error) {
|
|||||||
}
|
}
|
||||||
defer sql.Close()
|
defer sql.Close()
|
||||||
|
|
||||||
if _, err := sql.ExecContext(ctx, `
|
return db, err
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
uuid TEXT,
|
|
||||||
name TEXT
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS games (
|
|
||||||
uuid TEXT
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS players (
|
|
||||||
user_uuid TEXT,
|
|
||||||
game_uuid TEXT
|
|
||||||
);
|
|
||||||
CREATE TABLE IF NOT EXISTS events (
|
|
||||||
game_uuid TEXT
|
|
||||||
);
|
|
||||||
`); err != nil {
|
|
||||||
return DB{}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return db, err
|
func (db DB) WithLock(cb func() error) error {
|
||||||
|
db.rw.Lock()
|
||||||
|
defer db.rw.Unlock()
|
||||||
|
return cb()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db DB) Exec(ctx context.Context, q string, args ...any) error {
|
||||||
|
db.rw.RLock()
|
||||||
|
defer db.rw.RUnlock()
|
||||||
|
return db.exec(ctx, q, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db DB) exec(ctx context.Context, q string, args ...any) error {
|
||||||
|
c, err := db.dial(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
_, err = c.ExecContext(ctx, q, args...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db DB) Query(ctx context.Context, cb func(*sql.Rows) error, q string, args ...any) error {
|
||||||
|
db.rw.RLock()
|
||||||
|
defer db.rw.RUnlock()
|
||||||
|
return db.query(ctx, cb, q, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db DB) query(ctx context.Context, cb func(*sql.Rows) error, q string, args ...any) error {
|
||||||
|
c, err := db.dial(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
rows, err := c.QueryContext(ctx, q, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
if err := cb(rows); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db DB) dial(ctx context.Context) (*sql.DB, error) {
|
func (db DB) dial(ctx context.Context) (*sql.DB, error) {
|
||||||
|
|||||||
175
cmd/server/games.go
Normal file
175
cmd/server/games.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Games struct {
|
||||||
|
db DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGames(ctx context.Context, db DB) (Games, error) {
|
||||||
|
err := db.Exec(ctx, `
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
uuid TEXT,
|
||||||
|
updated DATETIME,
|
||||||
|
name TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS games (
|
||||||
|
uuid TEXT,
|
||||||
|
updated DATETIME,
|
||||||
|
name TEXT,
|
||||||
|
completed DATETIME
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS players (
|
||||||
|
user_uuid TEXT,
|
||||||
|
game_uuid TEXT,
|
||||||
|
updated DATETIME
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
game_uuid TEXT,
|
||||||
|
updated DATETIME,
|
||||||
|
payload TEXT
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
return Games{db: db}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (games Games) GamesForUser(ctx context.Context, id string) ([]string, error) {
|
||||||
|
result := []string{}
|
||||||
|
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
||||||
|
var game string
|
||||||
|
err := rows.Scan(&game)
|
||||||
|
result = append(result, game)
|
||||||
|
return err
|
||||||
|
}, `
|
||||||
|
SELECT players.game_uuid
|
||||||
|
FROM players
|
||||||
|
WHERE players.user_uuid=?
|
||||||
|
`, id)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (games Games) GameByName(ctx context.Context, name string) (string, error) {
|
||||||
|
return "", io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
GameState struct {
|
||||||
|
Completed time.Time
|
||||||
|
Players map[string]PlayerState
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerState struct {
|
||||||
|
Kills []Kill
|
||||||
|
KillWords KillWords
|
||||||
|
}
|
||||||
|
|
||||||
|
Kill struct {
|
||||||
|
Timestamp time.Time
|
||||||
|
Victim string
|
||||||
|
Public bool
|
||||||
|
}
|
||||||
|
|
||||||
|
KillWords struct {
|
||||||
|
Global string
|
||||||
|
Assigned time.Time
|
||||||
|
Assignee string
|
||||||
|
Assignment Assignment
|
||||||
|
}
|
||||||
|
|
||||||
|
Assignment struct {
|
||||||
|
Public []string
|
||||||
|
Private []string
|
||||||
|
}
|
||||||
|
|
||||||
|
EventType int
|
||||||
|
|
||||||
|
EventPlayerJoin struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
EventPlayerLeave struct {
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
EventGameComplete struct{}
|
||||||
|
EventAssignmentRotation struct {
|
||||||
|
Killer string
|
||||||
|
Killed string
|
||||||
|
Assignments map[string]Assignment
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlayerJoin EventType = iota + 1
|
||||||
|
PlayerLeave
|
||||||
|
GameComplete
|
||||||
|
AssignmentRotation
|
||||||
|
)
|
||||||
|
|
||||||
|
func (games Games) GameState(ctx context.Context, id string) (GameState, error) {
|
||||||
|
result := GameState{Players: map[string]PlayerState{}}
|
||||||
|
|
||||||
|
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
||||||
|
var timestamp time.Time
|
||||||
|
var payload []byte
|
||||||
|
if err := rows.Scan(×tamp, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var peek struct {
|
||||||
|
Type EventType
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(payload, &peek); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch peek.Type {
|
||||||
|
case PlayerJoin:
|
||||||
|
var playerJoin EventPlayerJoin
|
||||||
|
if err := json.Unmarshal(payload, &playerJoin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result.Players[playerJoin.ID] = PlayerState{}
|
||||||
|
return nil
|
||||||
|
case PlayerLeave:
|
||||||
|
var playerLeave EventPlayerLeave
|
||||||
|
if err := json.Unmarshal(payload, &playerLeave); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(result.Players, playerLeave.ID)
|
||||||
|
return nil
|
||||||
|
case GameComplete:
|
||||||
|
var gameComplete EventGameComplete
|
||||||
|
if err := json.Unmarshal(payload, &gameComplete); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result.Completed = timestamp
|
||||||
|
return nil
|
||||||
|
case AssignmentRotation:
|
||||||
|
var assignmentRotation EventAssignmentRotation
|
||||||
|
if err := json.Unmarshal(payload, &assignmentRotation); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// TODO gather current assignees
|
||||||
|
// TODO get victim's target
|
||||||
|
// TODO assign victim's target to killer
|
||||||
|
// TODO randomize everyone else so not the same as before
|
||||||
|
return fmt.Errorf("not impl: assignment rotation: %+v", assignmentRotation)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown event type %d: %s", peek.Type, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, `
|
||||||
|
SELECT timestamp, payload
|
||||||
|
FROM events
|
||||||
|
WHERE game_uuid=?
|
||||||
|
`, id)
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
@@ -27,10 +27,20 @@ func run(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db, err := NewDB(ctx, config.DB.Scheme, config.DB.Conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
games, err := NewGames(ctx, db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
S := &S{
|
S := &S{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
limiter: rate.NewLimiter(10, 10),
|
limiter: rate.NewLimiter(10, 10),
|
||||||
config: config,
|
config: config,
|
||||||
|
games: games,
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type S struct {
|
|||||||
ctx context.Context
|
ctx context.Context
|
||||||
limiter *rate.Limiter
|
limiter *rate.Limiter
|
||||||
config Config
|
config Config
|
||||||
|
games Games
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *S) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *S) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
BIN
cmd/testapi/exec-testapi
Executable file
BIN
cmd/testapi/exec-testapi
Executable file
Binary file not shown.
Reference in New Issue
Block a user