|
|
@@ -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
|
|
|
}
|
|
|
}
|