src to pkg, impl lobby.Closed
This commit is contained in:
58
pkg/state/lobby/'
Normal file
58
pkg/state/lobby/'
Normal file
@@ -0,0 +1,58 @@
|
||||
package lobby
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
lobby "gitea/price-is-wrong/pkg/state/lobby/internal"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func (l *Lobby) Join(ctx context.Context, id int) error {
|
||||
return l.upsertEvent(ctx, &lobby.PlayerJoin{ID: id})
|
||||
}
|
||||
|
||||
func (l *Lobby) Leave(ctx context.Context, id int) error {
|
||||
return l.upsertEvent(ctx, &lobby.PlayerLeave{ID: id})
|
||||
}
|
||||
|
||||
func (l *Lobby) Close(ctx context.Context) error {
|
||||
return l.upsertEvent(ctx, &lobby.Close{})
|
||||
}
|
||||
|
||||
func (l *Lobby) upsertEvent(ctx context.Context, e lobby.Event) error {
|
||||
if err := upsertEvent(ctx, l.id, e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l2, err := openID(ctx, l.id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = *l2
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertEvent(ctx context.Context, lobbyID int, e lobby.Event) error {
|
||||
b, err := lobby.MarshalEvent(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.From(ctx).ExecContext(ctx, `
|
||||
INSERT INTO lobby_events (lobby_id, payload) VALUES (?, ?)
|
||||
`, lobbyID, b)
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *Lobby) apply(e lobby.Event) error {
|
||||
switch e := e.(type) {
|
||||
case *lobby.PlayerJoin:
|
||||
if !slices.Contains(l.Players, e.ID) {
|
||||
l.Players = append(l.Players, e.ID)
|
||||
}
|
||||
case *lobby.PlayerLeave:
|
||||
l.Players = slices.DeleteFunc(l.Players, func(id int) bool { return id == e.ID })
|
||||
}
|
||||
return nil
|
||||
}
|
||||
60
pkg/state/lobby/events.go
Normal file
60
pkg/state/lobby/events.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package lobby
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
lobby "gitea/price-is-wrong/pkg/state/lobby/internal"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func (l *Lobby) Join(ctx context.Context, id int) error {
|
||||
return l.upsertEvent(ctx, &lobby.PlayerJoin{ID: id})
|
||||
}
|
||||
|
||||
func (l *Lobby) Leave(ctx context.Context, id int) error {
|
||||
return l.upsertEvent(ctx, &lobby.PlayerLeave{ID: id})
|
||||
}
|
||||
|
||||
func (l *Lobby) Close(ctx context.Context) error {
|
||||
return l.upsertEvent(ctx, &lobby.Close{})
|
||||
}
|
||||
|
||||
func (l *Lobby) upsertEvent(ctx context.Context, e lobby.Event) error {
|
||||
if err := upsertEvent(ctx, l.id, e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l2, err := openID(ctx, l.id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = *l2
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertEvent(ctx context.Context, lobbyID int, e lobby.Event) error {
|
||||
b, err := lobby.MarshalEvent(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.From(ctx).ExecContext(ctx, `
|
||||
INSERT INTO lobby_events (lobby_id, payload) VALUES (?, ?)
|
||||
`, lobbyID, b)
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *Lobby) apply(e lobby.Event) error {
|
||||
switch e := e.(type) {
|
||||
case *lobby.PlayerJoin:
|
||||
if !slices.Contains(l.Players, e.ID) {
|
||||
l.Players = append(l.Players, e.ID)
|
||||
}
|
||||
case *lobby.PlayerLeave:
|
||||
l.Players = slices.DeleteFunc(l.Players, func(id int) bool { return id == e.ID })
|
||||
case *lobby.Close:
|
||||
l.Closed = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
33
pkg/state/lobby/internal/event.go
Normal file
33
pkg/state/lobby/internal/event.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package lobby
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea/price-is-wrong/pkg/lib/event"
|
||||
)
|
||||
|
||||
type Event interface{}
|
||||
|
||||
type PlayerJoin struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
type PlayerLeave struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
type Close struct {
|
||||
}
|
||||
|
||||
func ParseEvent(b []byte) (Event, error) {
|
||||
typesToPointers := map[string]any{
|
||||
"*lobby.PlayerJoin": &PlayerJoin{},
|
||||
"*lobby.PlayerLeave": &PlayerLeave{},
|
||||
"*lobby.Close": &Close{},
|
||||
}
|
||||
t, err := event.Parse(b, typesToPointers)
|
||||
return typesToPointers[t], err
|
||||
}
|
||||
|
||||
func MarshalEvent(e Event) ([]byte, error) {
|
||||
return event.Serialize(fmt.Sprintf("%T", e), e)
|
||||
}
|
||||
37
pkg/state/lobby/internal/event_test.go
Normal file
37
pkg/state/lobby/internal/event_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package lobby_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
lobby "gitea/price-is-wrong/pkg/state/lobby/internal"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMarshalUnmarshal(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
e lobby.Event
|
||||
}{
|
||||
"PlayerJoin": {e: &lobby.PlayerJoin{ID: 1}},
|
||||
"PlayerLeave": {e: &lobby.PlayerLeave{ID: 1}},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
b, err := lobby.MarshalEvent(c.e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("marshalled: %s", b)
|
||||
|
||||
e, err := lobby.ParseEvent(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("unmarshalled: %+v", e)
|
||||
|
||||
if fmt.Sprintf("%+v", c.e) != fmt.Sprintf("%+v", e) {
|
||||
t.Errorf("expected %+v but got %+v", c.e, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
53
pkg/state/lobby/lobby_test.go
Normal file
53
pkg/state/lobby/lobby_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package lobby_test
|
||||
|
||||
import (
|
||||
"gitea/price-is-wrong/pkg/lib"
|
||||
"gitea/price-is-wrong/pkg/state/lobby"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
ctx := lib.NewTestCtx(t)
|
||||
|
||||
l, err := lobby.Open(ctx, "id")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%+v", l)
|
||||
|
||||
if len(l.Players) != 0 {
|
||||
t.Errorf("new lobby has players: %+v", l.Players)
|
||||
}
|
||||
|
||||
if err := l.Join(ctx, 1); err != nil {
|
||||
t.Fatal("failed to join:", err)
|
||||
} else if len(l.Players) != 1 {
|
||||
t.Errorf("wrong number of players after first join: %+v", l.Players)
|
||||
}
|
||||
|
||||
if err := l.Leave(ctx, 1); err != nil {
|
||||
t.Fatal("failed to join:", err)
|
||||
} else if len(l.Players) != 0 {
|
||||
t.Errorf("wrong number of players after only leaves: %+v", l.Players)
|
||||
}
|
||||
|
||||
if err := l.Join(ctx, 1); err != nil {
|
||||
t.Fatal("failed to join:", err)
|
||||
} else if err := l.Join(ctx, 1); err != nil {
|
||||
t.Fatal("failed to join redundant:", err)
|
||||
} else if len(l.Players) != 1 {
|
||||
t.Errorf("redundant join yielded wrong players: %+v", l.Players)
|
||||
}
|
||||
|
||||
if err := l.Join(ctx, 2); err != nil {
|
||||
t.Fatal("failed to second join:", err)
|
||||
} else if len(l.Players) != 2 {
|
||||
t.Errorf("second join yielded wrong players: %+v", l.Players)
|
||||
}
|
||||
|
||||
if err := l.Close(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !l.Closed {
|
||||
t.Error(l.Closed)
|
||||
}
|
||||
}
|
||||
111
pkg/state/lobby/open.go
Normal file
111
pkg/state/lobby/open.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package lobby
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"gitea/price-is-wrong/pkg/lib/db"
|
||||
lobby "gitea/price-is-wrong/pkg/state/lobby/internal"
|
||||
)
|
||||
|
||||
type Lobby struct {
|
||||
id int
|
||||
Players []int
|
||||
Closed bool
|
||||
}
|
||||
|
||||
func Open(ctx context.Context, name string) (*Lobby, error) {
|
||||
if err := initialize(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize lobbies: %w", err)
|
||||
}
|
||||
|
||||
if result, err := open(ctx, name); err != nil {
|
||||
return nil, err
|
||||
} else if result == nil {
|
||||
if err := create(ctx, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return mustOpen(ctx, name)
|
||||
}
|
||||
|
||||
func mustOpen(ctx context.Context, name string) (*Lobby, error) {
|
||||
result, err := open(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result == nil {
|
||||
return nil, fmt.Errorf("no lobby found with name %s", name)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func open(ctx context.Context, name string) (*Lobby, error) {
|
||||
row := db.From(ctx).QueryRow(`SELECT id FROM lobbies WHERE name=?`, name)
|
||||
if err := row.Err(); err != nil {
|
||||
return nil, fmt.Errorf("no lobby found with name %s", name)
|
||||
}
|
||||
|
||||
var id sql.NullInt32
|
||||
if err := row.Scan(&id); err == sql.ErrNoRows || !id.Valid {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan id from lobby: %w", err)
|
||||
}
|
||||
|
||||
return openID(ctx, int(id.Int32))
|
||||
}
|
||||
|
||||
func openID(ctx context.Context, id int) (*Lobby, error) {
|
||||
rows, err := db.From(ctx).QueryContext(ctx, `
|
||||
SELECT payload
|
||||
FROM lobby_events
|
||||
WHERE lobby_events.lobby_id=?
|
||||
ORDER BY id
|
||||
`, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query event payloads for id %d: %w", id, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := Lobby{id: id}
|
||||
for rows.Next() {
|
||||
var b []byte
|
||||
if err := rows.Scan(&b); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan event: %w", err)
|
||||
}
|
||||
event, err := lobby.ParseEvent(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse event: %w", err)
|
||||
}
|
||||
if err := result.apply(event); err != nil {
|
||||
return nil, fmt.Errorf("failed to apply event %s: %w", b, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &result, rows.Err()
|
||||
}
|
||||
|
||||
func create(ctx context.Context, name string) error {
|
||||
_, err := db.From(ctx).ExecContext(ctx, `
|
||||
INSERT INTO lobbies (name) VALUES (?)
|
||||
`, name)
|
||||
return err
|
||||
}
|
||||
|
||||
func initialize(ctx context.Context) error {
|
||||
_, err := db.From(ctx).ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS lobbies (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS lobby_events (
|
||||
id INTEGER PRIMARY KEY,
|
||||
lobby_id NUMBER,
|
||||
payload TEXT,
|
||||
FOREIGN KEY (lobby_id) REFERENCES lobbies (id)
|
||||
);
|
||||
`)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user