ちいかわぽけっと(ちいぽけ)がサイバーエージェントグループの applibot社からリリースされました。

スマホのゲームはあんまりやらないのですが、初めてのちいかわのちゃんとしたスマホゲームということでちょっとプレイしています。数字の扱い方がヤバすぎるのでどうなっているのか少し考えてみようと思います。

単位の表記

出典: ちいかわぽけっと

画面上に o とか h とかのアルファベットがありますが、これは桁数を表現しています。

ちいぽけにおける所持金や攻撃力、体力などのパラメータは非線形に急激に増加していき、 k = 10^3 とかではなく、 a = 10^3, b=10^6, c=10^9, ... といった具合にオーダーが増加していきます。

これを一般項にすると以下のように言えます。

  • : abなどのアルファベット
  • : aから数えたときのアルファベットの登場順
  • : Sの文字数
  • : Sの左から 番目の文字(abcのとき、 は b)
  • : 文字のインデックス(a=0, b=1, …, z=25)

とすると、

アルファベットS が何番目の記号かを示す n(S)

一般的な表現 V

現時点で確認されているのは aa とアルファベット二桁までなので、最低限ここまではあるだろうと思われるzzをこれに適用します。

, , , とすると、zzは702番目の記号とわかります。

702番目の記号zzは、 より であるとわかります。天文学的という形容では全く追いつかないくらい大きな値です。

論理的な表現

一般的な64ビットの倍精度浮動小数点数( double 型)では、大体 くらいの非常に大きな(or 小さな)数を表現できますが、ちいぽけには足りません。

しかしちいぽけはゲーム設計上、非常に大きな値と相対的に小さな値の演算は重要ではないはずなので数字の精度はざっくりで良いはずです。

どんなふうに扱っているのかを想像してみます。

浮動小数点数の応用を考えてみます。浮動小数点数では、数を「仮数部(有効桁数を持つ部分)」と「指数部(10の何乗かを示す部分)」に分けて表現します。

この構造を拡張して考えるとさらに大きな値も扱うことができそうです。ここで仮数部と指数部を以下のように定義してみます。

  • 仮数部: ゲームプレイに必要な精度のみ保持する
    • e.g. 5.25 のような2桁程度
  • 指数部: 「10の何乗か」を示す部分
    • 整数型など

こうすると以下のようになります。

  • 5.2aa (5.2×10^81) は、内部的に { 仮数部: 5.2, 指数部: 81 }
  • 1.0zz (1.0×10^2106) は、内部的に { 仮数部: 1.0, 指数部: 2106 }

といった形でデータを保持できるはずです。

javascriptで表現してみます。負の値は今のところ見たことがないので考慮しません。

class ChiipokeNumber {
  /**
   * @param {number} mantissa 仮数部
   * @param {number} exponent 指数部
   */
  constructor(mantissa = 0, exponent = 0) {
    this.mantissa = Number(mantissa);
    this.exponent = Number(exponent);
    this.normalize();
  }
 
  /**
   * 数値を正規化する(仮数部が1以上10未満になるようにする)
   * @method normalize
   * @returns {void}
   */
  normalize() {
    if (this.mantissa === 0) {
      this.exponent = 0;
      return;
    }
    while (this.mantissa >= 10) {
      this.mantissa /= 10;
      this.exponent++;
    }
    while (this.mantissa < 1 && this.mantissa !== 0) {
      this.mantissa *= 10;
      this.exponent--;
    }
  }
 
  /**
   * @param {ChiipokeNumber} num
   */
  multiply(num) {
    const newMantissa = this.mantissa * num.mantissa;
    const newExponent = this.exponent + num.exponent;
    return new ChiipokeNumber(newMantissa, newExponent);
  }
 
