All files XiangqiParser.ts

97.87% Statements 46/47
77.41% Branches 24/31
100% Functions 7/7
100% Lines 41/41

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                        3x 3x 3x 1x   2x 2x       1x       4x 4x             15x 14x   5x 5x 5x 21x 21x   21x   5x 5x   1x 1x   1x 1x   5x 5x 5x 5x           5x     4x 6x 6x   1x     6x 4x 4x       21x           11x 10x 1x 1x         9x      
import { IProtocolParser, ScoreNormalizer, PositionId, MiddlewareCommand, ProtocolValidator } from "@multi-game-engines/core";
import { IXiangqiSearchOptions,
  IXiangqiSearchInfo,
  IXiangqiSearchResult,
  createXiangqiMove, } from "@multi-game-engines/domain-xiangqi";
 
export class XiangqiParser implements IProtocolParser<
  IXiangqiSearchOptions,
  IXiangqiSearchInfo,
  IXiangqiSearchResult
> {
  createSearchCommand(options: IXiangqiSearchOptions): MiddlewareCommand {
    ProtocolValidator.assertNoInjection(options, "XiangqiSearchOptions", true);
    const commands: string[] = [];
    if (options.xfen) {
      commands.push(`position fen ${options.xfen}`);
    }
    commands.push("go");
    return commands;
  }
 
  createStopCommand(): MiddlewareCommand {
    return "stop";
  }
 
  createOptionCommand(name: string, value: unknown): MiddlewareCommand {
    ProtocolValidator.assertNoInjection({ name, value }, "XiangqiOption", true);
    return `setoption name ${name} value ${value}`;
  }
 
  parseInfo(
    data: string | Record<string, unknown>,
    positionId?: PositionId,
  ): IXiangqiSearchInfo | null {
    if (typeof data !== "string") return null;
    if (!data.startsWith("info ")) return null;
 
    const info: IXiangqiSearchInfo = { raw: data, positionId };
    const parts = data.split(/\s+/);
    for (let i = 0; i < parts.length; i++) {
      const token = parts[i];
      Iif (!token) continue;
 
      switch (token) {
        case "depth":
          info.depth = parseInt(parts[++i] || "0", 10);
          break;
        case "nodes":
          info.nodes = parseInt(parts[++i] || "0", 10);
          break;
        case "nps":
          info.nps = parseInt(parts[++i] || "0", 10);
          break;
        case "score": {
          const type = parts[++i];
          const val = parseInt(parts[++i] || "0", 10);
          Eif (type === "cp" || type === "mate") {
            info.score = {
              unit: type,
              [type]: val,
              normalized: ScoreNormalizer.normalize(val, type, "xiangqi"),
            };
          }
          break;
        }
        case "pv": {
          const pvMoves = parts.slice(i + 1).map((m) => {
            try {
              return createXiangqiMove(m);
            } catch {
              return null;
            }
          });
          info.pv = pvMoves.filter((m) => m !== null);
          i = parts.length; // PV is always last in UCCI/UCI
          break;
        }
      }
    }
    return info;
  }
 
  parseResult(
    data: string | Record<string, unknown>,
  ): IXiangqiSearchResult | null {
    if (typeof data !== "string") return null;
    if (data.startsWith("bestmove ")) {
      const parts = data.split(/\s+/);
      return {
        bestMove: parts[1] ? createXiangqiMove(parts[1]) : null,
        raw: data,
      };
    }
    return null;
  }
}