xiongzhu 2 éve
szülő
commit
5d822db6f9

+ 21 - 14
src/game/entities/charactors.entity.ts

@@ -1,29 +1,36 @@
 export class Charactor {
-    id: number
+    id?: number
 
-    gameId: number
+    gameId?: number
 
-    name: string
+    name?: string
 
-    age: number
+    age?: string
 
-    gender: string
+    gender?: string
 
-    occupation: string
+    occupation?: string
 
-    personality: string
+    personality?: string
 
-    background: string
+    background?: string
 
-    episode: string
+    episode?: string
 
-    properties: any
+    properties?: any
 
-    hp: number
+    hp?: number
 
-    joinAt: Date
+    joinAt?: Date
 
-    leaveAt: Date
+    survival?: number
+
+    dead?: boolean
+
+    deadAt?: Date
+
+    constructor(charactor: Partial<Charactor>) {
+        Object.assign(this, charactor)
+    }
 
-    survival: number
 }

+ 3 - 0
src/game/entities/game-round.entity.ts

@@ -39,4 +39,7 @@ export class GameRound {
 
     @Column({ type: 'longtext' })
     summary: string
+
+    @Column({ type: 'longtext', transformer: new JsonTransformer(), nullable: true })
+    modifyHp: object
 }

+ 6 - 3
src/game/game.controller.ts

@@ -47,8 +47,11 @@ export class GameController {
     }
 
     @Post('/:id/continue')
