forked from virtualcommons/port-of-mars
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(WIP): add sologame room, state, logic commands
TODO: - implement service layer for persistence, recovery(?), and retrieval ref virtualcommons#856
- Loading branch information
Showing
8 changed files
with
311 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import _ from "lodash"; | ||
import { Command } from "@colyseus/command"; | ||
import { User } from "@port-of-mars/server/entity"; | ||
import { SoloGameRoom } from "@port-of-mars/server/rooms/sologame"; | ||
import { getServices } from "@port-of-mars/server/services"; | ||
import { getRandomIntInclusive } from "@port-of-mars/server/util"; | ||
|
||
const { sologame: service } = getServices(); | ||
|
||
abstract class Cmd<Payload> extends Command<SoloGameRoom, Payload> {} | ||
abstract class CmdWithoutPayload extends Cmd<Record<string, never>> {} | ||
|
||
export class InitGameCmd extends Cmd<{ user: User }> { | ||
execute({ user } = this.payload) { | ||
return [ | ||
new SetPlayerCmd().setPayload({ user }), | ||
new CreateDeckCmd(), | ||
new SetTreatmentParamsCmd().setPayload({ user }), | ||
new SetFirstRoundCmd(), | ||
]; | ||
} | ||
} | ||
|
||
export class SetPlayerCmd extends Cmd<{ user: User }> { | ||
execute({ user } = this.payload) { | ||
this.state.player.assign({ | ||
username: user.username, | ||
points: 0, | ||
}); | ||
} | ||
} | ||
|
||
export class CreateDeckCmd extends CmdWithoutPayload { | ||
async execute() { | ||
const cards = await service.drawEventCardDeck(); | ||
this.state.eventCardDeck.concat(_.shuffle(cards)); | ||
} | ||
} | ||
|
||
export class SetTreatmentParamsCmd extends Cmd<{ user: User }> { | ||
async execute({ user } = this.payload) { | ||
// pick a random (unseen, if user hasn't been through them all) treatment configuration | ||
const treatmentIds = await service.getUserRemainingTreatments(user.id); | ||
if (treatmentIds.length > 0) { | ||
const treatmentId = treatmentIds[Math.floor(Math.random() * treatmentIds.length)]; | ||
this.state.treatmentParams = await service.getTreatmentById(treatmentId); | ||
} else { | ||
this.state.treatmentParams = await service.getRandomTreatment(); | ||
} | ||
} | ||
} | ||
|
||
export class SetFirstRoundCmd extends CmdWithoutPayload { | ||
execute() { | ||
// FIXME: pull these from defaults somewhere | ||
this.state.maxRound = getRandomIntInclusive(6, 14); | ||
this.state.twoCardThreshold = getRandomIntInclusive(12, 20); | ||
// this formula may need tweaking | ||
const threeCardThresholdMax = Math.min(15, this.state.twoCardThreshold - 3); | ||
this.state.threeCardThreshold = getRandomIntInclusive(5, threeCardThresholdMax); | ||
this.state.round = 1; | ||
this.state.systemHealth = 25; | ||
this.state.timeRemaining = 30; | ||
this.state.player.resources = 7; | ||
} | ||
} | ||
|
||
export class ApplyCardCmd extends Cmd<{ playerSkipped: boolean }> { | ||
validate() { | ||
return ( | ||
this.state.activeRoundCardIndex >= 0 && | ||
this.state.activeRoundCardIndex < this.state.roundEventCards.length | ||
); | ||
} | ||
|
||
execute({ playerSkipped } = this.payload) { | ||
if (playerSkipped) { | ||
this.room.eventTimeout?.clear(); | ||
} | ||
this.state.player.points += this.state.activeRoundCard!.pointsDelta; | ||
this.state.player.resources += this.state.activeRoundCard!.resourcesDelta; | ||
this.state.systemHealth += this.state.activeRoundCard!.systemHealthDelta; | ||
if (this.state.systemHealth <= 0) { | ||
return new EndGameCmd().setPayload({ status: "defeat" }); | ||
} | ||
|
||
// if we still have cards left, prepare the next one | ||
if (this.state.activeRoundCardIndex < this.state.roundEventCards.length - 1) { | ||
this.state.activeRoundCardIndex += 1; | ||
return new StartEventTimerCmd(); | ||
} | ||
} | ||
} | ||
|
||
export class StartEventTimerCmd extends CmdWithoutPayload { | ||
execute() { | ||
this.room.eventTimeout = this.clock.setTimeout(() => { | ||
return new ApplyCardCmd().setPayload({ playerSkipped: false }); | ||
}, 10 * 1000); | ||
} | ||
} | ||
|
||
export class DrawCardsCmd extends CmdWithoutPayload { | ||
execute() { | ||
let drawCount = 1; | ||
if (this.state.systemHealth > this.state.twoCardThreshold) { | ||
drawCount = 1; | ||
} else if (this.state.systemHealth > this.state.threeCardThreshold) { | ||
drawCount = 2; | ||
} else { | ||
drawCount = 3; | ||
} | ||
this.state.roundEventCards.concat(this.state.eventCardDeck.splice(0, drawCount)); | ||
// draw 2 more if murphy's law is in play | ||
if (this.state.roundEventCards.some(card => card.isMurphysLaw)) { | ||
this.state.roundEventCards.concat(this.state.eventCardDeck.splice(0, 2)); | ||
} | ||
this.state.timeRemaining += 10 * drawCount; | ||
this.state.activeRoundCardIndex = 0; | ||
return new StartEventTimerCmd(); | ||
} | ||
} | ||
|
||
export class InvestCmd extends Cmd<{ | ||
systemHealthInvestment: number; | ||
pointsInvestment: number; | ||
}> { | ||
validate({ systemHealthInvestment, pointsInvestment } = this.payload) { | ||
return this.state.resources === systemHealthInvestment + pointsInvestment; | ||
} | ||
|
||
execute({ systemHealthInvestment, pointsInvestment } = this.payload) { | ||
this.state.systemHealth = Math.min(25, this.state.systemHealth + systemHealthInvestment); | ||
this.state.player.points += pointsInvestment; | ||
return new SetNextRoundCmd(); | ||
} | ||
} | ||
|
||
export class SetNextRoundCmd extends CmdWithoutPayload { | ||
execute() { | ||
// TODO: persist round | ||
this.state.round += 1; | ||
if (this.state.round > this.state.maxRound) { | ||
return new EndGameCmd().setPayload({ status: "victory" }); | ||
} | ||
if (this.state.systemHealth <= 5) { | ||
return new EndGameCmd().setPayload({ status: "defeat" }); | ||
} | ||
this.state.systemHealth -= 5; // pull these from defaults somewhere | ||
this.state.player.resources = 7; | ||
this.state.timeRemaining = 30; | ||
} | ||
} | ||
|
||
export class EndGameCmd extends Cmd<{ status: string }> { | ||
execute({ status } = this.payload) { | ||
// do any cleanup | ||
this.state.status = status; | ||
this.room.disconnect(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { Client, Delayed, Room } from "colyseus"; | ||
import { Dispatcher } from "@colyseus/command"; | ||
import * as http from "http"; | ||
import { SoloGameState } from "@port-of-mars/server/rooms/sologame/state"; | ||
import { settings } from "@port-of-mars/server/settings"; | ||
import { getServices } from "@port-of-mars/server/services"; | ||
import { ApplyCardCmd, InitGameCmd, InvestCmd, SetNextRoundCmd } from "./commands"; | ||
import { User } from "@port-of-mars/server/entity"; | ||
|
||
const logger = settings.logging.getLogger(__filename); | ||
|
||
export class SoloGameRoom extends Room<SoloGameState> { | ||
public static get NAME() { | ||
return "solo_game_room"; | ||
} | ||
autoDispose = true; | ||
maxClients = 1; | ||
patchRate = 1000 / 5; // sends state to client 5 times per second | ||
|
||
dispatcher = new Dispatcher(this); | ||
eventTimeout: Delayed | null = null; | ||
|
||
onCreate(options: any) { | ||
// do we need any options? most things are set up after onJoin is called | ||
logger.trace("SoloGameRoom '%s' created", this.roomId); | ||
this.setState(new SoloGameState()); | ||
this.setPrivate(true); | ||
this.registerAllHandlers(); | ||
this.clock.setInterval(() => { | ||
this.state.timeRemaining -= 1; | ||
if (this.state.timeRemaining <= 0) { | ||
this.dispatcher.dispatch(new SetNextRoundCmd()); | ||
} | ||
}, 1000); | ||
} | ||
|
||
async onAuth(client: Client, options: any, request?: http.IncomingMessage) { | ||
try { | ||
const user = await getServices().account.findUserById((request as any).session.passport.user); | ||
if (user.isBanned) { | ||
logger.info("Banned user %s attempted to join", user.username); | ||
return false; | ||
} | ||
return user; | ||
} catch (e) { | ||
logger.fatal("Unable to authenticate user: %s", e); | ||
} | ||
return false; | ||
} | ||
|
||
onJoin(client: Client, options: any, auth: User) { | ||
logger.trace("Client %s joined SoloGameRoom %s", auth.username, this.roomId); | ||
this.dispatcher.dispatch(new InitGameCmd()); | ||
} | ||
|
||
onDispose() { | ||
this.dispatcher.stop(); | ||
} | ||
|
||
registerAllHandlers() { | ||
this.onMessage("event-continue", (client, message) => { | ||
this.dispatcher.dispatch(new ApplyCardCmd().setPayload({ playerSkipped: true })); | ||
}); | ||
this.onMessage("invest", (client, message) => { | ||
this.dispatcher.dispatch( | ||
new InvestCmd().setPayload({ | ||
systemHealthInvestment: message.systemHealthInvestment, | ||
pointsInvestment: message.pointsInvestment, | ||
}) | ||
); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { Schema, ArraySchema, type } from "@colyseus/schema"; | ||
|
||
export class EventCard extends Schema { | ||
@type("string") codeName = ""; | ||
@type("string") displayName = ""; | ||
@type("string") description = ""; | ||
@type("int8") pointsDelta = 0; | ||
@type("int8") resourcesDelta = 0; | ||
@type("int8") systemHealthDelta = 0; | ||
|
||
get isMurphysLaw() { | ||
return this.codeName === "murphys-law"; | ||
} | ||
} | ||
|
||
export class Player extends Schema { | ||
@type("string") username = ""; | ||
@type("uint8") resources = 0; | ||
@type("uint8") points = 0; | ||
} | ||
|
||
export class TreatmentParams extends Schema { | ||
@type("boolean") isKnownNumberOfRounds = false; | ||
@type("boolean") isEventDeckKnown = false; | ||
@type("string") thresholdInformation: "unknown" | "range" | "known" = "unknown"; | ||
} | ||
|
||
export class SoloGameState extends Schema { | ||
// FIXME: these should come from defaults as well | ||
@type("string") status = "incomplete"; | ||
@type("int8") systemHealth = 25; | ||
@type("uint8") twoCardThreshold = 15; | ||
@type("uint8") threeCardThreshold = 8; | ||
@type("uint8") timeRemaining = 30; | ||
@type("uint8") round = 1; | ||
@type("uint8") maxRound = 0; | ||
@type(TreatmentParams) treatmentParams = new TreatmentParams(); | ||
@type(Player) player!: Player; | ||
@type([EventCard]) eventCardDeck = new ArraySchema<EventCard>(); | ||
@type([EventCard]) roundEventCards = new ArraySchema<EventCard>(); | ||
@type("uint8") activeRoundCardIndex = -1; | ||
|
||
get points() { | ||
if (this.player) return this.player.points; | ||
} | ||
|
||
get resources() { | ||
if (this.player) return this.player.resources; | ||
} | ||
|
||
get activeRoundCard() { | ||
if (this.activeRoundCardIndex >= 0 && this.activeRoundCardIndex < this.roundEventCards.length) { | ||
return this.roundEventCards[this.activeRoundCardIndex]; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { BaseService } from "@port-of-mars/server/services/db"; | ||
|
||
export class SoloGameService extends BaseService {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters