Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ jobs:
LEXICON_PATH: /opt/lexica/
LETTER_DISTRIBUTION_PATH: /opt/liwords/data/letterdistributions/
TEST_DB_HOST: localhost
NATS_URL: nats://localhost:4222
REDIS_URL: redis://localhost:6379
docker:
- image: golang:alpine
- image: nats
- image: circleci/postgres:12.3
environment:
POSTGRES_USER: postgres
Expand Down
1 change: 1 addition & 0 deletions api/proto/ipc/ipc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ enum MessageType {
GAME_META_EVENT = 42;
PROFILE_UPDATE_EVENT = 43;

GAME_INSTANTIATION = 44;
// Add more events here. The total number of events should fit in a byte.
// We should definitely not be using anywhere close to 255 events, and
// in order to enforce that I'll be forcing the event type header to just be
Expand Down
77 changes: 69 additions & 8 deletions api/proto/ipc/omgwords.proto
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,13 @@ message GameInfoResponse {
string tournament_id = 6;
// done - is game done?
// bool done = 9;
ipc.GameEndReason game_end_reason = 11;
GameEndReason game_end_reason = 11;
repeated int32 scores = 13;
int32 winner = 14;
google.protobuf.Timestamp created_at = 15;
string game_id = 16;
google.protobuf.Timestamp last_update = 18;

ipc.GameRequest game_request = 19;
GameRequest game_request = 19;
string tournament_division = 20;
int32 tournament_round = 21;
// a game index within a round.
Expand All @@ -196,15 +195,65 @@ message GameInfoResponse {

message GameInfoResponses { repeated GameInfoResponse game_info = 1; }


// InstantiateGame is an internal message passed to gamesvc in order to
// instantiate a game.
message InstantiateGame {
repeated string user_ids = 1;
GameRequest game_request = 2;
// conn_ids are the connection IDs of the users, in the same order as the user
// ids.
repeated string conn_ids = 2;
GameRequest game_request = 3;
// assigned_first is -1 if random, or the player index in user_ids otherwise
int32 assigned_first = 3;
TournamentDataForGame tournament_data = 4;
int32 assigned_first = 4;
TournamentDataForGame tournament_data = 5;
}

message InstantiateGameResponse {
string id = 1;
GameInfoResponse game_info = 2;
}

message Timers {
// time_of_last_update is the timestamp of the last update, in milliseconds
// if no update has been made, this defaults to time_started
int64 time_of_last_update = 1 [ json_name = "lu" ];
// time_started is a unix timestamp, in milliseconds
int64 time_started = 2 [ json_name = "ts" ];
// time_remaining is an array of remaining time per player, in milliseconds
repeated int32 time_remaining = 3 [ json_name = "tr" ];
// max_overtime is the max overtime, in _minutes_
int32 max_overtime = 4 [ json_name = "mo" ];
}

message GetGame { string id = 1; }

// GetGameResponse should be sent by the store svc in response to a GetGame
// request. It needs to have all the information necessary to reconstruct
// a game. It should be a subset of the database representation of a game.
message GetGameResponse {
GameHistoryRefresher game_history_refresher = 1;
bool started = 2;
Timers timers = 3;
string lexicon = 4;
string board_layout_name = 5;
string letter_distribution_name = 6;
string variant_name = 7;
// meta events?
// last_game_update is the timestamp at which this game was last updated.
// We want to pass it back to the store when making any updates, so that
// we can use the `update` postgres function atomically.
google.protobuf.Timestamp last_game_update = 8;
}

// SetGame is a message sent to the storesvc with game updates.
message SetGame {
GameHistoryRefresher game_history_refresher = 1;
Timers timers = 2;
GameEndReason game_end_reason = 3;
int32 winner_idx = 4;
int32 loser_idx = 5;
// xxx: metaevents probably
google.protobuf.Timestamp last_known_game_update = 6;
}

message GameDeletion { string id = 1; }
Expand All @@ -220,7 +269,19 @@ message ActiveGameEntry {
int64 ttl = 3; // time to live, in seconds
}

message ReadyForGame { string game_id = 1; }
message ReadyForGame {
string game_id = 1;
string user_id = 2;
string conn_id = 3;
}

message ReadyForGameResponse { bool ready_to_start = 1; }

message ResetTimersAndStart { string game_id = 1; }

message ResetTimersAndStartResponse {
GameHistoryRefresher game_history_refresher = 1;
}

// The server will send back a ServerGameplayEvent to a ClientGameplayEvent.
// The server will also send these asynchronously for opponent gameplay
Expand Down
25 changes: 25 additions & 0 deletions docs/refactor/arch.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
digraph {

rankdir = TB;

twirp -> nats;
socket -> nats;
other -> nats;
tourneysvc -> nats;
store -> nats;
bots -> nats;
gamesvc -> nats;

nats -> {twirp socket other tourneysvc store bots gamesvc};

nats [shape=box label="NATS.io"];
twirp [label="Twirp\nAPI"];
socket [label="Socket\ncomms"];
other [label="Other\n(users, mod, etc)"];
tourneysvc [label="Tournament\nService"];
gamesvc [label="Game\nService"];
bots [label="Bots"];
store [label="Store\n(pgsql + redis cache)"];

{rank = same; twirp; socket;}
}
49 changes: 49 additions & 0 deletions docs/refactor/notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
See arch.dot

Since this is a real-time game, we don't need sub millisecond times.

A circle with NATS in the middle.

Each "service"/module communicates through NATS. Each module is easily testable, deployable, and independent. Each only has inputs/outputs

Communication between services and NATS should be protobuf. Communication to user doesn't need to be protobuf. Socket comms can be plain text with a translation later. Twirp API can maybe be replaced with something more RESTful if desired. See OpenAPI.

### Sample requests

#### Make a move in a game
- Player makes a move in a game; gets sent via socket
- Write GAME_EVT to NATS conn with game ID and information about the game. (Turn/evt number should be included too, as a good way to ensure the sequence is correct).
- NATS publishes to listening game service(s)
- We can use a "Queue group" so that only one game service takes the request
- Game service sends a request (via NATS) for the game information (use request/response with a queue group)
- Store service picks up the request, sends game information back as protobuf
- Game service deserializes it and plays the game to the current turn, and then plays the new turn.
- Deserialization should be optimized somewhat. Even right now it is very fast, HastyBot does this every turn.
- Game service publishes state back to NATS
- e.g. GAME_STATE_MSG, GAME_ID, full pb, last turn pb
- Any listeners/subscribers can do the needful:
- The db can save the state back, and if the game is over, it can save it to S3
- Save game + new user ratings in a transaction
- The socket server can publish just the last move (or maybe the full pb might simplify the front end)
- Anti-cheating module can be listening for finished games and do whatever analysis on them
- Tourney module can be listening for an ended game and update it
- Automod/other module can be listening and apply penalties to users who do bad things
- etc


#### Request last games in a profile
- Twirp API module picks up request, sends a message via NATS using req-response
- Store module picks up the request, makes the necessary DB requests and sends it back through protobuf
- Twirp API module receives answer, does any necessary conversions to send back data to user.


### Start a game
- A seek/match is answered, or a tournament game is ready
- Main API module (or whoever gets the signal? tournament module?) sends a message to gamesvc via PB
- user IDs/usernames
- req (GameRequest)
- assignedFirst
- tournament data, if any
- gamesvc tells store (req/resp) to save a new game with these parameters, and gets ID back
- use retry module with a short ID
- gamesvc publishes back details of new game (ID) to both participants
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ module github.com/domino14/liwords

go 1.17

// XXX: get rid of github.com/golang/protobuf module, it's obsolete
require (
github.com/avast/retry-go v3.0.0+incompatible
github.com/aws/aws-sdk-go-v2 v1.2.0
github.com/aws/aws-sdk-go-v2/config v1.1.1
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.0.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.2.0
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/domino14/macondo v0.4.5-0.20220119032411-9abf690996ed
github.com/domino14/macondo v0.4.5-0.20220125183153-f32108b23887
github.com/golang/protobuf v1.4.2
github.com/gomodule/redigo v1.8.2
github.com/hashicorp/golang-lru v0.5.4
Expand Down Expand Up @@ -40,7 +42,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sts v1.1.1 // indirect
github.com/aws/smithy-go v1.1.0 // indirect
github.com/go-chi/chi v4.0.0+incompatible // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.7.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
Expand Down
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSi
github.com/alecthomas/mph v0.0.0-20190930022807-712982e3d8a2/go.mod h1:HX5roj0AK3pjtDnP4HIV4zY4yW56odJ0LEwgoHL8NLI=
github.com/alecthomas/unsafeslice v0.1.0/go.mod h1:H7s9N0gAbfiwu02rQEexZbN/YMxm+2l3rVRa/zE2DM8=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go-v2 v1.2.0 h1:BS+UYpbsElC82gB+2E2jiCBg36i8HlubTB/dO/moQ9c=
github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo=
github.com/aws/aws-sdk-go-v2/config v1.1.1 h1:ZAoq32boMzcaTW9bcUacBswAmHTbvlvDJICgHFZuECo=
Expand Down Expand Up @@ -48,8 +50,8 @@ github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc h1:VRRKCwnzq
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/domino14/macondo v0.4.5-0.20220119032411-9abf690996ed h1:qnAIho8ixOFOuR8uFoTHgdp1u7cb4poZL0bC/7WQBJ0=
github.com/domino14/macondo v0.4.5-0.20220119032411-9abf690996ed/go.mod h1:PrS4/yiDGFM65QpBVqa6tJm5UJ7m5QU8BeGPqeRsv2Q=
github.com/domino14/macondo v0.4.5-0.20220125183153-f32108b23887 h1:Otew1Tgr3ESqqbIpk+bYOCgha7nPJMF1ZX8qt0QoTMo=
github.com/domino14/macondo v0.4.5-0.20220125183153-f32108b23887/go.mod h1:TzTxyBy6udsoIEuc0vEJmEsrzRxm1A8KVXS+M2JTfO0=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
Expand Down Expand Up @@ -93,8 +95,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
Expand Down
Binary file added liwords-ui/public/wasm/macondo.wasm
Binary file not shown.
1 change: 1 addition & 0 deletions liwords-ui/src/gen/api/proto/ipc/ipc_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export interface MessageTypeMap {
ACTIVE_GAME_ENTRY: 41;
GAME_META_EVENT: 42;
PROFILE_UPDATE_EVENT: 43;
GAME_INSTANTIATION: 44;
}

export const MessageType: MessageTypeMap;
Expand Down
3 changes: 2 additions & 1 deletion liwords-ui/src/gen/api/proto/ipc/ipc_pb.js
Original file line number Diff line number Diff line change
Expand Up @@ -1360,7 +1360,8 @@ proto.ipc.MessageType = {
PRESENCE_ENTRY: 40,
ACTIVE_GAME_ENTRY: 41,
GAME_META_EVENT: 42,
PROFILE_UPDATE_EVENT: 43
PROFILE_UPDATE_EVENT: 43,
GAME_INSTANTIATION: 44
};

goog.object.extend(exports, proto.ipc);
Loading