-    public async continue(@Param('id') id: string, @Body() body: { choice: string; genChoice: boolean }) {
-        return await this.gameService.continue(Number(id), body.choice, body.genChoice)
+    public async continue(
+        @Param('id') id: string,
+        @Body() body: { choice: string; genChoice: boolean; addCharactor: string }
+    ) {
+        return await this.gameService.continue(Number(id), body.choice, body.genChoice, body.addCharactor)
     }
 
     @Get('/:id/history')
@@ -63,6 +66,6 @@ export class GameController {
 
     @Post('/:id/addCharactor')
     public async addCharactor(@Param('id') id: string, @Body() body: { base: string }) {
-        return await this.gameService.addCharactor(Number(id), body.base)
+        return await this.gameService.createNewCharactor(Number(id), body.base)
     }
 }

+ 172 - 78
src/game/game.service.ts

@@ -27,6 +27,7 @@ import dedent from 'dedent'
 import { Serialized } from 'langchain/dist/load/serializable'
 import { HumanMessage, LLMResult, SystemMessage } from 'langchain/schema'
 import { CallbackManager } from 'langchain/callbacks'
+import { setTimeout } from 'timers/promises'
 
 @Injectable()
 export class GameService implements OnModuleInit {
@@ -173,6 +174,25 @@ export class GameService implements OnModuleInit {
         )
     }
 
+    formatSummary(history: GameRound[]) {
+        let lastSummarized = history.map((i) => !!i.summary).lastIndexOf(true)
+        let summary
+        if (lastSummarized < 0) {
+            summary = history.map((i) => this.formatDatetime(i.date, i.time) + '\n' + i.plot).join('\n\n')
+        } else {
+            summary = history[lastSummarized].summary
+            if (lastSummarized < history.length - 1) {
+                summary +=
+                    '\n' +
+                    history
+                        .slice(lastSummarized + 1)
+                        .map((i) => this.formatDatetime(i.date, i.time) + '\n' + i.plot)
+                        .join('\n\n')
+            }
+        }
+        return summary
+    }
+
     async startGame(id: number, date: Date) {
         const game = await this.gameRepository.findOneBy({ id })
         if (!game) {
@@ -195,6 +215,8 @@ export class GameService implements OnModuleInit {
             i.hp = 100
             i.joinAt = date
             i.survival = 0
+            i.dead = false
+
             return i
         })
 
@@ -207,7 +229,7 @@ export class GameService implements OnModuleInit {
 角色:
 {charactors}
 ------------
-情节不必太长,尽量控制在200字以内,但是要吸引人。
+情节不必太长,尽量控制在100字以内,但是要吸引人。
 {datetime},开始:`,
             inputVariables: ['background', 'charactors', 'datetime']
         })
@@ -229,7 +251,7 @@ export class GameService implements OnModuleInit {
         return round
     }
 
-    async continue(id: number, choice: string, createChoice: boolean) {
+    async continue(id: number, choice: string, createChoice: boolean, addCharactor) {
         const game = await this.gameRepository.findOneBy({ id })
         if (!game) {
             throw new NotFoundException(`game #${id} not found`)
@@ -254,26 +276,14 @@ export class GameService implements OnModuleInit {
         }
         gameRound.lastRound = lastRound.id
         gameRound.fistRound = false
-        gameRound.charactors = lastRound.charactors.filter((i) => i.hp > 0)
+        gameRound.charactors = this.calculateHp(lastRound.charactors, lastRound.modifyHp).filter(
+            (i) => i.dead === false
+        )
 
-        let lastSummarized = history.map((i) => !!i.summary).lastIndexOf(true)
-        let summary
-        if (lastSummarized < 0) {
-            summary = history.map((i) => this.formatDatetime(i.date, i.time) + '\n' + i.plot).join('\n\n')
-        } else {
-            summary = history[lastSummarized].summary
-            if (lastSummarized < history.length - 1) {
-                summary +=
-                    '\n' +
-                    history
-                        .slice(lastSummarized + 1)
-                        .map((i) => this.formatDatetime(i.date, i.time) + '\n' + i.plot)
-                        .join('\n\n')
-            }
-        }
+        let summary = this.formatSummary(history)
         let prompt = new PromptTemplate({
             template: dedent`你是一个富有想象力的写作助手,能够帮我创作引人入胜的谍战小说情节。请根据以下故事背景、角色,帮我继续创作这个故事。
-情节不必太长,尽量控制在200字以内,但是要吸引人
+情节不必太长,尽量控制在100字以内,但是要吸引人。剧情的时间跨度不超过一天。不要急于给出结局,我们会在后面的剧情中给出结局。
 ------------
 背景故事:
 {background}
@@ -283,18 +293,37 @@ export class GameService implements OnModuleInit {
 ------------
 历史情节概要:
 {summary}
-
 {choice}
 ------------
+{death}
+{newCharactor}
 {datetime},新的故事情节:`,
-            inputVariables: ['background', 'charactors', 'datetime', 'summary', 'choice']
+            inputVariables: ['background', 'charactors', 'datetime', 'summary', 'choice', 'death', 'newCharactor']
         })
+        const formatedCharactors = this.formatCharactors(gameRound.charactors)
+        const formatedDatatime = this.formatDatetime(gameRound.date, gameRound.time)
+        const deadCharactors = lastRound.charactors.filter((i) => i.hp <= 50)
+        let newCharactor = null
+        if (addCharactor) {
+            newCharactor = await this.createNewCharactor(id, addCharactor)
+            gameRound.charactors.push(
+                new Charactor({
+                    ...newCharactor,
+                    hp: 100,
+                    joinAt: gameRound.date
+                })
+            )
+        }
         const input = await prompt.format({
             background: game.background,
-            charactors: this.formatCharactors(gameRound.charactors),
-            datetime: this.formatDatetime(gameRound.date, gameRound.time),
+            charactors: formatedCharactors,
+            datetime: formatedDatatime,
             summary: summary,
-            choice: choice ? choice + '\n' : ''
+            choice: choice ? choice + '\n' : '',
+            death: deadCharactors.length === 0 ? '' : `请加入${deadCharactors.map((i) => i.name).join(',')}死亡的剧情`,
+            newCharactor: newCharactor
+                ? `加入新的角色:姓名:${newCharactor.name};性别:${newCharactor.gender};年龄:${newCharactor.age};职业:${newCharactor.occupation};性格:${newCharactor.personality};背景:${newCharactor.background};HP:100`
+                : ''
         })
 
         const response = await this.llm.call([new HumanMessage(input)])
@@ -302,44 +331,54 @@ export class GameService implements OnModuleInit {
         gameRound.plot = response.content
 
         if (createChoice) {
-            gameRound.choices = await this.createChoice(
-                summary + '\n\n' + this.formatDatetime(gameRound.date, gameRound.time) + '\n' + response.content
-            )
+            gameRound.choices = await this.createChoice(summary + '\n\n' + formatedDatatime + '\n' + response.content)
         } else {
             gameRound.choices = []
         }
-
+        gameRound.charactors.forEach((i) => {
+            if (i.hp === 0 && !i.dead) {
+                i.dead = true
+                i.deadAt = gameRound.date
+            }
+        })
         const round = await this.gameRoundRepository.save(gameRound)
 
         game.currentRound = round.id
         game.status = GameStatus.PLAYING
         await this.gameRepository.save(game)
 
-        await this.summarize(id)
+        const [modifyHp, newSummary] = await Promise.all([this.modifyHp(gameRound), this.summarize(gameRound)])
+        gameRound.summary = newSummary
+        gameRound.modifyHp = modifyHp
+        await this.gameRoundRepository.save(gameRound)
 
         return round
     }
 
     async createChoice(text) {
-        const parser = StructuredOutputParser.fromZodSchema(z.array(z.string().describe('剧情发展方向')))
-        const formatInstructions = parser.getFormatInstructions()
-        const prompt = new PromptTemplate({
-            template: dedent`你是一个富有想象力的写作助手,能够帮我创作引人入胜的谍战小说情节。
+        for (let i = 0; i < 5; i++) {
+            if (i > 0) await setTimeout(1000)
+            const parser = StructuredOutputParser.fromZodSchema(z.array(z.string().describe('剧情发展方向')))
+            const formatInstructions = parser.getFormatInstructions()
+            const prompt = new PromptTemplate({
+                template: dedent`你是一个富有想象力的写作助手,能够帮我创作引人入胜的谍战小说情节。
 {format_instructions}
 请根据以下故事背景、角色,帮我想象4个不同的剧情发展方向,每个选项不超过50字。
+剧情发展方向不一定是好的,但是要有趣。
 ------------
 {text}
 ------------
 请根据提供的JSON格式给出4个不同的剧情发展方向:`,
-            inputVariables: ['text'],
-            partialVariables: { format_instructions: formatInstructions }
-        })
-        const input = await prompt.format({
-            text
-        })
-        const response = await this.llm.call([new HumanMessage(input)])
-        const output = await parser.parse(response.content)
-        return output
+                inputVariables: ['text'],
+                partialVariables: { format_instructions: formatInstructions }
+            })
+            const input = await prompt.format({
+                text
+            })
+            const response = await this.llm.call([new HumanMessage(input)])
+            const output = await parser.parse(response.content)
+            return output
+        }
     }
 
     async revert(id: number) {
@@ -361,9 +400,8 @@ export class GameService implements OnModuleInit {
         }
     }
 
-    async summarize(id) {
-        const game = await this.findById(id)
-        const history: GameRound[] = await this.getHistory(game.currentRound, [])
+    async summarize(round: GameRound) {
+        const history: GameRound[] = await this.getHistory(round.id, [])
         let i = history.map((i) => !!i.summary).lastIndexOf(true)
         let input
         if (i < 0) {
@@ -401,47 +439,103 @@ export class GameService implements OnModuleInit {
         }
         const response = await this.llm.call([new HumanMessage(input)])
 
-        const lastRound = history[history.length - 1]
-        lastRound.summary = response.content
-        await this.gameRoundRepository.save(lastRound)
+        return response.content
     }
 
-    async addCharactor(gameId: number, base: string) {
-        const game = await this.gameRepository.findOneBy({ id: gameId })
-        if (!game) {
-            throw new NotFoundException(`game #${gameId} not found`)
-        }
-        const parser = StructuredOutputParser.fromZodSchema(
-            z.object({
-                name: z.string().describe('角色名称'),
-                gender: z.string().describe('性别'),
-                age: z.string().describe('年龄'),
-                occupation: z.string().describe('职业'),
-                personality: z.string().describe('性格'),
-                background: z.string().describe('背景')
-            })
-        )
-        const formatInstructions = parser.getFormatInstructions()
-        const prompt = new PromptTemplate({
-            template: dedent`{format_instructions}
+    async createNewCharactor(gameId: number, base: string) {
+        for (let i = 0; i < 5; i++) {
+            if (i > 0) await setTimeout(1000)
+            const game = await this.gameRepository.findOneBy({ id: gameId })
+            if (!game) {
+                throw new NotFoundException(`game #${gameId} not found`)
+            }
+            const parser = StructuredOutputParser.fromZodSchema(
+                z.object({
+                    name: z.string().describe('角色名称'),
+                    gender: z.string().describe('性别'),
+                    age: z.string().describe('年龄'),
+                    occupation: z.string().describe('职业'),
+                    personality: z.string().describe('性格'),
+                    background: z.string().describe('背景')
+                })
+            )
+            const formatInstructions = parser.getFormatInstructions()
+            const prompt = new PromptTemplate({
+                template: dedent`{format_instructions}
 请根据以下故事背景,和基本信息帮我想象一个角色:
 背景:{background}
 
 基本信息:{base}
 
 请根据提供的JSON格式给我这个角色的信息:`,
-            inputVariables: ['base', 'background'],
-            partialVariables: { format_instructions: formatInstructions }
-        })
-        const input = await prompt.format({
-            base,
-            background: game.background
+                inputVariables: ['base', 'background'],
+                partialVariables: { format_instructions: formatInstructions }
+            })
+            const input = await prompt.format({
+                base,
+                background: game.background
+            })
+            const response = await this.llm.call([
+                new SystemMessage('你是一个富有想象力的写作助手,你的任务是帮我想象一个小说里的角色。'),
+                new HumanMessage(input)
+            ])
+            const output = await parser.parse(response.content)
+            return output
+        }
+    }
+
+    async modifyHp(round: GameRound) {
+        for (let i = 0; i < 5; i++) {
+            if (i > 0) await setTimeout(1000)
+            const parser = StructuredOutputParser.fromZodSchema(
+                z.array(
+                    z.object({
+                        name: z.string().describe('角色名称'),
+                        modifyHp: z.string().describe('HP调整值')
+                    })
+                )
+            )
+            const formatInstructions = parser.getFormatInstructions()
+            const prompt = new PromptTemplate({
+                template: dedent`{format_instructions}
+
+角色信息:
+{charactors}
+历史剧情摘要:
+{summary}
+当前剧情:
+{text}
+
+HP最大值为100,最小值为0,如果HP值为0,角色将会死亡。
+不必给每个角色都调整HP值。
+请根据提供的JSON格式给我角色HP值的调整:`,
+                inputVariables: ['charactors', 'summary', 'text'],
+                partialVariables: { format_instructions: formatInstructions }
+            })
+            const formatedCharactors = this.formatCharactors(round.charactors)
+            const summary = this.formatSummary(await this.getHistory(round.id, []))
+            const input = await prompt.format({
+                charactors: formatedCharactors,
+                summary,
+                text: round.plot
+            })
+            const response = await this.llm.call([
+                new SystemMessage('你来扮演一个文字冒险游戏,你现在的任务是根据当前剧情随机调整角色的HP值'),
+                new HumanMessage(input)
+            ])
+            const output = await parser.parse(response.content)
+            return output
+        }
+    }
+    calculateHp(charactors: Charactor[], modifyHp) {
+        modifyHp = modifyHp || []
+        return charactors.map((i) => {
+            let c = { ...i }
+            const modify = modifyHp.find((j) => j.name === c.name)
+            if (modify) {
+                c.hp = Math.max(0, Math.min(100, c.hp + parseInt(modify.modifyHp)))
+            }
+            return c
         })
-        const response = await this.llm.call([
-            new SystemMessage('你是一个富有想象力的写作助手,你的任务是帮我想象一个小说里的角色。'),
-            new HumanMessage(input)
-        ])
-        const output = await parser.parse(response.content)
-        return output
     }
 }