feat: add quest class and session getActiveStages

This commit is contained in:
Justin Lin committed 2025-04-18 14:54:10 +08:00
1 parent 471b781d85
commit e622011b21
13 files changed
+127 -49

No files matched your search

+1 -2
View File
@@ -49,7 +49,7 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
'warn',
{
args: 'after-used',
argsIgnorePattern: '^_',
@@ -73,7 +73,6 @@ module.exports = {
},
],
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'no-empty-function': 'off',
+16
View File
@@ -0,0 +1,16 @@
import { IQuest, IStage } from './schema'
import { Stage } from './Stage'
export class Quest {
private stages: Stage[]
private quest: IQuest
constructor(quest: IQuest) {
this.quest = quest
this.stages = quest.stages.map((stage: IStage) => new Stage(stage))
}
getActiveStages(completed: string[]) {
return this.stages.filter((stage) => stage.active(completed))
}
}
+9 -1
View File
@@ -1,10 +1,14 @@
import { Quest } from './Quest'
import { IQuest } from './schema'
import { ISession } from './types'
export class Session implements ISession {
private completed: string[] = []
private quest: Quest
constructor({ completedStages }: ISession) {
constructor({ completedStages }: ISession, quest: IQuest) {
this.completed = completedStages
this.quest = new Quest(quest)
}
get completedStages() {
@@ -14,4 +18,8 @@ export class Session implements ISession {
complete(stageId: string) {
this.completed.push(stageId)
}
getActiveStages(): string[] {
return this.quest.getActiveStages(this.completed).map((stage) => stage.id)
}
}
+7 -9
View File
@@ -1,4 +1,4 @@
import { IStage, ICommand, IFileExistenceChecker, ISession } from './types'
import { IStage, ICommand, IFileExistenceChecker } from './types'
import { Condition } from './condition/Condition'
export class Stage {
@@ -21,17 +21,15 @@ export class Stage {
return this.condition.match(command) && this.condition.check(checker)
}
private checkRequirements(session: ISession): boolean {
return this.requirements.every((req) =>
session.completedStages.includes(req)
)
private checkRequirements(completed: string[]): boolean {
return this.requirements.every((req) => completed.includes(req))
}
private finished(session: ISession): boolean {
return session.completedStages.includes(this.id)
private finished(completed: string[]): boolean {
return completed.includes(this.id)
}
active(session: ISession): boolean {
return !this.finished(session) && this.checkRequirements(session)
active(completed: string[]): boolean {
return !this.finished(completed) && this.checkRequirements(completed)
}
}
+1 -1
View File
@@ -1,9 +1,9 @@
import {
ICommand,
ICondition,
IFileExistenceChecker,
FileExistenceInput,
} from '../types.js'
import { ICondition } from '../schema/condition.js'
import { PwdMatcher } from './PwdMatcher.js'
import { CommandMatcher } from './CommandMatcher.js'
import { ErrorMatcher } from './ErrorMatcher.js'
+4 -3
View File
@@ -1,8 +1,9 @@
import { z } from 'zod'
import { FileType } from '../types.js'
export const fileInputSchema = z.object({
path: z.string(),
type: z.enum(['file', 'folder']),
type: z.nativeEnum(FileType),
})
export type IFileInput = z.infer<typeof fileInputSchema>
@@ -28,6 +29,6 @@ export type ICondition = z.infer<typeof baseConditionSchema> & {
export const conditionSchema: z.ZodType<ICondition> =
baseConditionSchema.extend({
or: z.lazy(() => z.array(conditionSchema)).optional(),
not: z.lazy(() => conditionSchema).optional(),
or: z.lazy(() => z.array(conditionSchema).optional()),
not: z.lazy(() => conditionSchema.optional()),
})
+3 -5
View File
@@ -1,7 +1,5 @@
export { conditionSchema, ICondition } from './condition.js'
export { stageSchema, IStage } from './stage.js'
export {
requirementsSchema,
IRequirements,
responseSchema,
} from './requirements.js'
export { requirementsSchema, IRequirements } from './requirements.js'
export { responseSchema, IResponse } from './response.js'
export { questSchema, IQuest } from './quest.js'
+17
View File
@@ -0,0 +1,17 @@
import { z } from 'zod'
import { stageSchema } from './stage.js'
import { requirementsSchema } from './requirements.js'
// import { globalExceptionSchema } from './exception.js'
// import { requirementSchema } from './requirement.js'
export const questSchema = z.object({
id: z.string(),
title: z.string(),
image: z.string(),
instruction: z.string(),
requirements: requirementsSchema,
stages: z.array(stageSchema),
// exceptions: z.array(globalExceptionSchema).default([]),
})
export type IQuest = z.infer<typeof questSchema>
+1 -8
View File
@@ -1,12 +1,5 @@
import { z } from 'zod'
export const requirementsSchema = z.object({
requirements: z.array(z.string()),
})
export const requirementsSchema = z.array(z.string())
export type IRequirements = z.infer<typeof requirementsSchema>
export const responseSchema = z.object({
type: z.enum(['narrative', 'dialogue']),
content: z.string(),
})
+8
View File
@@ -0,0 +1,8 @@
import { z } from 'zod'
export const responseSchema = z.object({
type: z.enum(['narrative', 'dialogue']),
content: z.string(),
})
export type IResponse = z.infer<typeof responseSchema>
+2 -1
View File
@@ -1,5 +1,6 @@
import { z } from 'zod'
import { conditionSchema, responseSchema } from './index.js'
import { conditionSchema } from './condition.js'
import { responseSchema } from './response.js'
export const stageSchema = z.object({
id: z.string().regex(/^[a-zA-Z0-9_-]+$/),
+58
View File
@@ -0,0 +1,58 @@
import { expect, describe, it } from '@jest/globals'
import { IQuest } from '../src/schema'
import { Session } from '../src/Session'
const quest: IQuest = {
id: 'quest1',
title: 'Quest 1',
image: 'image1',
instruction: 'Instruction 1',
requirements: [],
stages: [
{
id: 'stage1',
requirements: [],
condition: {},
response: { type: 'narrative', content: 'Narrative 1' },
},
{
id: 'stage2',
requirements: ['stage1'],
condition: {},
response: { type: 'narrative', content: 'Narrative 2' },
},
{
id: 'stage3',
requirements: ['stage1'],
condition: {},
response: { type: 'narrative', content: 'Narrative 3' },
},
{
id: 'stage4',
requirements: ['stage2', 'stage3'],
condition: {},
response: { type: 'narrative', content: 'Narrative 4' },
},
],
}
describe('Session', () => {
it('should return active stages', () => {
const session = new Session(
{
completedStages: [],
},
quest
)
expect(session.getActiveStages()).toEqual(['stage1'])
session.complete('stage1')
expect(session.getActiveStages()).toEqual(['stage2', 'stage3'])
session.complete('stage2')
expect(session.getActiveStages()).toEqual(['stage3'])
session.complete('stage3')
expect(session.getActiveStages()).toEqual(['stage4'])
})
})
-19
View File
@@ -2,7 +2,6 @@ import { test, expect } from '@jest/globals'
import { Stage } from '../src/Stage'
import { FileType } from '../src/types'
import { MockFileChecker } from './asyncCondition.test'
import { Session } from '../src/Session'
const checker = new MockFileChecker()
@@ -68,21 +67,3 @@ test('check stage with async condition falsy', async () => {
})
expect(await stage.satisfies({}, checker)).toBe(false)
})
test('stage active checks', () => {
const session = new Session({
completedStages: ['stage1'],
})
const stage3 = new Stage({
id: 'stage3',
requirements: ['stage1', 'stage2'],
condition: {},
})
expect(stage3.active(session)).toBe(false)
session.complete('stage2')
expect(stage3.active(session)).toBe(true)
session.complete('stage3')
expect(stage3.active(session)).toBe(false)
})