0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Gleamであそぼ② 格ゲー体験向上ツールを作ろう(真顔)

0
Posted at

前回のあらすじ

Glaemってすげええ
https://qiita.com/sakura_gi215/items/eb3781dd86cc2029fc93

なに作る?

私は格ゲーが好きだ(告白)
ということでGuilty Gear Striveの上達のために個人ツールを作りまーす

この記事でやること

型設計と勝率ロジック実装

環境

ツール バージョン
Gleam 1.17.0
Erlang/OTP 29.0.2
gleam_stdlib 1.0.3
gleeunit 1.11.0

Windows + winget でのインストール:

winget install --id Erlang.ErlangOTP -e
winget install --id Gleam.Gleam -e

★メモ: Erlang を入れても erl が PATH に乗っておらず、gleam が動かなかった。
C:\Program Files\Erlang OTP\bin を PATH に通したら解決。新規シェルを開き直すのが確実。

エディタはずっと使ってみたかったZedを使用

UIより先に型とロジック

まずドメイン(型)と勝率計算ロジックだけを純粋関数で書き、テストで固める。 理由は単純で、ロジックがネットワークやDBを知らなければ、テストが書きやすく壊れにくいから

src/
  ggst_tracker.gleam          … gleam run 用のデモ
  ggst_tracker/
    character.gleam           … 全キャラを直和型で定義
    match.gleam               … 1試合の記録
    stats.gleam               … 勝率・マッチアップ集計(心臓部)
test/
  ggst_tracker_test.gleam     … stats のテスト

型設計

キャラを文字列ではなく型で管理

pub type Character {
  Sol
  Ky
  May
  // ... 
}

"Sol" のような文字列で扱うと typo で "sol" と書いてもコンパイルが通ってしまう。

直和型にすれば存在しないキャラはそもそも書けない。
さらに表示名へ変換する関数を case で書くと

pub fn to_string(character: Character) -> String {
  case character {
    Sol -> "Sol Badguy"
    Ky -> "Ky Kiske"
    // ...
  }

追加キャラを Character に足したのに to_string に足し忘れると、
コンパイラが「網羅できてない」と教えてくれる
キャラは継続的に増えるので、追加漏れを見張らずに済む。

勝敗もBoolにしない

pub type MatchResult {
  Win
  Loss
}

true/false だと「どっちが勝ち?」が読む人に伝わらない。専用型なら意味が明確で、取り違えが起きづらいかと

試合の記録

pub type Match {
  Match(
    id: Int,
    my_character: Character,
    opponent: Character,
    result: MatchResult,
    note: Option(String),  // 試合メモ(任意)
  )
}

メモはあればなので Option(String)。null ではなく Option で無いことを型で表す。


ロジック

勝率と一番苦手なキャラ

集計の最小単位として勝率の型を作り、ゼロ除算を型で回避する

pub type WinRate {
  WinRate(wins: Int, losses: Int)
}

pub fn percentage(rate: WinRate) -> Float {
  case rate.wins + rate.losses {
    0 -> 0.0  // 0試合なら0%(ゼロ除算しない)
    n -> int.to_float(rate.wins) /. int.to_float(n) *. 100.0
  }
}

相手キャラごとの勝率は Dict に畳み込む

pub fn by_opponent(matches: List(Match)) -> Dict(Character, WinRate) {
  list.fold(matches, dict.new(), fn(acc, m) {
    let current = case dict.get(acc, m.opponent) {
      Ok(rate) -> rate
      Error(_) -> empty()
    }
    dict.insert(acc, m.opponent, add(current, m.result))
  })
}

そして一番苦手な相手を返す関数

|> list.fold(None, fn(worst, candidate) {
  case worst {
    Some(current) ->
      case percentage(candidate.1) <. percentage(current.1) { ... }
    // ...
  }
})

上記だとコンパイルエラーを起こすので次のようにアキュムレータに型注釈を追加

let initial: Option(#(Character, WinRate)) = None
// ...
|> list.fold(initial, fn(worst, candidate) { ... })

強い型推論を持つ言語でも、空のコレクション/None起点だと注釈が要る

動かしてみる

$ gleam run
=== GGST 上達トラッカー (Phase 1 demo) ===

総合成績: 3勝 3敗 (勝率 50.0%)

--- キャラ対別 ---
  vs Ky Kiske: 1-1 (50.0%)
  vs Happy Chaos: 1-2 (33.33333333333333%)
  vs May: 1-0 (100.0%)

⚠ 一番の苦手: Happy Chaos (勝率 33.33333333333333%) — 対策メモを書こう

所感

仕事終わりにやるにはむっずい

次回予告

入力UIを作ろう

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?