diff --git a/packages/constants/src/quest/condition.ts b/packages/constants/src/quest/condition.ts index cbeebf8..e9f4519 100644 --- a/packages/constants/src/quest/condition.ts +++ b/packages/constants/src/quest/condition.ts @@ -2,7 +2,7 @@ import { z } from 'zod' import { fileConditionSchema } from './file-condition.js' import { toArray } from '../utils.js' -export const conditionSchema = z.object({ +const conditionSchemaBase = z.object({ command: z.preprocess(toArray, z.array(z.string()).optional()), output: z.preprocess(toArray, z.array(z.string()).optional()), error: z.preprocess(toArray, z.array(z.string()).optional()), @@ -10,4 +10,12 @@ export const conditionSchema = z.object({ files: z.array(fileConditionSchema).optional(), }) -export type Condition = z.infer +export type Condition = z.infer & { + not?: Condition + or?: Condition[] +} + +export const conditionSchema = conditionSchemaBase.extend({ + not: z.lazy(() => conditionSchema).optional(), + or: z.lazy(() => z.array(conditionSchema)).optional(), +}) as z.ZodType diff --git a/packages/constants/src/quest/exception.ts b/packages/constants/src/quest/exception.ts new file mode 100644 index 0000000..4014c6f --- /dev/null +++ b/packages/constants/src/quest/exception.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' +import { conditionSchema } from './condition.js' +import { responseSchema } from './response.js' + +export const exceptionSchema = z.object({ + condition: conditionSchema, + responses: z.array(responseSchema), +}) + +export type Exception = z.infer + +export const stageExceptionSchema = exceptionSchema.extend({ + catchAll: z.boolean().default(false), +}) + +export type StageException = z.infer + +export const globalExceptionSchema = exceptionSchema.extend({ + requirements: z.array(z.string()), +}) + +export type GlobalException = z.infer diff --git a/packages/constants/src/quest/global-exception.ts b/packages/constants/src/quest/global-exception.ts deleted file mode 100644 index 92d1038..0000000 --- a/packages/constants/src/quest/global-exception.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod' -import { conditionSchema } from './condition.js' -import { responseSchema } from './response.js' - -export const globalExceptionSchema = z.object({ - condition: conditionSchema, - responses: z.array(responseSchema), - requirements: z.array(z.string()), -}) - -export type GlobalException = z.infer diff --git a/packages/constants/src/quest/index.ts b/packages/constants/src/quest/index.ts index 0dc6cf7..6ab676a 100644 --- a/packages/constants/src/quest/index.ts +++ b/packages/constants/src/quest/index.ts @@ -6,6 +6,6 @@ export * from './response.js' export * from './command-responder.js' export * from './stage-exception.js' export * from './stage.js' -export * from './global-exception.js' +export * from './exception.js' export * from './quest.js' export * from './quest-detail-response.js' diff --git a/packages/constants/src/quest/quest-detail-response.ts b/packages/constants/src/quest/quest-detail-response.ts index ddc5e1c..44ceabd 100644 --- a/packages/constants/src/quest/quest-detail-response.ts +++ b/packages/constants/src/quest/quest-detail-response.ts @@ -1,7 +1,7 @@ import { z } from 'zod' export const questDetailResponseSchema = z.object({ - _id: z.string(), + id: z.string(), title: z.string(), instruction: z.string(), requirements: z.array(z.string()), diff --git a/packages/constants/src/quest/quest.ts b/packages/constants/src/quest/quest.ts index e8d0440..5a5c899 100644 --- a/packages/constants/src/quest/quest.ts +++ b/packages/constants/src/quest/quest.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import { stageSchema } from './stage.js' -import { globalExceptionSchema } from './global-exception.js' +import { globalExceptionSchema } from './exception.js' import { requirementSchema } from './requirement.js' export const questSchema = z.object({ diff --git a/packages/constants/src/quest/stage-exception.ts b/packages/constants/src/quest/stage-exception.ts deleted file mode 100644 index fd0415d..0000000 --- a/packages/constants/src/quest/stage-exception.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from 'zod' -import { conditionSchema } from './condition.js' -import { responseSchema } from './response.js' - -export const stageExceptionSchema = z.object({ - condition: conditionSchema, - responses: z.array(responseSchema), -}) - -export type StageException = z.infer diff --git a/packages/constants/src/quest/stage.ts b/packages/constants/src/quest/stage.ts index 616e37e..643ce2e 100644 --- a/packages/constants/src/quest/stage.ts +++ b/packages/constants/src/quest/stage.ts @@ -1,5 +1,5 @@ import { z } from 'zod' -import { stageExceptionSchema } from './stage-exception.js' +import { stageExceptionSchema } from './exception.js' import { conditionSchema } from './condition.js' import { responseSchema } from './response.js' import { hintSchema } from './hint.js' diff --git a/packages/models/src/commandResponder.ts b/packages/models/src/commandResponder.ts deleted file mode 100644 index adddcab..0000000 --- a/packages/models/src/commandResponder.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Schema } from 'mongoose' -import { conditionSchema, ICondition } from './condition.js' -import { responseSchema, IResponse } from './response.js' -import { hintSchema, IHint } from './hint.js' - -export interface ICommandResponder extends IHint { - condition?: ICondition - responses: IResponse[] -} - -export const commandResponderSchema = new Schema({ - condition: { - type: conditionSchema, - required: false, - }, - responses: { - type: [responseSchema], - required: true, - }, -}) - -commandResponderSchema.add(hintSchema) diff --git a/packages/models/src/condition.ts b/packages/models/src/condition.ts deleted file mode 100644 index 0b4daa4..0000000 --- a/packages/models/src/condition.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Schema } from 'mongoose' - -export interface IFileCondition { - path: string - type: string - exists: boolean -} - -export interface ICondition { - command?: string[] - output?: string[] - error?: string[] - pwd?: string[] - files?: IFileCondition[] - or?: ICondition[] - not?: ICondition -} - -export const fileConditionSchema = new Schema({ - path: { - type: String, - required: true, - }, - type: String, - exists: { - type: Boolean, - default: true, - }, -}) - -export const conditionSchema = new Schema({ - command: [String], - output: [String], - error: [String], - pwd: [String], - files: [fileConditionSchema], -}) - -conditionSchema.add({ - or: [conditionSchema], - not: conditionSchema, -}) diff --git a/packages/models/src/exception.ts b/packages/models/src/exception.ts deleted file mode 100644 index b883489..0000000 --- a/packages/models/src/exception.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Schema } from 'mongoose' -import { - commandResponderSchema, - ICommandResponder, -} from './commandResponder.js' - -export interface IException extends ICommandResponder {} - -export const exceptionSchema = new Schema({}) - -exceptionSchema.add(commandResponderSchema) diff --git a/packages/models/src/globalException.ts b/packages/models/src/globalException.ts deleted file mode 100644 index a307c56..0000000 --- a/packages/models/src/globalException.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Schema } from 'mongoose' -import { exceptionSchema, IException } from './exception.js' -import { requirementSchema, IRequirement } from './requirement.js' - -export interface IGlobalException extends IException, IRequirement {} - -export const globalExceptionSchema = new Schema({}) - -globalExceptionSchema.add(exceptionSchema) -globalExceptionSchema.add(requirementSchema) diff --git a/packages/models/src/hint.ts b/packages/models/src/hint.ts deleted file mode 100644 index 867c333..0000000 --- a/packages/models/src/hint.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Schema } from 'mongoose' - -export interface IHint { - hints: string[] -} - -export const hintSchema = new Schema({ - hints: { - type: [String], - required: true, - }, -}) diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 014f076..2427bde 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -1,15 +1,9 @@ import mongoose from 'mongoose' export { User, IUser } from './user.js' -export { Quest, IQuest } from './quest.js' -export { Session, ISession, ITask } from './session.js' +export { Session, ISession } from './session.js' export { Command, ICommand } from './command.js' export { UserProfile, IUserProfile, IProgress } from './userProfile.js' -export { IFileCondition, ICondition } from './condition.js' -export { IStage } from './stage.js' -export { IRequirement } from './requirement.js' -export { IResponse } from './response.js' -export { IException } from './exception.js' export default async function connectDB(uri: string) { const db = await mongoose.connect(uri) diff --git a/packages/models/src/quest.ts b/packages/models/src/quest.ts deleted file mode 100644 index 5a9607c..0000000 --- a/packages/models/src/quest.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { model, Schema } from 'mongoose' -import { IStage, stageSchema } from './stage.js' -import { globalExceptionSchema, IGlobalException } from './globalException.js' -import { IRequirement } from './requirement.js' - -export interface IQuest extends IRequirement { - _id: string - title: string - image: string - instruction: string - stages: IStage[] - exceptions: IGlobalException[] -} - -export const Quest = model( - 'Quest', - new Schema({ - _id: { - type: String, - required: true, - }, - title: { - type: String, - required: true, - }, - image: { - type: String, - required: true, - }, - requirements: { - type: [ - { - type: String, - ref: 'Quest', - }, - ], - required: true, - }, - instruction: { - type: String, - required: true, - }, - stages: { - type: [stageSchema], - required: true, - }, - exceptions: { - type: [globalExceptionSchema], - required: true, - }, - }) -) diff --git a/packages/models/src/requirement.ts b/packages/models/src/requirement.ts deleted file mode 100644 index 7c95b3a..0000000 --- a/packages/models/src/requirement.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Schema } from 'mongoose' - -export interface IRequirement { - requirements: string[] -} - -export const requirementSchema = new Schema({ - requirements: [ - { - type: String, - required: true, - }, - ], -}) diff --git a/packages/models/src/response.ts b/packages/models/src/response.ts deleted file mode 100644 index 44fd231..0000000 --- a/packages/models/src/response.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Schema } from 'mongoose' - -export interface IResponse { - type: 'narrative' | 'dialogue' - content: string[] - speaker?: string - color?: string -} - -export const responseSchema = new Schema({ - type: { - type: String, - enum: ['narrative', 'dialogue'], - default: 'dialogue', - required: true, - }, - content: { - type: [String], - required: true, - }, - speaker: { type: String, default: 'Ada' }, - color: String, -}) diff --git a/packages/models/src/session.ts b/packages/models/src/session.ts index 49185fd..e014cd2 100644 --- a/packages/models/src/session.ts +++ b/packages/models/src/session.ts @@ -1,7 +1,5 @@ import { model, Schema, Types } from 'mongoose' -import { IResponse, responseSchema } from './response.js' - export interface INode { path: string type: string @@ -22,27 +20,6 @@ nodeSchema.add({ children: [nodeSchema], }) -export interface ITask { - id: string - name: string - completed: boolean -} - -const taskSchema = new Schema({ - id: { - type: String, - required: true, - }, - name: { - type: String, - required: true, - }, - completed: { - type: Boolean, - default: false, - }, -}) - export interface ISession { _id: Types.ObjectId user: Types.ObjectId @@ -53,9 +30,7 @@ export interface ISession { updatedAt: Date finishedAt?: Date lastActivityAt: Date - tasks: ITask[] - hints: string[][] - responses: IResponse[][] + stages: string[] graph: INode } @@ -86,13 +61,7 @@ export const Session = model( default: Date.now, required: true, }, - tasks: [taskSchema], - hints: [[String]], - responses: { - type: [[responseSchema]], - required: true, - default: [], - }, + stages: [String], graph: { type: nodeSchema, default: { diff --git a/packages/models/src/stage.ts b/packages/models/src/stage.ts deleted file mode 100644 index 05ac5ff..0000000 --- a/packages/models/src/stage.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Schema } from 'mongoose' -import { - commandResponderSchema, - ICommandResponder, -} from './commandResponder.js' -import { stageExceptionSchema, IStageException } from './stageException.js' -import { requirementSchema, IRequirement } from './requirement.js' - -export interface IStage extends IRequirement, ICommandResponder { - id: string - task: string - exceptions: IStageException[] -} - -export const stageSchema = new Schema({ - id: { - type: String, - required: true, - }, - task: { - type: String, - required: true, - }, - exceptions: { - type: [stageExceptionSchema], - required: true, - }, -}) - -stageSchema.add(commandResponderSchema) -stageSchema.add(requirementSchema) diff --git a/packages/models/src/stageException.ts b/packages/models/src/stageException.ts deleted file mode 100644 index 8d886f2..0000000 --- a/packages/models/src/stageException.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Schema } from 'mongoose' -import { exceptionSchema, IException } from './exception.js' - -export interface IStageException extends IException { - catchAll: boolean -} - -export const stageExceptionSchema = new Schema({ - catchAll: { - type: Boolean, - required: true, - default: false, - }, -}) - -stageExceptionSchema.add(exceptionSchema) diff --git a/server/src/api/controllers/commandController.ts b/server/src/api/controllers/commandController.ts index f72761b..b3a7314 100644 --- a/server/src/api/controllers/commandController.ts +++ b/server/src/api/controllers/commandController.ts @@ -1,10 +1,11 @@ -import { Command, Quest, Session } from '@linux-odyssey/models' +import { Command, Session } from '@linux-odyssey/models' import { matchedData } from 'express-validator' import type { Request, Response } from 'express' import CommandHandler from '../../game/commandHandler.js' import { pushToSession } from '../socket.js' import { finishSession } from '../../models/sessionManager.js' import { asyncHandler } from '../../middleware/error.js' +import { questManager } from '../../models/quest.js' export const newCommand = asyncHandler(async (req: Request, res: Response) => { const { command, pwd, output, error, params } = matchedData(req) @@ -37,7 +38,7 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => { await c.save() - const quest = await Quest.findById(session.quest) + const quest = await questManager.get(session.quest) if (!quest) { res.status(404).json({ message: 'quest not found' }) return diff --git a/server/src/api/controllers/sessionController.ts b/server/src/api/controllers/sessionController.ts index 4ec6e9d..3eb1ed1 100644 --- a/server/src/api/controllers/sessionController.ts +++ b/server/src/api/controllers/sessionController.ts @@ -22,10 +22,7 @@ function sessionSummary(session: ISession) { function sessionDetail(session: ISession) { return { ...sessionSummary(session), - hints: session.hints, - tasks: session.tasks, - responses: session.responses, - graph: session.graph, + stages: session.stages, } } diff --git a/server/src/game/commandHandler.ts b/server/src/game/commandHandler.ts index e7b3dc0..faec149 100644 --- a/server/src/game/commandHandler.ts +++ b/server/src/game/commandHandler.ts @@ -2,14 +2,13 @@ import minimist from 'minimist' import { HydratedDocument } from 'mongoose' import { FileGraph } from '@linux-odyssey/file-graph' import { - ICommand, - ICondition, - IFileCondition, - IQuest, - ISession, - IStage, - IException, -} from '@linux-odyssey/models' + Condition, + FileCondition, + Quest, + Stage, + Exception, +} from '@linux-odyssey/constants' +import { ICommand, ISession } from '@linux-odyssey/models' import { pushToSession } from '../api/socket.js' import SessionHandler, { ExecuteResult } from './sessionHandler.js' import { checkFile } from '../containers/cli.js' @@ -31,7 +30,7 @@ export default class CommandHandler extends SessionHandler { constructor( session: HydratedDocument, - quest: HydratedDocument, + quest: Quest, commandInput: ICommand, params: Record ) { @@ -82,7 +81,7 @@ export default class CommandHandler extends SessionHandler { } } - async isMatch(condition: ICondition): Promise { + async isMatch(condition: Condition): Promise { return ( this.checkKeys(condition) && (await this.checkFiles(condition.files)) && @@ -91,7 +90,7 @@ export default class CommandHandler extends SessionHandler { ) } - checkKeys(condition: ICondition) { + checkKeys(condition: Condition) { return ( checkMatch(condition.command, this.commandInput.command) && checkMatch(condition.output, this.commandInput.output) && @@ -100,7 +99,7 @@ export default class CommandHandler extends SessionHandler { ) } - async checkFiles(files?: IFileCondition[]): Promise { + async checkFiles(files?: FileCondition[]): Promise { if (!files || files.length === 0) { return true } @@ -117,14 +116,14 @@ export default class CommandHandler extends SessionHandler { } } - async checkNot(condition?: ICondition): Promise { + async checkNot(condition?: Condition): Promise { if (!condition) { return true } return !(await this.isMatch(condition)) } - async checkOr(conditions?: ICondition[]): Promise { + async checkOr(conditions?: Condition[]): Promise { if (!conditions || conditions.length === 0) { return true } @@ -132,7 +131,7 @@ export default class CommandHandler extends SessionHandler { return matches.some((m) => m === true) } - async checkException(stage: IStage): Promise { + async checkException(stage: Stage): Promise { const exceptions = stage.exceptions || [] for (const exception of exceptions) { // eslint-disable-next-line no-await-in-loop @@ -200,7 +199,7 @@ export default class CommandHandler extends SessionHandler { logger.error('CommandHandler error', { error: (err as Error).message, session: this.session._id, - quest: this.quest._id, + quest: this.quest.id, command: this.commandInput, }) return null diff --git a/server/src/game/sessionHandler.ts b/server/src/game/sessionHandler.ts index c061d57..5a26798 100644 --- a/server/src/game/sessionHandler.ts +++ b/server/src/game/sessionHandler.ts @@ -1,18 +1,16 @@ import { HydratedDocument } from 'mongoose' import type { - IException, - IQuest, - IRequirement, - IResponse, - ISession, - IStage, - ITask, -} from '@linux-odyssey/models' + Quest, + Response, + Requirement, + Stage, + Task, + Exception, +} from '@linux-odyssey/constants' +import { ISession } from '@linux-odyssey/models' export interface ExecuteResult { - responses: IResponse[] - hints: string[] - tasks?: ITask[] + responses: Response[] status: string stage?: string end?: boolean @@ -20,12 +18,9 @@ export interface ExecuteResult { export default class SessionHandler { session: HydratedDocument - quest: HydratedDocument + quest: Quest - constructor( - session: HydratedDocument, - quest: HydratedDocument - ) { + constructor(session: HydratedDocument, quest: Quest) { this.session = session this.quest = quest } @@ -34,7 +29,7 @@ export default class SessionHandler { return this.session } - getTask(stage: Partial): ITask | undefined { + getTask(stage: Partial): Task | undefined { return this.session.tasks.find((t) => t.id === stage.id) } @@ -43,7 +38,9 @@ export default class SessionHandler { return task ? task.completed : false } - getUnlockedComponents(components: T[]): T[] { + getUnlockedComponents( + components: T[] + ): T[] { return components.filter((component) => component.requirements.every((r: string) => this.isCompleted({ id: r })) ) @@ -67,39 +64,30 @@ export default class SessionHandler { this.session.tasks.push(...newTasks) } - execute(stage: IStage): ExecuteResult { + execute(stage: Stage): ExecuteResult { this.session.tasks .filter((t) => t.id === stage.id) .forEach((t) => { t.completed = true }) - if (stage.hints && stage.hints.length > 0) - this.session.hints.push(stage.hints) - this.addNewTasks() - this.session.responses.push(stage.responses) - if (stage.id === 'END') { this.session.status = 'finished' } return { responses: stage.responses, - hints: stage.hints, - tasks: this.session.tasks, status: this.session.status, } } - executeException(exception: IException): ExecuteResult { - const { hints, responses } = exception - if (hints && hints.length > 0) this.session.hints.push(hints) + executeException(exception: Exception): ExecuteResult { + const { responses } = exception return { responses, - hints, status: this.session.status, } } diff --git a/server/src/routers/questRouter.ts b/server/src/routers/questRouter.ts index 0c39a1b..91c1b73 100644 --- a/server/src/routers/questRouter.ts +++ b/server/src/routers/questRouter.ts @@ -1,14 +1,14 @@ import { TRPCError } from '@trpc/server' -import { Quest } from '@linux-odyssey/models' import { QuestDetailResponse } from '@linux-odyssey/constants' import { z } from 'zod' import { router, publicProcedure } from '../trpc.js' +import { questManager } from '../models/quest.js' export const questRouter = router({ getQuests: publicProcedure.query(async () => { - const quests = await Quest.find({}) - return quests.map(({ title, _id, requirements }) => ({ - _id, + const quests = await questManager.getAll() + return quests.map(({ title, id, requirements }) => ({ + id, title, requirements, })) @@ -16,15 +16,15 @@ export const questRouter = router({ getQuestDetail: publicProcedure .input(z.string()) .query(async (opts): Promise => { - const id = opts.input - const quest = await Quest.findById(id) + const questId = opts.input + const quest = await questManager.get(questId) if (!quest) { throw new TRPCError({ code: 'NOT_FOUND', message: 'Quest not found.', }) } - const { _id, title, instruction, requirements } = quest - return { _id, title, instruction, requirements } + const { id, title, instruction, requirements } = quest + return { id, title, instruction, requirements } }), }) diff --git a/server/src/routers/sessionRouter.ts b/server/src/routers/sessionRouter.ts index 3fee166..fe93f03 100644 --- a/server/src/routers/sessionRouter.ts +++ b/server/src/routers/sessionRouter.ts @@ -34,10 +34,7 @@ export const sessionRouter = router({ status: session.status, createdAt: session.createdAt, lastActivityAt: session.lastActivityAt, - hints: session.hints, - tasks: session.tasks, - responses: session.responses, - graph: session.graph, + stages: session.stages, } } throw new TRPCError({