All files index.ts

100% Statements 27/27
100% Branches 18/18
100% Functions 4/4
100% Lines 27/27

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122                                                                                                      3x           60x 3x 3x           57x 57x 7x 7x 7x             49x             14x 14x         68x 1x 1x 1x             67x 20x         20x   47x 10x 17x   10x   37x 34x 37x       14x    
import { tCommon as translate } from "@multi-game-engines/i18n-common";
import { EngineError,
  EngineErrorCode,
  ProtocolValidator,
  Move,
  createMove,
  IBaseSearchOptions,
  IBaseSearchInfo,
  IBaseSearchResult,
  createI18nKey } from "@multi-game-engines/core";
 
/** 麻雀の指し手(打牌、副露等) */
export type MahjongMove = Move<"MahjongMove">;
 
/**
 * 麻雀の探索オプション。
 */
export interface IMahjongSearchOptions extends IBaseSearchOptions {
  board: Record<string, unknown> | unknown[];
  [key: string]: unknown;
}
 
/**
 * 麻雀の探索状況。
 */
export interface IMahjongSearchInfo extends IBaseSearchInfo {
  raw: string;
  thinking: string;
  evaluations?:
    | {
        move: MahjongMove;
        ev: number;
        prob?: number | undefined;
      }[]
    | undefined;
  [key: string]: unknown;
}
 
/**
 * 麻雀の探索結果。
 */
export interface IMahjongSearchResult extends IBaseSearchResult {
  raw: string;
  bestMove: MahjongMove | null;
  [key: string]: unknown;
}
 
/**
 * 麻雀の指し手形式(例: 1m, tsumo, riichi 等)を検証する正規表現。
 */
export const MAHJONG_MOVE_REGEX =
  /^([1-9][mpsz]|tsumo|ron|riichi|chi|pon|kan|kakan|nuki|none)$/;
 
/**
 * 文字列を MahjongMove へ変換し、厳密に検証します。
 */
export function createMahjongMove(move: string): MahjongMove {
  if (typeof move !== "string" || move.trim().length === 0) {
    const i18nKey = createI18nKey("engine.errors.invalidMahjongMove");
    throw new EngineError({
      code: EngineErrorCode.VALIDATION_ERROR,
      message: translate(i18nKey),
      i18nKey,
    });
  }
  ProtocolValidator.assertNoInjection(move, "MahjongMove");
  if (!MAHJONG_MOVE_REGEX.test(move)) {
    const i18nKey = createI18nKey("engine.errors.invalidMahjongMove");
    const i18nParams = { move };
    throw new EngineError({
      code: EngineErrorCode.VALIDATION_ERROR,
      message: translate(i18nKey, i18nParams),
      i18nKey,
      i18nParams,
    });
  }
  return createMove<"MahjongMove">(move);
}
 
/**
 * 麻雀盤面データ(JSON構造)のバリデータ。
 */
export function validateMahjongBoard(board: unknown): void {
  const MAX_DEPTH = 10;
  const validateValue = (
    value: unknown,
    path: string = "board",
    depth: number = 0,
  ): void => {
    if (depth > MAX_DEPTH) {
      const i18nKey = createI18nKey("engine.errors.nestedTooDeep");
      const i18nParams = { path };
      throw new EngineError({
        code: EngineErrorCode.VALIDATION_ERROR,
        message: translate(i18nKey, i18nParams),
        i18nKey,
        i18nParams,
      });
    }
    if (typeof value === "string") {
      ProtocolValidator.assertNoInjection(
        value,
        `mahjong board data: ${path}`,
        true,
      );
      return;
    }
    if (Array.isArray(value)) {
      value.forEach((v, i) => {
        validateValue(v, `${path}[${i}]`, depth + 1);
      });
      return;
    }
    if (value && typeof value === "object") {
      for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
        validateValue(v, `${path}.${k}`, depth + 1);
      }
    }
  };
  validateValue(board);
}