mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-12 22:14:45 +00:00
feat(online)!: seat two to four players in a room
A room holds up to four people and needs two to start. Both numbers are server constants sent to the client in RoomState, so the lobby draws whatever the server allows and widening a room is a server change alone. Failing a turn eliminates that player rather than ending the game. The syllable and the used words survive them, the turn passes to whoever is next, and the last player standing wins. Two seats is that same rule seen from close up, which is why there is one implementation of it and not two. A dead end still costs the first player to face it their own clock, as before: they get their turn, and lose it. Everyone behind them has already seen that board, so they go out together rather than each sitting out a turn limit they cannot use — which leaves the player who closed the position standing, the same outcome two players get. A player who is knocked out keeps their seat. They watch the rest of the game, chat included, with only the word input gone, and everybody lands back in the same lobby when it ends. The result screen is the whole table, ranked by who outlasted whom, with each score reported beside the place rather than deciding it. The turn clock is deliberately not paused for a seat that has dropped, so a player who loses their connection on their own turn loses it the way anybody else would. Their reconnect window decides only whether they are still in the game afterwards. Any number of windows can be open at once, settled by one timer armed for the nearest of them. Starting waits for every guest, not merely the first: a room of four that began on one yes would have dealt three people a turn they never agreed to. Kicking names a seat and is still refused on a player who is ready, and on the owner's own — leaving is what an owner who wants out does, and it hands the room on. Joining stays a lobby thing: a room with a game running turns a latecomer away even with seats going spare, because there is no way to hand somebody a game already in progress. BREAKING CHANGE: RoomState, TurnUpdate and GameOver lose the fields that could only ever describe a second player, OpponentLeft is retired in favour of presence on RoomState, and suggestions move to the new PlayerEliminated — they describe the position that beat a player, which by the end of a longer game is nobody else's position. ProtocolVersion goes to 2, so a client built against 1 is refused with a readable error rather than decoding a frame that now means something else.
This commit is contained in:
1 parent
0b507c3537
commit
4e2e9e1433
54 files changed
+3692
-1129
No files matched your search
@@ -65,12 +65,6 @@ func moveRejectedMsg(reason noituv1.RejectReason, word string, turnSeq uint32) *
|
||||
}}
|
||||
}
|
||||
|
||||
func opponentLeftMsg(canReconnect bool, graceMs uint32) *noituv1.ServerMessage {
|
||||
return &noituv1.ServerMessage{Payload: &noituv1.ServerMessage_OpponentLeft{
|
||||
OpponentLeft: &noituv1.OpponentLeft{CanReconnect: canReconnect, GraceMs: graceMs},
|
||||
}}
|
||||
}
|
||||
|
||||
// errorMsg carries a UI key, never prose and never an internal error string.
|
||||
// The Vietnamese copy lives in the frontend so all wording stays in one place,
|
||||
// and a raw error would leak server internals to anyone with a socket.
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
// agree on everything they both know how to decode. Bump it when a change
|
||||
// would make an older client misread a frame rather than merely ignore an
|
||||
// unknown field.
|
||||
const ProtocolVersion uint32 = 1
|
||||
const ProtocolVersion uint32 = 2
|
||||
|
||||
// RejectReason maps an engine rejection onto the wire enum.
|
||||
//
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
package wsapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
noituv1 "github.com/tiennm99dev/noitu/server/gen/noitu/v1"
|
||||
)
|
||||
|
||||
// What a room of more than two people does that a pair does not: seat up to
|
||||
// four, refuse the fifth, wait for every guest before it starts, kick by name,
|
||||
// and outlive the first player it knocks out.
|
||||
|
||||
// roomOf seats n players and leaves them in the lobby. The first is the owner.
|
||||
//
|
||||
// It returns the owner's last view of the room as well as the clients: every
|
||||
// room_state the seating produced has been read by then, so a caller that
|
||||
// awaited another would wait for one nothing is going to send.
|
||||
func roomOf(t *testing.T, url string, n int) (clients []*testClient, state *noituv1.RoomState) {
|
||||
t.Helper()
|
||||
|
||||
names := []string{"Chủ phòng", "Khách", "Người thứ ba", "Người thứ tư", "Người thứ năm"}
|
||||
host := dial(t, url)
|
||||
host.hello(names[0])
|
||||
host.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_CreateRoom{CreateRoom: &noituv1.CreateRoom{}}})
|
||||
state = host.await("room_state").GetRoomState()
|
||||
clients = append(clients, host)
|
||||
|
||||
for i := 1; i < n; i++ {
|
||||
c := dial(t, url)
|
||||
c.hello(names[i])
|
||||
c.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_JoinRoom{
|
||||
JoinRoom: &noituv1.JoinRoom{RoomCode: state.GetRoomCode()},
|
||||
}})
|
||||
clients = append(clients, c)
|
||||
// Everybody seated sees the arrival, which is also what keeps each
|
||||
// client's inbox drained before the next assertion reads from it.
|
||||
for _, seated := range clients {
|
||||
got := seated.await("room_state").GetRoomState()
|
||||
if seated == host {
|
||||
state = got
|
||||
}
|
||||
}
|
||||
}
|
||||
return clients, state
|
||||
}
|
||||
|
||||
func TestARoomSeatsFourAndRefusesTheFifth(t *testing.T) {
|
||||
_, url := newTestServer(t, chainDict(), Config{})
|
||||
_, state := roomOf(t, url, maxPlayers)
|
||||
|
||||
if got := len(state.GetPlayers()); got != maxPlayers {
|
||||
t.Fatalf("the room holds %d players, want %d", got, maxPlayers)
|
||||
}
|
||||
if state.GetMaxPlayers() != maxPlayers || state.GetMinPlayers() != minPlayers {
|
||||
t.Errorf("limits = %d/%d, want %d/%d",
|
||||
state.GetMinPlayers(), state.GetMaxPlayers(), minPlayers, maxPlayers)
|
||||
}
|
||||
// Exactly one row is the recipient's, and exactly one is the owner's.
|
||||
me, owners := 0, 0
|
||||
for _, p := range state.GetPlayers() {
|
||||
if p.GetIsMe() {
|
||||
me++
|
||||
}
|
||||
if p.GetIsOwner() {
|
||||
owners++
|
||||
}
|
||||
}
|
||||
if me != 1 || owners != 1 {
|
||||
t.Errorf("the room has %d rows marked mine and %d marked owner, want 1 of each", me, owners)
|
||||
}
|
||||
|
||||
fifth := dial(t, url)
|
||||
fifth.hello("Người thứ năm")
|
||||
fifth.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_JoinRoom{
|
||||
JoinRoom: &noituv1.JoinRoom{RoomCode: state.GetRoomCode()},
|
||||
}})
|
||||
if got := fifth.await("error").GetError().GetCode(); got != "room_full" {
|
||||
t.Errorf("the fifth joiner got %q, want room_full", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Every guest, not merely the first: a room of four that starts on one yes has
|
||||
// dealt three people a turn they never agreed to.
|
||||
func TestStartWaitsForEveryGuest(t *testing.T) {
|
||||
_, url := newTestServer(t, chainDict(), Config{})
|
||||
clients, _ := roomOf(t, url, 3)
|
||||
host, second, third := clients[0], clients[1], clients[2]
|
||||
|
||||
second.setReady(true)
|
||||
for _, c := range clients {
|
||||
c.await("room_state")
|
||||
}
|
||||
|
||||
host.startGame()
|
||||
if got := host.await("error").GetError().GetCode(); got != "not_everyone_ready" {
|
||||
t.Fatalf("starting with one guest ready returned %q, want not_everyone_ready", got)
|
||||
}
|
||||
|
||||
third.setReady(true)
|
||||
for _, c := range clients {
|
||||
state := c.await("room_state").GetRoomState()
|
||||
if !state.GetCanStart() {
|
||||
t.Errorf("can_start is false with everybody ready: %+v", state)
|
||||
}
|
||||
}
|
||||
|
||||
host.startGame()
|
||||
for _, c := range clients {
|
||||
start := c.await("game_started").GetGameStarted()
|
||||
if got := len(start.GetPlayers()); got != 3 {
|
||||
t.Errorf("the game was dealt to %d players, want 3", got)
|
||||
}
|
||||
if start.GetTurnPlayerId() != "p1" {
|
||||
t.Errorf("turn_player_id = %q, want p1 — seat order is turn order", start.GetTurnPlayerId())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKickNamesASeat(t *testing.T) {
|
||||
_, url := newTestServer(t, chainDict(), Config{})
|
||||
clients, _ := roomOf(t, url, 3)
|
||||
host, second, third := clients[0], clients[1], clients[2]
|
||||
|
||||
// The owner cannot free their own seat: leaving is what an owner who wants
|
||||
// out does, and it hands the room on rather than dropping it.
|
||||
host.kickPlayer("p1")
|
||||
if got := host.await("error").GetError().GetCode(); got != "cannot_kick_self" {
|
||||
t.Errorf("kicking themselves returned %q, want cannot_kick_self", got)
|
||||
}
|
||||
|
||||
// Readiness is a commitment, per target rather than per room.
|
||||
second.setReady(true)
|
||||
for _, c := range clients {
|
||||
c.await("room_state")
|
||||
}
|
||||
host.kickPlayer("p2")
|
||||
if got := host.await("error").GetError().GetCode(); got != "player_is_ready" {
|
||||
t.Errorf("kicking a ready player returned %q, want player_is_ready", got)
|
||||
}
|
||||
|
||||
// The unready one goes, and the ready one is untouched.
|
||||
host.kickPlayer("p3")
|
||||
if got := third.await("error").GetError().GetCode(); got != "kicked" {
|
||||
t.Fatalf("the kicked player was told %q", got)
|
||||
}
|
||||
|
||||
state := host.await("room_state").GetRoomState()
|
||||
if got := len(state.GetPlayers()); got != 2 {
|
||||
t.Fatalf("the room holds %d players after one kick, want 2", got)
|
||||
}
|
||||
if slotFor(state, "p3") != nil {
|
||||
t.Error("the kicked seat is still occupied")
|
||||
}
|
||||
if seat := slotFor(state, "p2"); seat == nil || !seat.GetReady() {
|
||||
t.Error("kicking one player disturbed another's readiness")
|
||||
}
|
||||
}
|
||||
|
||||
// The room outlives the first player it knocks out: the rest carry on from the
|
||||
// same syllable, and the player who went out stays to watch.
|
||||
func TestAGameOutlivesItsFirstElimination(t *testing.T) {
|
||||
_, url := newTestServer(t, chainDict(), Config{TurnLimit: 10 * time.Second})
|
||||
clients, _ := roomOf(t, url, 3)
|
||||
host, second, third := clients[0], clients[1], clients[2]
|
||||
|
||||
second.setReady(true)
|
||||
third.setReady(true)
|
||||
for _, c := range clients {
|
||||
c.await("room_state")
|
||||
}
|
||||
|
||||
host.startGame()
|
||||
starts := map[*testClient]*noituv1.GameStarted{}
|
||||
for _, c := range clients {
|
||||
starts[c] = c.await("game_started").GetGameStarted()
|
||||
}
|
||||
|
||||
// The player on turn gives up. Two are left, so the game does not end.
|
||||
host.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_Resign{Resign: &noituv1.Resign{}}})
|
||||
|
||||
for _, c := range clients {
|
||||
out := c.await("player_eliminated").GetPlayerEliminated()
|
||||
if out.GetPlayerId() != "p1" {
|
||||
t.Errorf("%q went out, want p1", out.GetPlayerId())
|
||||
}
|
||||
if got := out.GetIsMe(); got != (c == host) {
|
||||
t.Errorf("is_me = %v for the wrong recipient", got)
|
||||
}
|
||||
if c != host && len(out.GetSuggestions()) != 0 {
|
||||
t.Errorf("a player who is still in was sent suggestions %v", out.GetSuggestions())
|
||||
}
|
||||
}
|
||||
|
||||
// A turn update with no word: the position survived the player who left it.
|
||||
update := second.await("turn_update").GetTurnUpdate()
|
||||
if update.GetPlayed() != nil {
|
||||
t.Error("an elimination reported a word as played")
|
||||
}
|
||||
if update.GetCurrentSyllable() != starts[second].GetCurrentSyllable() {
|
||||
t.Errorf("the syllable moved on an elimination: %q -> %q",
|
||||
starts[second].GetCurrentSyllable(), update.GetCurrentSyllable())
|
||||
}
|
||||
if update.GetTurnPlayerId() != "p2" || !update.GetMyTurn() {
|
||||
t.Errorf("turn went to %q, want p2", update.GetTurnPlayerId())
|
||||
}
|
||||
if update.GetTurnSeq() == starts[second].GetTurnSeq() {
|
||||
t.Error("the turn sequence did not move, so a stale submission could still land")
|
||||
}
|
||||
|
||||
// The player who went out is still in the room and still being told what
|
||||
// is happening in it.
|
||||
watching := host.await("turn_update").GetTurnUpdate()
|
||||
if watching.GetMyTurn() {
|
||||
t.Error("an eliminated player was dealt a turn")
|
||||
}
|
||||
var eliminated bool
|
||||
for _, p := range watching.GetPlayers() {
|
||||
if p.GetPlayerId() == "p1" {
|
||||
eliminated = p.GetEliminated()
|
||||
}
|
||||
}
|
||||
if !eliminated {
|
||||
t.Error("the table does not show the eliminated player as out")
|
||||
}
|
||||
|
||||
// And they can still talk, which is the other half of staying in the room.
|
||||
host.say("chúc may mắn")
|
||||
if got := second.await("chat_message").GetChatMessage().GetText(); got != "chúc may mắn" {
|
||||
t.Errorf("an eliminated player's message arrived as %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The last elimination ends it, and everybody is shown the same table from
|
||||
// their own side.
|
||||
func TestStandingsReachEverySeat(t *testing.T) {
|
||||
_, url := newTestServer(t, chainDict(), Config{TurnLimit: 10 * time.Second})
|
||||
clients, _ := roomOf(t, url, 3)
|
||||
host, second, third := clients[0], clients[1], clients[2]
|
||||
|
||||
second.setReady(true)
|
||||
third.setReady(true)
|
||||
for _, c := range clients {
|
||||
c.await("room_state")
|
||||
}
|
||||
host.startGame()
|
||||
for _, c := range clients {
|
||||
c.await("game_started")
|
||||
}
|
||||
|
||||
host.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_Resign{Resign: &noituv1.Resign{}}})
|
||||
for _, c := range clients {
|
||||
c.await("player_eliminated")
|
||||
}
|
||||
second.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_Resign{Resign: &noituv1.Resign{}}})
|
||||
|
||||
winners := 0
|
||||
for _, c := range clients {
|
||||
over := c.await("game_over").GetGameOver()
|
||||
if over.GetIWon() {
|
||||
winners++
|
||||
}
|
||||
|
||||
standings := over.GetStandings()
|
||||
if len(standings) != 3 {
|
||||
t.Fatalf("standings has %d rows, want 3", len(standings))
|
||||
}
|
||||
// Finishing order: the survivor, then the players who went out, latest
|
||||
// first. Rank matches position, so the two cannot drift apart.
|
||||
want := []string{"p3", "p2", "p1"}
|
||||
mine := 0
|
||||
for i, row := range standings {
|
||||
if row.GetPlayerId() != want[i] {
|
||||
t.Errorf("rank %d is %q, want %q", i+1, row.GetPlayerId(), want[i])
|
||||
}
|
||||
if int(row.GetRank()) != i+1 {
|
||||
t.Errorf("%q has rank %d at position %d", row.GetPlayerId(), row.GetRank(), i+1)
|
||||
}
|
||||
if row.GetIsMe() {
|
||||
mine++
|
||||
}
|
||||
}
|
||||
if mine != 1 {
|
||||
t.Errorf("%d standings rows are marked mine, want 1", mine)
|
||||
}
|
||||
}
|
||||
if winners != 1 {
|
||||
t.Errorf("%d players were told they won, want 1", winners)
|
||||
}
|
||||
}
|
||||
|
||||
// Two people dropping at once is two windows, not one. The room has to hold
|
||||
// both seats and settle each on its own deadline.
|
||||
func TestSeveralReconnectWindowsRunAtOnce(t *testing.T) {
|
||||
_, url := newTestServer(t, chainDict(), Config{GraceFor: 300 * time.Millisecond})
|
||||
clients, _ := roomOf(t, url, 3)
|
||||
host, second, third := clients[0], clients[1], clients[2]
|
||||
|
||||
second.conn.CloseNow()
|
||||
third.conn.CloseNow()
|
||||
|
||||
// Both seats are held first, then both are freed. The owner is the one
|
||||
// still here to watch it happen.
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
state := host.await("room_state").GetRoomState()
|
||||
if len(state.GetPlayers()) == 1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Error("the room never freed the seats whose windows had run out")
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package wsapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
@@ -110,16 +112,25 @@ func capMarks(s string, maxMarks int) string {
|
||||
// defaultNickname is what an unusable name falls back to.
|
||||
const defaultNickname = "Người chơi"
|
||||
|
||||
// distinguish returns a name for the joining player that their opponent cannot
|
||||
// be confused with. Nicknames are the only way to tell two strangers apart, so
|
||||
// letting both sides render the same string defeats the point of having them.
|
||||
func distinguish(name, taken string) string {
|
||||
if name != taken {
|
||||
// distinguish returns a name the joining player cannot be confused with any of
|
||||
// the ones already in the room. Nicknames are the only way to tell strangers
|
||||
// apart, so letting two of them render the same string defeats the point of
|
||||
// having names at all.
|
||||
//
|
||||
// The counter is bounded by the room: taken holds at most one name per seat, so
|
||||
// a free suffix is always found within that many tries.
|
||||
func distinguish(name string, taken []string) string {
|
||||
if !slices.Contains(taken, name) {
|
||||
return name
|
||||
}
|
||||
suffix := " 2"
|
||||
if runes := []rune(name); len(runes)+len(suffix) > maxNicknameRunes {
|
||||
name = strings.TrimSpace(string(runes[:maxNicknameRunes-len(suffix)]))
|
||||
for n := 2; ; n++ {
|
||||
suffix := fmt.Sprintf(" %d", n)
|
||||
trimmed := name
|
||||
if runes := []rune(name); len(runes)+len(suffix) > maxNicknameRunes {
|
||||
trimmed = strings.TrimSpace(string(runes[:maxNicknameRunes-len(suffix)]))
|
||||
}
|
||||
if candidate := trimmed + suffix; !slices.Contains(taken, candidate) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return name + suffix
|
||||
}
|
||||
@@ -109,8 +109,11 @@ func TestStrangerCannotResignForASeatedPlayer(t *testing.T) {
|
||||
stranger.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_JoinRoom{
|
||||
JoinRoom: &noituv1.JoinRoom{RoomCode: code},
|
||||
}})
|
||||
if got := stranger.await("error").GetError().GetCode(); got != "room_full" {
|
||||
t.Fatalf("third joiner got %q, want room_full", got)
|
||||
// Not room_full: a four-seat room with two people in it has seats going
|
||||
// spare. Arriving in the middle of a game is what is refused, and a
|
||||
// stranger holding the code is refused it like anybody else.
|
||||
if got := stranger.await("error").GetError().GetCode(); got != "game_in_progress" {
|
||||
t.Fatalf("third joiner got %q, want game_in_progress", got)
|
||||
}
|
||||
|
||||
stranger.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_Resign{Resign: &noituv1.Resign{}}})
|
||||
@@ -244,6 +247,15 @@ func TestPvPGameRunsToAWinner(t *testing.T) {
|
||||
hostTurn := awaitMyTurn(t, host)
|
||||
host.submit("d e", hostTurn.GetTurnSeq())
|
||||
|
||||
// The elimination comes first and carries what the position had left, so
|
||||
// it has to be read before the result it caused.
|
||||
guestOut := guest.await("player_eliminated").GetPlayerEliminated()
|
||||
hostOut := host.await("player_eliminated").GetPlayerEliminated()
|
||||
|
||||
if !guestOut.GetIsMe() || hostOut.GetIsMe() {
|
||||
t.Error("is_me should be true only for the player who went out")
|
||||
}
|
||||
|
||||
hostOver := host.await("game_over").GetGameOver()
|
||||
guestOver := guest.await("game_over").GetGameOver()
|
||||
|
||||
@@ -258,12 +270,14 @@ func TestPvPGameRunsToAWinner(t *testing.T) {
|
||||
t.Errorf("reason = %v, want NO_LEGAL_MOVE", hostOver.GetReason())
|
||||
}
|
||||
|
||||
// Nothing could have been played, and the empty list is how the loser is
|
||||
// told so. The winner is never sent one.
|
||||
if got := guestOver.GetSuggestions(); len(got) != 0 {
|
||||
// Nothing could have been played, and the empty list is how the player who
|
||||
// was stuck is told so. It rides on the elimination rather than the
|
||||
// result: it describes the position they were looking at, which by the end
|
||||
// of a longer game is nobody else's position.
|
||||
if got := guestOut.GetSuggestions(); len(got) != 0 {
|
||||
t.Errorf("the losing player was offered %v out of a dead end, want nothing", got)
|
||||
}
|
||||
if got := hostOver.GetSuggestions(); len(got) != 0 {
|
||||
if got := hostOut.GetSuggestions(); len(got) != 0 {
|
||||
t.Errorf("the winner was sent suggestions %v, want none", got)
|
||||
}
|
||||
}
|
||||
@@ -278,16 +292,18 @@ func TestLosingPlayerIsToldWhatCouldHaveBeenPlayed(t *testing.T) {
|
||||
// The opening is "a b", so "b c" is still there to be played.
|
||||
guest.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_Resign{Resign: &noituv1.Resign{}}})
|
||||
|
||||
guestOut := guest.await("player_eliminated").GetPlayerEliminated()
|
||||
hostOut := host.await("player_eliminated").GetPlayerEliminated()
|
||||
|
||||
guestOver := guest.await("game_over").GetGameOver()
|
||||
hostOver := host.await("game_over").GetGameOver()
|
||||
|
||||
if guestOver.GetIWon() {
|
||||
t.Fatal("the player who resigned was told they won")
|
||||
}
|
||||
if got := guestOver.GetSuggestions(); !slices.Equal(got, []string{"b c"}) {
|
||||
if got := guestOut.GetSuggestions(); !slices.Equal(got, []string{"b c"}) {
|
||||
t.Errorf("suggestions = %v, want [b c]", got)
|
||||
}
|
||||
if got := hostOver.GetSuggestions(); len(got) != 0 {
|
||||
if got := hostOut.GetSuggestions(); len(got) != 0 {
|
||||
t.Errorf("the winner was sent suggestions %v, want none", got)
|
||||
}
|
||||
}
|
||||
@@ -325,7 +341,7 @@ func TestOpponentNeverSeesAnUnsanitizedNickname(t *testing.T) {
|
||||
JoinRoom: &noituv1.JoinRoom{RoomCode: code},
|
||||
}})
|
||||
|
||||
shown := host.await("room_state").GetRoomState().GetOpponentName()
|
||||
shown := otherSlot(host.await("room_state").GetRoomState()).GetName()
|
||||
for _, r := range []rune{nul, zeroWidthSpace, bidiOverride} {
|
||||
if strings.ContainsRune(shown, r) {
|
||||
t.Errorf("opponent name %q still carries the invisible rune %U", shown, r)
|
||||
@@ -412,7 +428,7 @@ func TestResumeAfterGameEndedLandsInTheLobby(t *testing.T) {
|
||||
if state.GetRoomCode() != code {
|
||||
t.Errorf("resumed into room %q, want %q", state.GetRoomCode(), code)
|
||||
}
|
||||
if !state.GetOpponentPresent() {
|
||||
if otherSlot(state) == nil {
|
||||
t.Errorf("the player who stayed is missing from the resumed lobby: %+v", state)
|
||||
}
|
||||
}
|
||||
|
||||
+462
-256
File diff suppressed because it is too large.
Load diff
@@ -460,7 +460,7 @@ func (s *session) dispatch(msg *noituv1.ClientMessage) error {
|
||||
s.toRoom(lobbyInput{sess: s, action: lobbyStart})
|
||||
|
||||
case *noituv1.ClientMessage_KickPlayer:
|
||||
s.toRoom(lobbyInput{sess: s, action: lobbyKick})
|
||||
s.toRoom(lobbyInput{sess: s, action: lobbyKick, target: playerIDFor(p.KickPlayer.GetPlayerId())})
|
||||
|
||||
case *noituv1.ClientMessage_LeaveRoom:
|
||||
s.toRoom(lobbyInput{sess: s, action: lobbyLeave})
|
||||
@@ -492,8 +492,8 @@ func (s *session) dispatch(msg *noituv1.ClientMessage) error {
|
||||
|
||||
// toRoom forwards one lobby action to the room this connection is seated in.
|
||||
//
|
||||
// Rate-limited like a submission: every accepted action is broadcast to both
|
||||
// seats, so an unbounded one lets a player flood the other's outbox until
|
||||
// Rate-limited like a submission: every accepted action is broadcast to every
|
||||
// seat, so an unbounded one lets a player flood the other's outbox until
|
||||
// their session is closed for falling behind. A dropped action would leave a
|
||||
// button that did nothing and no reason why, so every failure answers.
|
||||
func (s *session) toRoom(in lobbyInput) {
|
||||
|
||||
@@ -51,7 +51,9 @@ func clientVariants() map[string]*noituv1.ClientMessage {
|
||||
Ready: true,
|
||||
}}},
|
||||
"client_start_game": {Payload: &noituv1.ClientMessage_StartGame{StartGame: &noituv1.StartGame{}}},
|
||||
"client_kick_player": {Payload: &noituv1.ClientMessage_KickPlayer{KickPlayer: &noituv1.KickPlayer{}}},
|
||||
"client_kick_player": {Payload: &noituv1.ClientMessage_KickPlayer{KickPlayer: &noituv1.KickPlayer{
|
||||
PlayerId: "p3",
|
||||
}}},
|
||||
"client_leave_room": {Payload: &noituv1.ClientMessage_LeaveRoom{LeaveRoom: &noituv1.LeaveRoom{}}},
|
||||
"client_send_chat": {Payload: &noituv1.ClientMessage_SendChat{SendChat: &noituv1.SendChat{
|
||||
Text: "Chào bạn, ván này khó thật!",
|
||||
@@ -65,7 +67,7 @@ func serverVariants() map[string]*noituv1.ServerMessage {
|
||||
"server_welcome": {Payload: &noituv1.ServerMessage_Welcome{Welcome: &noituv1.Welcome{
|
||||
SessionId: "s-1a2b3c",
|
||||
ResumeToken: "r-8f2c",
|
||||
ProtocolVersion: 1,
|
||||
ProtocolVersion: ProtocolVersion,
|
||||
AcceptedNickname: "Người chơi ẩn danh",
|
||||
}}},
|
||||
"server_game_started": {Payload: &noituv1.ServerMessage_GameStarted{GameStarted: &noituv1.GameStarted{
|
||||
@@ -75,6 +77,8 @@ func serverVariants() map[string]*noituv1.ServerMessage {
|
||||
DeadlineUnixMs: 1756998020000,
|
||||
TurnSeq: 1,
|
||||
TurnLimitMs: 20000,
|
||||
Players: fixtureScores(),
|
||||
TurnPlayerId: "p1",
|
||||
}}},
|
||||
"server_turn_update": {Payload: &noituv1.ServerMessage_TurnUpdate{TurnUpdate: &noituv1.TurnUpdate{
|
||||
Played: &noituv1.PlayedWord{
|
||||
@@ -83,14 +87,15 @@ func serverVariants() map[string]*noituv1.ServerMessage {
|
||||
ByMe: false,
|
||||
Points: 2,
|
||||
Syllables: 2,
|
||||
PlayerId: "p2",
|
||||
},
|
||||
CurrentSyllable: "yên",
|
||||
MyTurn: true,
|
||||
DeadlineUnixMs: 1756998040000,
|
||||
TurnSeq: 2,
|
||||
MyScore: 3,
|
||||
OpponentScore: 5,
|
||||
ChainLength: 2,
|
||||
Players: fixtureScores(),
|
||||
TurnPlayerId: "p3",
|
||||
}}},
|
||||
"server_move_rejected": {Payload: &noituv1.ServerMessage_MoveRejected{MoveRejected: &noituv1.MoveRejected{
|
||||
Reason: noituv1.RejectReason_REJECT_REASON_WRONG_LINK,
|
||||
@@ -100,16 +105,26 @@ func serverVariants() map[string]*noituv1.ServerMessage {
|
||||
"server_game_over": {Payload: &noituv1.ServerMessage_GameOver{GameOver: &noituv1.GameOver{
|
||||
IWon: false,
|
||||
Reason: noituv1.GameEndReason_GAME_END_REASON_NO_LEGAL_MOVE,
|
||||
MyScore: 7,
|
||||
ChainLength: 11,
|
||||
// A repeated string of Vietnamese words: the one field in the
|
||||
// contract whose encoding is neither a scalar nor a submessage.
|
||||
Suggestions: []string{"sinh viên", "sinh sôi"},
|
||||
}}},
|
||||
"server_opponent_left": {Payload: &noituv1.ServerMessage_OpponentLeft{OpponentLeft: &noituv1.OpponentLeft{
|
||||
CanReconnect: true,
|
||||
GraceMs: 30000,
|
||||
// Ranked, so a fixture that quietly reordered the table would show
|
||||
// up as ranks that no longer match their positions.
|
||||
Standings: []*noituv1.PlayerScore{
|
||||
{PlayerId: "p2", Name: "Khách mời", Score: 42, Connected: true, Rank: 1},
|
||||
{PlayerId: "p1", Name: "Người chơi", IsMe: true, Score: 7, Eliminated: true, Connected: true, Rank: 2},
|
||||
{PlayerId: "p3", Name: "Khách 2", Score: 3, Eliminated: true, Rank: 3},
|
||||
},
|
||||
}}},
|
||||
"server_player_eliminated": {Payload: &noituv1.ServerMessage_PlayerEliminated{
|
||||
PlayerEliminated: &noituv1.PlayerEliminated{
|
||||
PlayerId: "p1",
|
||||
Name: "Người chơi",
|
||||
IsMe: true,
|
||||
Reason: noituv1.GameEndReason_GAME_END_REASON_TIMEOUT,
|
||||
// A repeated string of Vietnamese words: the one field in the
|
||||
// contract whose encoding is neither a scalar nor a submessage.
|
||||
Suggestions: []string{"sinh viên", "sinh sôi"},
|
||||
},
|
||||
}},
|
||||
"server_error": {Payload: &noituv1.ServerMessage_Error{Error: &noituv1.ServerError{
|
||||
Code: "room_not_found",
|
||||
Message: "room_not_found",
|
||||
@@ -137,21 +152,35 @@ func serverVariants() map[string]*noituv1.ServerMessage {
|
||||
{FromMe: false, Author: "", Text: "Tôi phải đi", SentUnixMs: 1756998000789},
|
||||
},
|
||||
}}},
|
||||
// An owner looking at two guests, one ready and here and one ready but
|
||||
// away. Every boolean in a slot is load-bearing in at least one row,
|
||||
// and no two rows agree on all of them.
|
||||
"server_room_state": {Payload: &noituv1.ServerMessage_RoomState{RoomState: &noituv1.RoomState{
|
||||
RoomCode: "K7QX",
|
||||
// An owner looking at a guest who is here, ready, and connected:
|
||||
// the one combination in which every boolean is load-bearing.
|
||||
IAmOwner: true,
|
||||
CanStart: true,
|
||||
IAmReady: false,
|
||||
OpponentPresent: true,
|
||||
OpponentName: "Khách mời",
|
||||
OpponentReady: true,
|
||||
OpponentConnected: true,
|
||||
RoomCode: "K7QX",
|
||||
CanStart: true,
|
||||
MaxPlayers: 4,
|
||||
MinPlayers: 2,
|
||||
GraceMs: 30000,
|
||||
Players: []*noituv1.PlayerSlot{
|
||||
{PlayerId: "p1", Name: "Người chơi", IsMe: true, IsOwner: true, Connected: true},
|
||||
{PlayerId: "p2", Name: "Khách mời", Ready: true, Connected: true},
|
||||
{PlayerId: "p3", Name: "Khách 2", Ready: true},
|
||||
},
|
||||
}}},
|
||||
}
|
||||
}
|
||||
|
||||
// fixtureScores is the players table both in-game messages carry. Three seats
|
||||
// in three different states, so a fixture that dropped or reordered a row
|
||||
// cannot still decode to something plausible.
|
||||
func fixtureScores() []*noituv1.PlayerScore {
|
||||
return []*noituv1.PlayerScore{
|
||||
{PlayerId: "p1", Name: "Người chơi", IsMe: true, Score: 7, Connected: true},
|
||||
{PlayerId: "p2", Name: "Khách mời", Score: 42, Eliminated: true, Connected: true},
|
||||
{PlayerId: "p3", Name: "Khách 2", Score: 3},
|
||||
}
|
||||
}
|
||||
|
||||
// TestRoundTripEveryVariant marshals and unmarshals each oneof arm. Equality
|
||||
// alone is not enough: an empty arm such as CreateRoom encodes to a payload of
|
||||
// zero bytes, so checking that the case survived is what proves the arm is
|
||||
|
||||
@@ -186,6 +186,59 @@ func (c *testClient) await(want string) *noituv1.ServerMessage {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mySlot is the recipient's own row in a RoomState, which is the only place
|
||||
// their role and their readiness live now.
|
||||
func mySlot(state *noituv1.RoomState) *noituv1.PlayerSlot {
|
||||
for _, p := range state.GetPlayers() {
|
||||
if p.GetIsMe() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// otherSlot is the one other player in a two-player room, or nil when the
|
||||
// recipient is alone in it. Most of the lobby tests below are about two
|
||||
// people, so this is the shape they read the list in.
|
||||
func otherSlot(state *noituv1.RoomState) *noituv1.PlayerSlot {
|
||||
for _, p := range state.GetPlayers() {
|
||||
if !p.GetIsMe() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// slotFor finds one named seat, for the tests that seat more than two.
|
||||
func slotFor(state *noituv1.RoomState, id string) *noituv1.PlayerSlot {
|
||||
for _, p := range state.GetPlayers() {
|
||||
if p.GetPlayerId() == id {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// myScore and otherScore read a two-player turn update the way it used to
|
||||
// carry the numbers, out of the table that replaced the two fields.
|
||||
func myScore(u *noituv1.TurnUpdate) uint32 {
|
||||
for _, p := range u.GetPlayers() {
|
||||
if p.GetIsMe() {
|
||||
return p.GetScore()
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func otherScore(u *noituv1.TurnUpdate) uint32 {
|
||||
for _, p := range u.GetPlayers() {
|
||||
if !p.GetIsMe() {
|
||||
return p.GetScore()
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func payloadCase(m *noituv1.ServerMessage) string {
|
||||
switch m.GetPayload().(type) {
|
||||
case *noituv1.ServerMessage_Welcome:
|
||||
@@ -198,8 +251,8 @@ func payloadCase(m *noituv1.ServerMessage) string {
|
||||
return "move_rejected"
|
||||
case *noituv1.ServerMessage_GameOver:
|
||||
return "game_over"
|
||||
case *noituv1.ServerMessage_OpponentLeft:
|
||||
return "opponent_left"
|
||||
case *noituv1.ServerMessage_PlayerEliminated:
|
||||
return "player_eliminated"
|
||||
case *noituv1.ServerMessage_Error:
|
||||
return "error"
|
||||
case *noituv1.ServerMessage_Pong:
|
||||
@@ -326,9 +379,9 @@ func TestPvPGameAlternatesTurns(t *testing.T) {
|
||||
if !guestUpdate.GetMyTurn() {
|
||||
t.Error("opponent should now be on turn")
|
||||
}
|
||||
if hostUpdate.GetMyScore() != guestUpdate.GetOpponentScore() {
|
||||
if myScore(hostUpdate) != otherScore(guestUpdate) {
|
||||
t.Errorf("scores disagree across recipients: %d vs %d",
|
||||
hostUpdate.GetMyScore(), guestUpdate.GetOpponentScore())
|
||||
myScore(hostUpdate), otherScore(guestUpdate))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,8 +527,11 @@ func TestResumeWithinGraceRestoresGame(t *testing.T) {
|
||||
|
||||
_ = host.conn.Close(websocket.StatusGoingAway, "")
|
||||
|
||||
if left := guest.await("opponent_left").GetOpponentLeft(); !left.GetCanReconnect() {
|
||||
t.Error("opponent should be told the seat is being held")
|
||||
// Presence is part of the room's state now, so the seat being held is
|
||||
// something the other player reads there rather than in a message of its
|
||||
// own.
|
||||
if away := otherSlot(guest.await("room_state").GetRoomState()); away == nil || away.GetConnected() {
|
||||
t.Error("opponent should be shown as away while the seat is held")
|
||||
}
|
||||
|
||||
// Reconnect with the token and expect the position back.
|
||||
@@ -797,19 +853,31 @@ func TestSanitizeNicknameCountsRunesNotBytes(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestDistinguishSeparatesIdenticalNames covers the collision the fallback
|
||||
// creates — two players who both send nothing — and the one it always could:
|
||||
// two players who choose the same name.
|
||||
// creates — players who all send nothing — and the one it always could:
|
||||
// players who choose the same name.
|
||||
func TestDistinguishSeparatesIdenticalNames(t *testing.T) {
|
||||
if got := distinguish(defaultNickname, defaultNickname); got == defaultNickname {
|
||||
t.Error("two unnamed players must not render identically")
|
||||
}
|
||||
if got := distinguish("Minh", "Thuý"); got != "Minh" {
|
||||
if got := distinguish("Minh", []string{"Thuý"}); got != "Minh" {
|
||||
t.Errorf("distinct names should be left alone, got %q", got)
|
||||
}
|
||||
|
||||
// The suffix must not push the name past the cap.
|
||||
// A whole room of unnamed players, seated one at a time. Every one of them
|
||||
// has to end up with a name none of the others is already using: two
|
||||
// suffixed identically is the same failure as two unsuffixed.
|
||||
var taken []string
|
||||
for range maxPlayers {
|
||||
got := distinguish(defaultNickname, taken)
|
||||
if slices.Contains(taken, got) {
|
||||
t.Fatalf("distinguish returned %q, which is already in %v", got, taken)
|
||||
}
|
||||
if n := len([]rune(got)); n > maxNicknameRunes {
|
||||
t.Errorf("distinguished name %q is %d runes, over the %d cap", got, n, maxNicknameRunes)
|
||||
}
|
||||
taken = append(taken, got)
|
||||
}
|
||||
|
||||
// The suffix must not push a name that is already at the cap past it.
|
||||
long := strings.Repeat("a", maxNicknameRunes)
|
||||
if got := distinguish(long, long); len([]rune(got)) > maxNicknameRunes {
|
||||
if got := distinguish(long, []string{long}); len([]rune(got)) > maxNicknameRunes {
|
||||
t.Errorf("distinguished name is %d runes, over the %d cap", len([]rune(got)), maxNicknameRunes)
|
||||
}
|
||||
}
|
||||
@@ -988,9 +1056,11 @@ func (c *testClient) startGame() {
|
||||
c.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_StartGame{StartGame: &noituv1.StartGame{}}})
|
||||
}
|
||||
|
||||
func (c *testClient) kickPlayer() {
|
||||
func (c *testClient) kickPlayer(target string) {
|
||||
c.t.Helper()
|
||||
c.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_KickPlayer{KickPlayer: &noituv1.KickPlayer{}}})
|
||||
c.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_KickPlayer{
|
||||
KickPlayer: &noituv1.KickPlayer{PlayerId: target},
|
||||
}})
|
||||
}
|
||||
|
||||
func (c *testClient) say(text string) {
|
||||
@@ -1037,16 +1107,16 @@ func TestLobbyOpensWithNobodyReady(t *testing.T) {
|
||||
hostState := host.await("room_state").GetRoomState()
|
||||
guestState := guest.await("room_state").GetRoomState()
|
||||
|
||||
if !hostState.GetIAmOwner() {
|
||||
if !mySlot(hostState).GetIsOwner() {
|
||||
t.Error("the player who created the room does not own it")
|
||||
}
|
||||
if guestState.GetIAmOwner() {
|
||||
if mySlot(guestState).GetIsOwner() {
|
||||
t.Error("the player who joined was made owner")
|
||||
}
|
||||
if hostState.GetCanStart() || guestState.GetCanStart() {
|
||||
t.Error("a game can start with nobody ready")
|
||||
}
|
||||
if !hostState.GetOpponentPresent() || hostState.GetOpponentName() == "" {
|
||||
if otherSlot(hostState) == nil || otherSlot(hostState).GetName() == "" {
|
||||
t.Errorf("the owner cannot see who joined: %+v", hostState)
|
||||
}
|
||||
// Nothing starts on its own. Joining used to be the start signal, and a
|
||||
@@ -1065,10 +1135,10 @@ func TestReadyIsRenderedPerRecipient(t *testing.T) {
|
||||
guestState := guest.await("room_state").GetRoomState()
|
||||
hostState := host.await("room_state").GetRoomState()
|
||||
|
||||
if !guestState.GetIAmReady() || guestState.GetOpponentReady() {
|
||||
if !mySlot(guestState).GetReady() || otherSlot(guestState).GetReady() {
|
||||
t.Errorf("the guest should see only their own readiness, got %+v", guestState)
|
||||
}
|
||||
if hostState.GetIAmReady() || !hostState.GetOpponentReady() {
|
||||
if mySlot(hostState).GetReady() || !otherSlot(hostState).GetReady() {
|
||||
t.Errorf("the owner should see only the guest's readiness, got %+v", hostState)
|
||||
}
|
||||
if !hostState.GetCanStart() {
|
||||
@@ -1101,8 +1171,8 @@ func TestStartIsRefusedUntilTheGuestIsReady(t *testing.T) {
|
||||
|
||||
// Alone in the room.
|
||||
host.startGame()
|
||||
if got := host.await("error").GetError().GetCode(); got != "need_two_players" {
|
||||
t.Errorf("starting alone returned %q, want need_two_players", got)
|
||||
if got := host.await("error").GetError().GetCode(); got != "need_more_players" {
|
||||
t.Errorf("starting alone returned %q, want need_more_players", got)
|
||||
}
|
||||
|
||||
guest := dial(t, url)
|
||||
@@ -1141,7 +1211,7 @@ func TestOnlyTheOwnerStartsAndKicks(t *testing.T) {
|
||||
if got := guest.await("error").GetError().GetCode(); got != "not_the_owner" {
|
||||
t.Errorf("a guest starting the game returned %q, want not_the_owner", got)
|
||||
}
|
||||
guest.kickPlayer()
|
||||
guest.kickPlayer("p1")
|
||||
if got := guest.await("error").GetError().GetCode(); got != "not_the_owner" {
|
||||
t.Errorf("a guest kicking returned %q, want not_the_owner", got)
|
||||
}
|
||||
@@ -1155,7 +1225,7 @@ func TestNextGameNeedsAFreshReady(t *testing.T) {
|
||||
host, guest, first := pvpRoom(t, url)
|
||||
|
||||
hostState, guestState := resignAndSettle(t, host, guest)
|
||||
if hostState.GetOpponentReady() || guestState.GetIAmReady() {
|
||||
if otherSlot(hostState).GetReady() || mySlot(guestState).GetReady() {
|
||||
t.Error("the readiness that started the last game survived it")
|
||||
}
|
||||
if hostState.GetCanStart() {
|
||||
@@ -1194,7 +1264,7 @@ func TestLobbyActionsAreRefusedDuringAGame(t *testing.T) {
|
||||
if got := guest.await("error").GetError().GetCode(); got != "game_in_progress" {
|
||||
t.Errorf("unreadying mid-game returned %q, want game_in_progress", got)
|
||||
}
|
||||
host.kickPlayer()
|
||||
host.kickPlayer("p2")
|
||||
if got := host.await("error").GetError().GetCode(); got != "game_in_progress" {
|
||||
t.Errorf("kicking mid-game returned %q, want game_in_progress", got)
|
||||
}
|
||||
@@ -1226,10 +1296,10 @@ func TestLeavingNeedsAnUnreadyFirst(t *testing.T) {
|
||||
|
||||
// The room survives: the owner is still in it, now on their own.
|
||||
alone := host.await("room_state").GetRoomState()
|
||||
if alone.GetOpponentPresent() {
|
||||
if otherSlot(alone) != nil {
|
||||
t.Errorf("the owner still sees a guest who left: %+v", alone)
|
||||
}
|
||||
if !alone.GetIAmOwner() || alone.GetCanStart() {
|
||||
if !mySlot(alone).GetIsOwner() || alone.GetCanStart() {
|
||||
t.Errorf("the room the owner is left with is wrong: %+v", alone)
|
||||
}
|
||||
}
|
||||
@@ -1242,19 +1312,19 @@ func TestKickFreesAnUnreadySeatOnly(t *testing.T) {
|
||||
|
||||
guest.setReady(true)
|
||||
host.await("room_state")
|
||||
host.kickPlayer()
|
||||
host.kickPlayer("p2")
|
||||
if got := host.await("error").GetError().GetCode(); got != "player_is_ready" {
|
||||
t.Errorf("kicking a ready guest returned %q, want player_is_ready", got)
|
||||
}
|
||||
|
||||
guest.setReady(false)
|
||||
host.await("room_state")
|
||||
host.kickPlayer()
|
||||
host.kickPlayer("p2")
|
||||
|
||||
if got := guest.await("error").GetError().GetCode(); got != "kicked" {
|
||||
t.Errorf("the kicked player was told %q", got)
|
||||
}
|
||||
if got := host.await("room_state").GetRoomState(); got.GetOpponentPresent() {
|
||||
if got := host.await("room_state").GetRoomState(); otherSlot(got) != nil {
|
||||
t.Errorf("the kicked seat is still occupied: %+v", got)
|
||||
}
|
||||
|
||||
@@ -1262,7 +1332,7 @@ func TestKickFreesAnUnreadySeatOnly(t *testing.T) {
|
||||
guest.send(&noituv1.ClientMessage{Payload: &noituv1.ClientMessage_JoinRoom{
|
||||
JoinRoom: &noituv1.JoinRoom{RoomCode: code},
|
||||
}})
|
||||
if got := guest.await("room_state").GetRoomState(); !got.GetOpponentPresent() {
|
||||
if got := guest.await("room_state").GetRoomState(); otherSlot(got) == nil {
|
||||
t.Errorf("a kicked player could not come back: %+v", got)
|
||||
}
|
||||
}
|
||||
@@ -1276,10 +1346,10 @@ func TestOwnerLeavingPromotesTheOtherPlayer(t *testing.T) {
|
||||
host.leaveRoom()
|
||||
|
||||
promoted := guest.await("room_state").GetRoomState()
|
||||
if !promoted.GetIAmOwner() {
|
||||
if !mySlot(promoted).GetIsOwner() {
|
||||
t.Errorf("the player left behind was not promoted: %+v", promoted)
|
||||
}
|
||||
if promoted.GetOpponentPresent() {
|
||||
if otherSlot(promoted) != nil {
|
||||
t.Errorf("the owner who left is still shown as present: %+v", promoted)
|
||||
}
|
||||
|
||||
@@ -1314,7 +1384,7 @@ func TestPromotedOwnerLosesTheirReadiness(t *testing.T) {
|
||||
host.leaveRoom()
|
||||
|
||||
promoted := guest.await("room_state").GetRoomState()
|
||||
if promoted.GetIAmReady() {
|
||||
if mySlot(promoted).GetReady() {
|
||||
t.Errorf("the promoted owner is still carrying a guest's readiness: %+v", promoted)
|
||||
}
|
||||
}
|
||||
@@ -1563,7 +1633,7 @@ func TestChatHistoryIsCappedAndOrdered(t *testing.T) {
|
||||
sess := offlineSession(t, chatHistoryLimit+64)
|
||||
r := &room{
|
||||
code: "TESTRM",
|
||||
seats: [2]*seat{{id: "p1", nickname: "Chủ phòng", sess: sess}},
|
||||
seats: [maxPlayers]*seat{{id: "p1", nickname: "Chủ phòng", sess: sess}},
|
||||
owner: "p1",
|
||||
}
|
||||
|
||||
@@ -1716,7 +1786,7 @@ func TestChatFromASeatlessConnectionIsRefused(t *testing.T) {
|
||||
_, url := newTestServer(t, chainDict(), Config{})
|
||||
host, guest, _ := pvpLobby(t, url)
|
||||
|
||||
host.kickPlayer()
|
||||
host.kickPlayer("p2")
|
||||
if got := guest.await("error").GetError().GetCode(); got != "kicked" {
|
||||
t.Fatalf("the guest was told %q rather than being kicked", got)
|
||||
}
|
||||
@@ -1889,7 +1959,7 @@ func TestChatFromAConnectionThatLostItsSeatIsRefused(t *testing.T) {
|
||||
|
||||
r := &room{
|
||||
code: "TESTRM",
|
||||
seats: [2]*seat{{id: "p1", nickname: "Chủ phòng", sess: evicted}},
|
||||
seats: [maxPlayers]*seat{{id: "p1", nickname: "Chủ phòng", sess: evicted}},
|
||||
owner: "p1",
|
||||
}
|
||||
|
||||
@@ -1925,7 +1995,7 @@ func TestChatToAFullOutboxIsDroppedNotFatal(t *testing.T) {
|
||||
|
||||
r := &room{
|
||||
code: "TESTRM",
|
||||
seats: [2]*seat{
|
||||
seats: [maxPlayers]*seat{
|
||||
{id: "p1", nickname: "Chủ phòng", sess: sender},
|
||||
{id: "p2", nickname: "Khách", sess: slow},
|
||||
},
|
||||
@@ -2002,10 +2072,10 @@ func TestResumeReclaimsALobbySeat(t *testing.T) {
|
||||
second.await("welcome")
|
||||
|
||||
back := second.await("room_state").GetRoomState()
|
||||
if !back.GetIAmOwner() {
|
||||
if !mySlot(back).GetIsOwner() {
|
||||
t.Errorf("the owner came back as a guest: %+v", back)
|
||||
}
|
||||
if !back.GetOpponentPresent() || back.GetRoomCode() != code {
|
||||
if otherSlot(back) == nil || back.GetRoomCode() != code {
|
||||
t.Errorf("the resumed lobby is not the one they left: %+v", back)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user