From 494ed3f3f15578c183d1151128c4e56dddeed9f0 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Fri, 15 Mar 2024 17:35:32 +0000 Subject: [PATCH] =?UTF-8?q?Command=20=EC=B4=88=EC=95=88=20-=20PHP=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=20=EA=B1=B0=EC=9D=98=20=EA=B7=B8=EB=8C=80?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EC=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- @sammo/game_logic/package.json | 3 +- @sammo/game_logic/src/Command/index.ts | 252 ++++++++++++++++++ @sammo/game_logic/src/Constraint/index.ts | 50 ++++ @sammo/game_logic/src/Entity/GeneralEntity.ts | 3 +- pnpm-lock.yaml | 3 + 5 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 @sammo/game_logic/src/Command/index.ts create mode 100644 @sammo/game_logic/src/Constraint/index.ts diff --git a/@sammo/game_logic/package.json b/@sammo/game_logic/package.json index 31f9f22..89e08dd 100644 --- a/@sammo/game_logic/package.json +++ b/@sammo/game_logic/package.json @@ -17,7 +17,8 @@ "@strpc/express": "workspace:^", "date-fns": "^3.3.1", "dotenv": "^16.4.5", - "mongoose": "^8.2.1" + "mongoose": "^8.2.1", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20.11.24" diff --git a/@sammo/game_logic/src/Command/index.ts b/@sammo/game_logic/src/Command/index.ts new file mode 100644 index 0000000..4ba1985 --- /dev/null +++ b/@sammo/game_logic/src/Command/index.ts @@ -0,0 +1,252 @@ +import type { City } from "@/City.js"; +import { testContraints, type IConstraint, type TestInput } from "@/Constraint/index.js"; +import type { GameEnv } from "@/GameEnv.js"; +import type { General } from "@/General.js"; +import type { Nation } from "@/Nation.js"; +import type { IntYearMonth, ReservedTurn } from "@/defs.js"; +import type { RandUtil } from "@sammo/crypto"; +import { InvalidArgument, must } from "@sammo/util"; +import zod, { z } from "zod"; + +export const enum CommandType { + General = 0, + Nation = 1, +}; + +export const enum CompensateType { + Neutral = 0, + Positive = 1, + Negative = -1, +} + +export abstract class BaseCommand>{ + abstract readonly actionName: string; + abstract readonly className: string; + abstract readonly type: CommandType; + + public abstract readonly compensateType: CompensateType; + + readonly isLazyCalcReqTurn: boolean = false; + + protected cachedPermissionToReserve = false; + protected cachedMinConditionMet = false; + protected cachedFullConditionMet = false; + + protected reasonNotFullConditionMet: string | undefined = undefined; + protected reasonNotMinConditionMet: string | undefined = undefined; + protected reasonNotPermissionToReserve: string | undefined = undefined; + + protected _destNation?: Nation; + protected _destCity?: City; + protected _destGeneral?: General; + + public get destNation(): Readonly | undefined { + return this._destNation; + } + public get destCity(): Readonly | undefined { + return this._destCity; + } + public get destGeneral(): Readonly | undefined { + return this._destGeneral; + } + + protected abstract alternative: BaseCommand | undefined; + + protected abstract permissionConstraints: IConstraint[] | undefined; + protected abstract minConditionConstraints: IConstraint[] | undefined; + protected abstract fullConditionConstraints: IConstraint[] | undefined; + protected abstract errorInvoker: string | undefined; + + public constructor( + public readonly general: General, + public readonly env: GameEnv, + public readonly arg: ArgType + ) { + this.init(); + } + + protected abstract init(): void; + protected initWithArg(): void { + if (this.arg === undefined) { + return; + } + throw new InvalidArgument("initWithArg must be overridden if arg is not undefined"); + } + + public get commandBrief(): string { + return this.actionName; + } + + + //TODO: ExecuteKey의 타입 + abstract getNextExecuteKey(): string; + abstract getNextAvailableTurn(): IntYearMonth | undefined; + abstract setNextAvailableTurn(turn?: IntYearMonth): void; + + abstract getCost(): [gold: number, rice: number]; + abstract getPreReqTurn(): number; + abstract getPostReqTurn(): number; + + protected testPostReqTurn(): ["testPostReqTurn", string] | undefined { + if (this.getPostReqTurn() === 0) { + return undefined; + } + + const nextAvailableTurn = this.getNextAvailableTurn(); + if (nextAvailableTurn === undefined) { + return undefined; + } + + const yearMonth = this.env.raw.yearMonth; + const remainTurn = nextAvailableTurn - yearMonth; + if (remainTurn <= 0) { + return undefined; + } + + return ["testPostReqTurn", `${remainTurn}턴 더 기다려야 합니다`]; + } + + testPermissionToReserve(): string | undefined { + if (this.cachedPermissionToReserve) { + return this.reasonNotPermissionToReserve; + } + + if (this.reasonNotPermissionToReserve) { + return this.reasonNotPermissionToReserve; + } + + const testResult = testContraints(this.permissionConstraints ?? [], this); + if (testResult !== undefined) { + [this.errorInvoker, this.reasonNotPermissionToReserve] = testResult; + } + this.cachedPermissionToReserve = true; + return this.reasonNotPermissionToReserve; + } + + public canDisplay(): boolean { + return true; + } + + public testMinConditionMet(): string | undefined { + if (this.minConditionConstraints === undefined) { + return this.testFullConditionMet(); + } + + if (this.cachedMinConditionMet) { + return this.reasonNotMinConditionMet; + } + + const testResult = testContraints(this.minConditionConstraints, this); + if (testResult !== undefined) { + [this.errorInvoker, this.reasonNotMinConditionMet] = testResult; + } + + if (this.reasonNotMinConditionMet === undefined && !this.isLazyCalcReqTurn) { + const result = this.testPostReqTurn(); + if (result !== undefined) { + [this.errorInvoker, this.reasonNotMinConditionMet] = result; + } + } + + this.cachedMinConditionMet = true; + return this.reasonNotMinConditionMet; + } + + public testFullConditionMet(): string | undefined { + if (this.fullConditionConstraints === undefined) { + throw new InvalidArgument('fullConditionConstraints가 제대로 설정되지 않았습니다'); + } + + if (this.cachedFullConditionMet) { + return this.reasonNotFullConditionMet; + } + + const testResult = testContraints(this.fullConditionConstraints, this); + if (testResult !== undefined) { + [this.errorInvoker, this.reasonNotFullConditionMet] = testResult; + } + + if (this.reasonNotFullConditionMet === undefined) { + const result = this.testPostReqTurn(); + if (result !== undefined) { + [this.errorInvoker, this.reasonNotFullConditionMet] = result; + } + } + + this.cachedFullConditionMet = true; + return this.reasonNotFullConditionMet; + } + + public get termString(): string { + const commandName = this.commandBrief; + const term = this.getNextAvailableTurn(); + const termMax = this.getPreReqTurn() + 1; + return `${commandName} 수행중... (${term}/${termMax})`; + } + + public addTermStack(): boolean { + if (this.getPreReqTurn() === 0) { + return true; + } + + const lastTurn = this.general.raw.aux.lastTurn; + + const nextTurn: Exclude = { + action: this.className, + arg: this.arg, + brief: this.commandBrief, + term: 1 + }; + + if (lastTurn === undefined) { + this.general.update((raw) => { + raw.aux.lastTurn = nextTurn; + }); + return false; + } + + if (lastTurn.action !== this.className || lastTurn.arg !== this.arg) { + this.general.update((raw) => { + raw.aux.lastTurn = nextTurn; + }); + return false; + } + + if (lastTurn.term < this.getPreReqTurn()) { + nextTurn.term = lastTurn.term + 1; + this.general.update((raw) => { + raw.aux.lastTurn = nextTurn; + }); + return false; + } + + return true; + } + + public get hasPermissionToReserve(): boolean { + return this.testPermissionToReserve() === undefined; + } + + public get hasMinConditionMet(): boolean { + return this.testMinConditionMet() === undefined; + } + + public get hasFullConditionMet(): boolean { + return this.testFullConditionMet() === undefined; + } + + public get failString(): string { + const commandName = this.commandBrief; + const failReason = this.testFullConditionMet(); + if (failReason === undefined) { + throw new Error('실행 가능한 커맨드에 대해 실패 이유를 수집'); + } + return `${failReason} ${commandName} 실패.`; + } + + public get alternativeCommand(): BaseCommand { + return must(this.alternative); + } + + abstract run(rng: RandUtil): boolean; +} \ No newline at end of file diff --git a/@sammo/game_logic/src/Constraint/index.ts b/@sammo/game_logic/src/Constraint/index.ts new file mode 100644 index 0000000..a60c107 --- /dev/null +++ b/@sammo/game_logic/src/Constraint/index.ts @@ -0,0 +1,50 @@ +import type { City } from "@/City.js" +import type { GameEnv } from "@/GameEnv.js" +import type { General } from "@/General.js" +import type { Nation } from "@/Nation.js" + +export const enum RequiredDestLevel { + None = 0, + Nation = 1, + City = 2, + General = 3, +} + +export interface IConstraint = {}> { + readonly reason?: string; + readonly requiredDestLevel: RequiredDestLevel; + readonly arg: ArgType; + test(input: TestInput): undefined | [testName: string, err: string]; +} + +export const constraints: { + [key: string]: IConstraint +} = { +} + +export interface TestInput { + general: Readonly, + destNation?: Readonly, + destCity?: Readonly, + destGeneral?: Readonly, + env: Readonly, +} + +export function testContraints(testList: IConstraint[], input: TestInput): undefined | [testName: string, err: string]{ + for(const test of testList){ + if(test.requiredDestLevel >= RequiredDestLevel.Nation && input.destNation === undefined){ + return [test.constructor.name, "require destNation"]; + } + else if(test.requiredDestLevel >= RequiredDestLevel.City && input.destCity === undefined){ + return [test.constructor.name, "require destCity"]; + } + else if(test.requiredDestLevel >= RequiredDestLevel.General && input.destGeneral === undefined){ + return [test.constructor.name, "require destGeneral"]; + } + const result = test.test(input); + if(result !== undefined){ + return result; + } + } + return undefined; +} diff --git a/@sammo/game_logic/src/Entity/GeneralEntity.ts b/@sammo/game_logic/src/Entity/GeneralEntity.ts index b066efc..2956df8 100644 --- a/@sammo/game_logic/src/Entity/GeneralEntity.ts +++ b/@sammo/game_logic/src/Entity/GeneralEntity.ts @@ -1,4 +1,4 @@ -import type { CityID, DiplomaticPermission, FractionalRange, GeneralID, GeneralName, IntMonth, IntYear, IntYearMonth, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, PersonalityType, SpecialityDomesticType, SpecialityWarType, SquadID, URILike, UserID, UserName } from "../defs.js"; +import type { CityID, DiplomaticPermission, FractionalRange, GeneralID, GeneralName, IntMonth, IntYear, IntYearMonth, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, PersonalityType, ReservedTurn, SpecialityDomesticType, SpecialityWarType, SquadID, URILike, UserID, UserName } from "../defs.js"; export interface IGeneralEntity { id: GeneralID; @@ -73,5 +73,6 @@ export interface IGeneralEntity { aux: { lastBattleDate?: Date; lastBattleYearMonth?: IntYearMonth; + lastTurn?: ReservedTurn & { term: number }; } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0283637..979b7f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -230,6 +230,9 @@ importers: mongoose: specifier: ^8.2.1 version: 8.2.1 + zod: + specifier: ^3.22.4 + version: 3.22.4 devDependencies: '@types/node': specifier: ^20.11.24