Skip to content

Commit 93d1cb8

Browse files
committed
feat: litegame reconnection strategies
1 parent 2704614 commit 93d1cb8

6 files changed

Lines changed: 109 additions & 28 deletions

File tree

client/src/api/lobby/response.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ export function applyLobbyResponses(room: Room, component: any, lobbyType: Lobby
8787
export function applyLiteLobbyResponses(room: Room, component: any) {
8888
const store = component.$tstore;
8989

90+
// always clear the lobby clients on reconnect
91+
store.commit("CLEAR_LOBBY_CLIENTS");
92+
9093
room.onError((code: number, message?: string) => {
9194
console.log(`Error ${code} occurred in room: ${message} `);
9295
alert("sorry, we encountered an error, please try refreshing the page or contact us");

client/src/store/mutations/lobby.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { State, defaultLobbyState } from "@port-of-mars/shared/game/client/state
22
import { LobbyChatMessageData, LobbyClientData, LobbyType } from "@port-of-mars/shared/types";
33

44
export default {
5+
CLEAR_LOBBY_CLIENTS(state: State) {
6+
state.lobby.clients = [];
7+
},
8+
59
SET_LOBBY_GROUP_SIZE(state: State, payload: number) {
610
state.lobby.groupSize = payload;
711
},

client/src/views/ProlificMultiplayerStudy.vue

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<template>
22
<div class="backdrop d-flex flex-column justify-content-center align-items-center">
3+
<button @click="leaveAll">leave all</button>
4+
<p v-if="reconnectTimeout && !isTransitioning">Connection lost. Attempting to reconnect...</p>
35
<b-alert v-if="started" variant="warning" show dismissible>
46
<small>
57
<p>
@@ -17,7 +19,7 @@
1719
<p>{{ joinFailureReason }}</p>
1820
</div>
1921
<div
20-
v-if="!started && lobbyRoom"
22+
v-else-if="!started && lobbyRoom"
2123
class="mt-5 text-center p-5"
2224
style="max-width: 30rem; margin: auto"
2325
>
@@ -30,7 +32,7 @@
3032
other participants to join. If 5 minutes passes since the last player joined the lobby,
3133
you will be redirected back to Prolific and compensated for your time.
3234
</p>
33-
<p>Please <b>do not</b> refresh this page</p>
35+
<p>Please <b>do not</b> refresh this page as this will reset the timer</p>
3436
</small>
3537
</div>
3638

@@ -143,7 +145,7 @@ export default class ProlificMultiplayerStudy extends Vue {
143145
}
144146
145147
get gameOverText() {
146-
return this.isSecondGame
148+
return this.isStudyComplete || this.isSecondGame
147149
? "You have completed the study. Thank you for your participation!"
148150
: "You will be advanced to the next game in a few seconds..";
149151
}
@@ -168,16 +170,7 @@ export default class ProlificMultiplayerStudy extends Vue {
168170
async created() {
169171
await this.fetchParticipantStatus();
170172
if (this.participantStatus.status === "not-started") {
171-
try {
172-
await this.joinLobby();
173-
} catch (error) {
174-
console.error("Failed to join lobby:", error);
175-
if (error instanceof Error) {
176-
this.joinFailureReason = error.message;
177-
} else {
178-
this.joinFailureReason = "Failed to join lobby. Please try refreshing the page.";
179-
}
180-
}
173+
await this.joinLobby();
181174
} else if (this.participantStatus.status === "in-progress") {
182175
if (this.participantStatus.activeRoomId) {
183176
// try to re-join active game
@@ -186,24 +179,81 @@ export default class ProlificMultiplayerStudy extends Vue {
186179
}
187180
}
188181
189-
private async joinLobby() {
190-
this.lobbyRoom = await this.$client.joinOrCreate(LITE_LOBBY_NAME, { type: "prolificBaseline" });
191-
applyLiteLobbyResponses(this.lobbyRoom, this);
192-
this.lobbyApi.connect(this.lobbyRoom);
182+
private async onLobbyLeave() {
183+
this.lobbyRoom = null;
184+
this.scheduleReconnect();
185+
}
186+
187+
private async onGameLeave() {
188+
this.gameRoom = null;
189+
this.scheduleReconnect();
190+
}
191+
192+
private reconnectTimeout: number | null = null;
193+
private isTransitioning = false;
193194
194-
// intercept lobby -> game messages
195-
this.lobbyRoom.onMessage("removed-client-from-lobby", () => this.transitionToGame());
196-
this.lobbyRoom.onMessage("join-existing-game", () => this.transitionToGame());
195+
private async scheduleReconnect() {
196+
if (this.reconnectTimeout) {
197+
clearTimeout(this.reconnectTimeout);
198+
}
199+
this.reconnectTimeout = setTimeout(async () => {
200+
this.reconnectTimeout = null;
201+
this.attemptReconnect();
202+
}, 10000);
203+
}
204+
205+
private async attemptReconnect() {
206+
// abort if we're already connected
207+
if (this.lobbyRoom || this.gameRoom) return;
208+
await this.fetchParticipantStatus();
209+
if (this.participantStatus.status === "in-progress" && this.participantStatus.activeRoomId) {
210+
await this.joinGame(this.participantStatus.activeRoomId);
211+
} else if (this.participantStatus.status === "not-started") {
212+
await this.joinLobby();
213+
}
214+
}
215+
216+
private async joinLobby() {
217+
try {
218+
this.lobbyRoom = await this.$client.joinOrCreate(LITE_LOBBY_NAME, {
219+
type: "prolificBaseline",
220+
});
221+
applyLiteLobbyResponses(this.lobbyRoom, this);
222+
this.lobbyApi.connect(this.lobbyRoom);
223+
// intercept lobby -> game messages
224+
this.lobbyRoom.onMessage("removed-client-from-lobby", () => this.transitionToGame());
225+
this.lobbyRoom.onMessage("join-existing-game", () => this.transitionToGame());
226+
this.lobbyRoom.onLeave(() => this.onLobbyLeave());
227+
} catch (error) {
228+
console.error("Failed to join lobby:", error);
229+
this.joinFailureReason =
230+
"Failed to join lobby. Please try refreshing the page. If the problem persists, please contact the researcher with error details and you will be compensated for your time.";
231+
if (error instanceof Error) {
232+
this.joinFailureReason += ` Error details: ${error.message}`;
233+
}
234+
}
197235
}
198236
199237
private async joinGame(roomId: string) {
200-
this.gameRoom = await this.$client.joinById(roomId);
201-
applyMultiplayerGameServerResponses(this.gameRoom, this, this.$tstore.state.user);
202-
this.api.connect(this.gameRoom);
203-
this.started = true;
238+
try {
239+
this.gameRoom = await this.$client.joinById(roomId);
240+
applyMultiplayerGameServerResponses(this.gameRoom, this, this.$tstore.state.user);
241+
this.api.connect(this.gameRoom);
242+
this.started = true;
243+
this.gameRoom.onLeave(() => this.onGameLeave());
244+
} catch (error) {
245+
console.error("Failed to join game:", error);
246+
this.joinFailureReason =
247+
"Failed to join game. Please try refreshing the page. If the problem persists, please contact the researcher with error details and you will be compensated for your time.";
248+
if (error instanceof Error) {
249+
this.joinFailureReason += ` Error details: ${error.message}`;
250+
}
251+
}
204252
}
205253
206254
private async transitionToGame() {
255+
this.isTransitioning = true;
256+
console.log(this.isTransitioning);
207257
// leave lobby and join the real game room
208258
await this.lobbyRoom!.leave();
209259
this.lobbyApi.leave();
@@ -212,6 +262,12 @@ export default class ProlificMultiplayerStudy extends Vue {
212262
return console.error("Missing roomId");
213263
}
214264
await this.joinGame(roomId);
265+
// clear timeout
266+
if (this.reconnectTimeout) {
267+
clearTimeout(this.reconnectTimeout);
268+
this.reconnectTimeout = null;
269+
}
270+
this.isTransitioning = false;
215271
}
216272
217273
async handleContinue() {

server/src/services/litegame.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ export class LiteGameService extends BaseService {
601601
.leftJoinAndSelect("deckCard.round", "round")
602602
.leftJoinAndSelect("round.game", "game")
603603
.leftJoinAndSelect("deckCard.card", "eventCard")
604-
.leftJoinAndSelect("round.game.players", "gamePlayers") // need to count players
604+
.leftJoinAndSelect("game.players", "gamePlayers") // need to count players
605605
.where("deckCard.roundId IS NOT NULL"); // ensure card was actually drawn in a round
606606

607607
if (gameIds && gameIds.length > 0) {

server/src/services/study.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { Repository } from "typeorm";
1515
import { settings } from "@port-of-mars/server/settings";
1616
import { BaseService } from "@port-of-mars/server/services/db";
17-
import { generateUsername, getRandomIntInclusive, ServerError } from "@port-of-mars/server/util";
17+
import { generateProbablyUniqueUsername, generateUsername, getRandomIntInclusive, ServerError } from "@port-of-mars/server/util";
1818
import { LiteGameType } from "@port-of-mars/shared/lite";
1919
import {
2020
ProlificMultiplayerParticipantStatus,
@@ -599,11 +599,17 @@ export class MultiplayerStudyService extends BaseStudyService {
599599
if (!participant) {
600600
// create a new user if not found
601601
const user = new User();
602-
user.username = await generateUsername();
602+
user.username = generateProbablyUniqueUsername();
603603
user.name = "";
604604
user.dateConsented = new Date(); // assuming this happens externally
605605
user.isSystemBot = false;
606-
await this.getUserRepository().save(user);
606+
try {
607+
await this.getUserRepository().save(user);
608+
} catch (e) {
609+
// if the username is already taken, try again
610+
user.username = generateProbablyUniqueUsername();
611+
await this.getUserRepository().save(user);
612+
}
607613
// create a new participant record and link to user
608614
participant = new ProlificMultiplayerStudyParticipant();
609615
participant.user = user;

server/src/util.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import _ from "lodash";
22
import * as assert from "assert";
3+
import { v4 as uuidv4 } from "uuid";
34
import { Builder, Loader, Parser, Resolver, fixturesIterator } from "typeorm-fixtures-cli/dist";
45
import { ROLES, DashboardMessage, GameType } from "@port-of-mars/shared/types";
56
import { GameOpts, GameStateOpts } from "@port-of-mars/server/rooms/pom/game/types";
@@ -164,6 +165,17 @@ export class ValidationError extends ServerError {
164165
}
165166
}
166167

168+
export function generateProbablyUniqueUsername() {
169+
/**
170+
* synchronous username gen that uses partial uuid
171+
* this is not guaranteed to be unique but is highly probable
172+
*/
173+
const adj = Math.floor(Math.random() * ADJECTIVES.length);
174+
const noun = Math.floor(Math.random() * NOUNS.length);
175+
const uuid = uuidv4();
176+
return ADJECTIVES[adj] + NOUNS[noun] + uuid.slice(-12);
177+
}
178+
167179
export async function generateUsername() {
168180
let isUnique = false;
169181
let username = "";

0 commit comments

Comments
 (0)