  /**
   * @param {ChiipokeNumber} num
   */
  add(num) {
    // 片方の値が相対的に非常に大きい場合は小さい方を無視
    if (Math.abs(this.exponent - num.exponent) > 16) {
      return this.exponent > num.exponent ? new ChiipokeNumber(this.mantissa, this.exponent) : new ChiipokeNumber(num.mantissa, num.exponent);
    }
  
    let newMantissa;
    let newExponent;
    if (this.exponent >= num.exponent) {
      newExponent = this.exponent;
      newMantissa = this.mantissa + num.mantissa / (10**(this.exponent - num.exponent));
    } else {
      newExponent = num.exponent;
      newMantissa = num.mantissa + this.mantissa / (10**(num.exponent - this.exponent));
    }
    return new ChiipokeNumber(newMantissa, newExponent);
  }
 
  /**
   * アルファベット表記を得る
   * @method getNotationString
   * @param {number} n   単位インデックス (a=1, aa=27, ...)
   * @returns {string} アルファベット表記
   */
  static getNotationString(n) {
    // 0以下の場合はアルファベットなし
    if (n <= 0) return "";
 
    const alphabet = "abcdefghijklmnopqrstuvwxyz";
    let L = 0; // アルファベットの文字数
    let sumPowers = 0; // L-1 文字以下の単位の総数
    let currentPower = 1; // 基数
 
    while (true) {
        const unitsInThisLength = currentPower * 26;
        if (n <= sumPowers + unitsInThisLength) {
            L++;
            break;
        }
        L++;
        sumPowers += unitsInThisLength;
        currentPower *= 26;
 
        // アルファベット10文字数を超えるとエラーにする(現状確認されているのは2までだが)
        if (L > 10) throw new Error("L is too large"); 
    }
 
    const nPrime = n - sumPowers - 1; // 長さL内での0基準インデックス
 
    let notation = "";
    let tempNPrime = nPrime;
    for (let i = 0; i < L; i++) {
        const power = 26**(L - 1 - i);
        const index = Math.floor(tempNPrime / power);
        if (index < 0 || index >= 26) throw new Error("index is out of range");
        notation += alphabet[index];
        tempNPrime %= power;
    }
    return notation;
  }
 
  /**
   * ゲーム内表記文字列を返す
   * @method toDisplayNumber
   * @returns {string} ゲーム内表記文字列
   */
  toDisplayNumber() {
    if (this.exponent < 3) {
      const value = this.mantissa * (10**this.exponent);
      const fixedValue = value.toFixed(2);
      const displayValue = fixedValue.endsWith('.00') ? fixedValue.slice(0, -3) : fixedValue;
      return displayValue;
    }
 
    const notationIndex = Math.floor(this.exponent / 3);
    const remainderExponent = this.exponent % 3;
    const displayMantissa = this.mantissa * (10**remainderExponent);
    const notationString = ChiipokeNumber.getNotationString(notationIndex);
 
    return `${displayMantissa.toFixed(2)}${notationString}`;
  }
}

使用例

// 足し算
const a = new ChiipokeNumber(5.2, 20); 
const b = new ChiipokeNumber(1.5, 21);
 
a.toDisplayNumber() 
// -> '520.00f'
b.toDisplayNumber() 
// -> '1.50g'
a.add(b).toDisplayNumber() 
// -> '2.02g'
 
// 掛け算
const a = new ChiipokeNumber(5, 35); 
const _15 = new ChiipokeNumber(15, 0);
 
a.toDisplayNumber() 
// -> '500.00k'
a.multiply(_15).toDisplayNumber() 
// -> '7.50l'
 
// 超巨大な値同士の掛け算
const a = new ChiipokeNumber(123.456, 1234); 
const b = new ChiipokeNumber(456.789, 5678);
 
a.toDisplayNumber()
// -> '1.23ov'
b.toDisplayNumber()
// -> '45.68btu'
a.multiply(b).toDisplayNumber()
// -> '56.39cjq'

実際のロジックはわかりませんが、このようにしてゲーム上のパラメータというユースケースにおいては非常に巨大な値を扱うことができそうです。

おわりに

ちいぽけのゲーム、賛否両論かなりあるけど個人的にはちいかわのゲームっぽくて好きです。みんなでプレイしよう!