Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
yndajas committed Feb 21, 2024
1 parent 349f4b0 commit da3bf0d
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 190 deletions.
1 change: 1 addition & 0 deletions client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const showStartButton = (): void => {
};

startButton.addEventListener("click", () => {
console.log("emitting start round");
socket.emit("round:start");
});

Expand Down
9 changes: 4 additions & 5 deletions server/lobby.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import type { Socket } from "socket.io";
import type { InterpreterFrom } from "xstate";
import { interpret } from "xstate";
import { Actor, createActor } from "xstate";

import type { Player } from "./@types/models";
import { context, lobbyMachine } from "./machines/lobby";
import type { SocketServer } from "./socketServer";

class Lobby {
machine: InterpreterFrom<typeof lobbyMachine>;
machine: Actor<typeof lobbyMachine>;
server: SocketServer;

constructor(server: SocketServer) {
this.server = server;
this.machine = interpret(lobbyMachine.withContext({ ...context })).start();
this.machine = createActor(lobbyMachine, { ...context }).start();
this.machine.start();
this.machine.onTransition((state) => {
this.machine.subscribe((state) => {
console.info({ context: state.context, state: state.value });

switch (state.value) {
Expand Down
55 changes: 41 additions & 14 deletions server/machines/lobby.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it } from "bun:test";
import type { InterpreterFrom } from "xstate";
import { interpret } from "xstate";
import type { Actor } from "xstate";
import { createActor, getNextSnapshot } from "xstate";

import { context, isNewPlayer, lobbyMachine } from "./lobby";

Expand All @@ -11,17 +11,27 @@ describe("lobbyMachine states", () => {

describe("empty", () => {
it("transitions to the onePlayer state when it receives the player joins event", () => {
expect(lobbyMachine.transition("empty", "playerJoins").value).toBe(
"onePlayer",
);
expect(
getNextSnapshot(
lobbyMachine,
lobbyMachine.resolveState({
value: "empty",
context: { players: [] },
}),
{
type: "playerJoins",
player: player1,
},
).value,
).toBe("onePlayer");
});
});

describe("onePlayer", () => {
let actor: InterpreterFrom<typeof lobbyMachine>;
let actor: Actor<typeof lobbyMachine>;

beforeEach(() => {
actor = interpret(lobbyMachine);
actor = createActor(lobbyMachine);
actor.start();
actor.send({
player: player1,
Expand All @@ -43,9 +53,16 @@ describe("lobbyMachine states", () => {
});

it("transitions from onePlayer to empty state when it receives player leaves event", () => {
expect(lobbyMachine.transition("onePlayer", "playerLeaves").value).toBe(
"empty",
);
expect(
getNextSnapshot(
lobbyMachine,
lobbyMachine.resolveState({
value: "onePlayer",
context: { players: [player1] },
}),
{ type: "playerLeaves", socketId: player1.socketId },
).value,
).toBe("empty");
});

it("removes a player from the player list when it receives playerLeaves event", () => {
Expand All @@ -59,10 +76,10 @@ describe("lobbyMachine states", () => {
});

describe("multiplePlayers", () => {
let actor: InterpreterFrom<typeof lobbyMachine>;
let actor: Actor<typeof lobbyMachine>;

beforeEach(() => {
actor = interpret(lobbyMachine);
actor = createActor(lobbyMachine);
actor.start();

actor.send({
Expand Down Expand Up @@ -136,13 +153,23 @@ describe("isNewPlayer", () => {
it("returns true if the player is not present in the players array", () => {
const player = { name: "a name", socketId: "id" };
const contextWithNoPlayers = { players: [] };
expect(isNewPlayer(contextWithNoPlayers, { player })).toBe(true);
expect(
isNewPlayer({
context: contextWithNoPlayers,
event: { player, type: "playerJoins" },
}),
).toBe(true);
});

it("returns false if the player is present in the players array", () => {
const player = { name: "a name", socketId: "id" };
const contextWithPlayer = { players: [player] };

expect(isNewPlayer(contextWithPlayer, { player })).toBe(false);
expect(
isNewPlayer({
context: contextWithPlayer,
event: { player, type: "playerJoins" },
}),
).toBe(false);
});
});
46 changes: 24 additions & 22 deletions server/machines/lobby.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assign, createMachine } from "xstate";
import { EventObject, assign, createMachine } from "xstate";

import { GuardArgs } from "xstate/guards";
import type { Player } from "../@types/models";

const context = {
Expand All @@ -8,11 +9,13 @@ const context = {

type Context = typeof context;

type PlayerJoinsEvent = {
player: Player;
type: "playerJoins";
};

type Events =
| {
player: Player;
type: "playerJoins";
}
| PlayerJoinsEvent
| {
socketId: Player["socketId"];
type: "playerLeaves";
Expand All @@ -21,33 +24,31 @@ type Events =
type: "playerClicksStart";
};

const isNewPlayer = (
{ players }: { players: Player[] },
{ player: playerFromEvent }: { player: Player },
) =>
players.find((player) => player.socketId === playerFromEvent.socketId) ===
undefined;
const isNewPlayer = (args: GuardArgs<Context, PlayerJoinsEvent>) =>
args.context.players.find(
(player) => player.socketId === args.event.player.socketId,
) === undefined;

const isOnlyPlayer = ({ players }: { players: Player[] }) =>
players.length === 1;
const isOnlyPlayer = (args: GuardArgs<Context, EventObject>) =>
args.context.players.length === 1;

const lobbyMachine = createMachine(
{
context: context,
context,
id: "lobby",
initial: "empty",
predictableActionArguments: true,
schema: {
types: {
context: {} as Context,
events: {} as Events,
typegen: {} as import("./lobby.typegen").Typegen0,
},
states: {
empty: {
on: { playerJoins: { actions: "addPlayer", target: "onePlayer" } },
},
multiplePlayers: {
always: {
cond: "isOnlyPlayer",
guard: "isOnlyPlayer",
target: "onePlayer",
},
on: {
Expand All @@ -59,7 +60,7 @@ const lobbyMachine = createMachine(
on: {
playerJoins: {
actions: "addPlayer",
cond: "isNewPlayer",
guard: "isNewPlayer",
target: "multiplePlayers",
},
playerLeaves: {
Expand All @@ -69,16 +70,17 @@ const lobbyMachine = createMachine(
},
},
},
tsTypes: {} as import("./lobby.typegen").Typegen0,
},
{
actions: {
addPlayer: assign({
players: ({ players }, { player }) => [...players, player],
players: (args) => [...args.context.players, args.event.player],
}),
removePlayer: assign({
players: ({ players }, { socketId }) =>
players.filter((p) => p.socketId !== socketId),
players: (args) =>
args.context.players.filter(
(player) => player.socketId !== args.event.socketId,
),
}),
},
guards: {
Expand Down
16 changes: 8 additions & 8 deletions server/machines/round.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { assign, createMachine } from "xstate";
import { Answer } from "../@types/models";
import questions from "../data/questions.json";

type Question = {
answer: string[];
Expand All @@ -9,7 +10,7 @@ type Question = {

const context = {
answers: [] as Answer[],
questions: [] as Question[],
questions: questions as Question[],
selectedQuestion: {} as Question | undefined,
};

Expand All @@ -25,10 +26,10 @@ const gameMachine = createMachine(
context,
id: "game",
initial: "gameStart",
predictableActionArguments: true,
schema: {
types: {
context: {} as Context,
events: {} as Events,
typegen: {} as import("./round.typegen").Typegen0,
},
states: {
gameStart: {
Expand All @@ -38,19 +39,18 @@ const gameMachine = createMachine(
},
},
},
tsTypes: {} as import("./round.typegen").Typegen0,
},
{
actions: {
addAnswer: assign({
answers: (context, event) => [...context.answers, event.answer],
answers: (args) => [...args.context.answers, args.event.answer],
}),
setQuestion: assign({
selectedQuestion: ({ questions }) => {
selectedQuestion: (args) => {
const questionIndex = Math.floor(
Math.random() * (questions.length - 1),
Math.random() * (args.context.questions.length - 1),
);
return questions[questionIndex];
return args.context.questions[questionIndex];
},
}),
},
Expand Down
15 changes: 7 additions & 8 deletions server/round.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import type { InterpreterFrom } from "xstate";
import { interpret } from "xstate";
import type { Actor } from "xstate";
import { createActor } from "xstate";

import { Answer, Question } from "./@types/models";
import questions from "./data/questions.json";
import { context, gameMachine } from "./machines/round";
import type { SocketServer } from "./socketServer";

class Round {
machine: InterpreterFrom<typeof gameMachine>;
machine: Actor<typeof gameMachine>;
server: SocketServer;

constructor(server: SocketServer) {
this.server = server;
this.machine = interpret(
gameMachine.withContext({ ...context, questions }),
).start();
this.machine = createActor(gameMachine, { ...context }).start();

this.machine.onTransition((state) => {
console.log(this.machine);

this.machine.subscribe((state) => {
console.info({ context: state.context, state: state.value });

switch (state.value) {
Expand Down
2 changes: 2 additions & 0 deletions server/socketServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ export class SocketServer {
}

onQuestionSet(question: Question) {
console.log("emitting question set");
this.server.emit(...clientboundEvents.getQuestion(question));
}

onRoundStarted() {
console.log("got to onRoundStarted");
this.round = new Round(this);
}

Expand Down
Loading

0 comments on commit da3bf0d

Please sign in to comment.