All files / src/utils ScoreNormalizer.ts

100% Statements 18/18
92.3% Branches 12/13
100% Functions 3/3
100% Lines 18/18

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                                            15x   2x 2x       13x 3x 3x       10x               6x 6x       2x 2x         1x 1x       1x 1x     10x                 10x       13x      
import { NormalizedScore } from "../types.js";
 
/**
 * 異なるゲームドメインの評価値を、UI での表示に適した共通のスケール (-1.0 to 1.0) に正規化するユーティリティ。
 */
export class ScoreNormalizer {
  /**
   * 評価値を正規化します。
   *
   * @param raw - 生の数値 (cp, points, diff 等)
   * @param unit - 単位 ('cp', 'points', 'diff', 'winrate')
   * @param domain - ゲームドメイン (chess, shogi, reversi 等)
   * @returns 正規化されたスコア (-1.0 〜 1.0)
   */
  public static normalize(
    raw: number,
    unit: "cp" | "mate" | "points" | "winrate" | "diff" | string,
    domain?: string,
  ): NormalizedScore {
    let normalized: number;
 
    // 詰みの特殊処理
    if (unit === "mate") {
      // 詰みの場合は距離に関わらず端に近い値(0.99 or -0.99)を返す
      normalized = raw > 0 ? 0.99 : -0.99;
      return normalized as NormalizedScore;
    }
 
    // 勝率の場合は既に 0.0 - 1.0 なので -1.0 〜 1.0 にスケーリング
    if (unit === "winrate") {
      normalized = (raw - 0.5) * 2;
      return this.clamp(normalized) as NormalizedScore;
    }
 
    // ゲームドメインに応じたシグモイド正規化
    switch (domain) {
      case "shogi":
      case "chess":
      case "xiangqi":
      case "janggi":
        // センチポーン (cp) 基準: 600cp (約1ポーン/歩の差) を 0.5 付近にマッピング
        // シグモイド関数: 2 / (1 + exp(-raw / k)) - 1
        // k=600 のとき raw=600 -> 0.46, raw=1200 -> 0.76, raw=2500 -> 0.96
        normalized = this.sigmoid(raw, 600);
        break;
 
      case "reversi":
        // 石差 (diff) 基準: 最大 64 石。16石差を 0.5 付近にマッピング
        normalized = this.sigmoid(raw, 16);
        break;
 
      case "go":
        // 囲碁 (points/scoreLead) 基準: 20目差を 0.8 付近にマッピング
        // 囲碁は形勢判断が急激に動くため、k=10 程度で急峻にする
        normalized = this.sigmoid(raw, 10);
        break;
 
      default:
        // 不明なドメインは標準的なスケーリング (k=1000)
        normalized = this.sigmoid(raw, 1000);
        break;
    }
 
    return this.clamp(normalized) as NormalizedScore;
  }
 
  /**
   * シグモイド関数によるスケーリング。
   * @param x - 入力値
   * @param k - スケール係数 (x=k の時に 0.46 付近、x=2k の時に 0.76 付近)
   */
  private static sigmoid(x: number, k: number): number {
    return 2 / (1 + Math.exp(-x / k)) - 1;
  }
 
  private static clamp(val: number): number {
    return Math.max(-1, Math.min(1, val));
  }
}