Command 초안

- PHP 버전 거의 그대로 이식
This commit is contained in:
2024-03-15 17:35:32 +00:00
parent 78bcc95e2e
commit 494ed3f3f1
5 changed files with 309 additions and 2 deletions
+2 -1
View File
@@ -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"
+252
View File
@@ -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<ArgType extends Record<string, unknown>>{
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<Nation> | undefined {
return this._destNation;
}
public get destCity(): Readonly<City> | undefined {
return this._destCity;
}
public get destGeneral(): Readonly<General> | undefined {
return this._destGeneral;
}
protected abstract alternative: BaseCommand<any> | undefined;
protected abstract permissionConstraints: IConstraint<any>[] | undefined;
protected abstract minConditionConstraints: IConstraint<any>[] | undefined;
protected abstract fullConditionConstraints: IConstraint<any>[] | 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<typeof lastTurn, undefined> = {
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<any> {
return must(this.alternative);
}
abstract run(rng: RandUtil): boolean;
}
+50
View File
@@ -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<ArgType extends Record<string, unknown> = {}> {
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<General>,
destNation?: Readonly<Nation>,
destCity?: Readonly<City>,
destGeneral?: Readonly<General>,
env: Readonly<GameEnv>,
}
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;
}
@@ -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 };
}
}
+3
View File
@@ -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