Apple プラットフォーム向け 花札こいこい アプリをリリースしました

·2 分で読めます
Apple プラットフォーム向け 花札こいこい アプリをリリースしました

花札こいこいを遊べるアプリ Koikoi を、iPhone / iPad / Mac / Apple Vision Pro 向けにリリースしました。

広告もアプリ内課金もなく、オフラインで遊べます。

App Store / 公式サイト

モチベーション

ターミナルで動く花札アプリ を作ったり、子供達に幼少のときに英才教育するぐらい、花札を愛しています。

普段使っている iPhone や iPad、Mac でも、以前から余計な機能、有料・広告付きのアプリしか見つけられず、SwiftUI で作られた、クリーンな花札アプリが欲しいと思っていました。

それと、Vision Pro で XR 空間で遊べる花札を、実験的に作ってみたかったのもあります。

ルールの正解は Go 版の実装とテストにあるので、今回はそれを Swift に移して、札の見た目と、それぞれの端末での操作に手をかけることにしました。

使い方

起動すると対局設定の画面が出るので、対局数 (三月・六月・十二月) と難易度、テーマを選んで始めます。

  • 難易度: かんたん / ふつう / つよい / たつじん
  • テーマ: システム / 緑羅紗 / 畳 / 夜
  • 操作: タップ・ドラッグ&ドロップ・矢印キー

成立している役と、あと 1 枚のリーチも表示します。

対局は自動で保存され、途中で閉じても続きから打てます。

Game Center のリーダーボードにも対応しています。

Mac

Mac 版は、ウィンドウに合わせて、自分の取り札・場・相手の取り札を横に三列で並べています。

キーボードだけで最後まで遊べるようにしていて、矢印キーで札を選び、Return / Space で出し、Esc で取り消します。

ウィンドウは自由にリサイズでき、「背景を透過」を入れるとデスクトップが透けます。

Apple Vision Pro

visionOS 版は volumetric window の中に実寸大の卓を置いています。

札は卓の上に並び、視線とピンチで選びます。

役のパネルとスコアボードは、下端のバーをつかんで好きな位置へ動かせて、動かした位置は保存されます。

導入方法

App Store よりインストールできます。

iPhone / iPad / Mac / Apple Vision Pro にサポートしており、同じアプリとして提供しています。

App Store

対応 OS は iOS 26 / macOS 26 / visionOS 26 以降です。

ソースコードは GitHub で公開していて、ルールエンジンのテストは手元で回せます。

git clone https://github.com/ngs/koikoi-swift.git
cd koikoi-swift
swift test                 # ルールエンジン・対戦相手・ビューモデルのテスト
tuist generate --no-open   # Xcode のワークスペースを生成

札の絵とアプリアイコンは別の private リポジトリにあり MIT ライセンスの対象外なので、公開リポジトリだけではアプリ本体はビルドできません。

Under the hood

このアプリは Swift 6 で実装しており、以下の 3 つの構成でできています。

モジュール役割主な型
KoikoiCore札の定義・役判定・ラウンドと対局の進行。Go 版からの移植で、Foundation 以外に依存しないCard / Game / YakuChecker / HeuristicOpponent
KoikoiAI対戦相手の探索RoundSimulator / Determinizer / ISMCTSEngine
KoikoiUISwiftUI のビューとビューモデル。全プラットフォームで共有GameViewModel / GameRecord

KoikoiAI は歴史的な経緯で AI と名乗っていますが、LLM や機械学習のモデルは使っておらず、Pure Swift でアルゴリズムを実装しています。

  • ふつう: HeuristicOpponent の評価ルール (Go 版の cpu.go の移植) で、札の価値を 光 20・タネ 10・短冊 5・カス 1 として、取れる札の合計がいちばん高い手を選ぶ
  • かんたん: ふつうの評価ルールに加えて、3 回に 1 回はランダムに出し、こいこいはしない
  • つよい: ふつうの評価ルールに加えて、光や、猪鹿蝶・赤短・青短の札を取れる手にボーナスを足し、手札に余裕があれば積極的にこいこいする
  • たつじん: ISMCTSEngine の探索 (情報集合モンテカルロ木探索) で、相手の見えない札を仮定して、1 手ごとに 400 回シミュレーションする

札と ID

札は 48 枚の固定の配列で、id (0〜47) の並びを Go 版の AllCards と同じにしています。

enum Month: Int { case january, february, /* ... */ december }
enum CardType: Int { case kasu, tane, tanzaku, hikari }

struct Card {
    let id: Int        // 0〜47。Go 版と同じ並び
    let month: Month
    let type: CardType
}

// Card.all[0] = Card(id: 0, month: .january, type: .hikari)   // 松に鶴
// Card.all[1] = Card(id: 1, month: .january, type: .tanzaku)  // 松に赤短

Go 側のテストも、ID の列から札を作る形のまま移しています。

たつじんの探索

たつじんは、自分からは見えない相手の手札と山札を、枚数の辻褄が合うようにシャッフルし直して 1 つの局面を仮定し、その局面でラウンドの最後までを、ふつうの評価ルールで打ち進めます。

// 仮定した局面 (相手の手札と山札を配り直したもの)
struct RoundSimulator {
    var game: Game
    var phase: RoundPhase
}

// 探索の木の 1 ノード
final class Node {
    let move: Move?
    var children: [Move: Node]
    var visits: Int           // 通った回数
    var availability: Int     // この手が打てた回数
    var totalReward: Double   // 勝ち負けと文数を 0〜1 にした報酬の合計
}

これを 400 回くり返し、通った回数がいちばん多い手を選びます。

対局の保存

対局は、盤面のスナップショットではなく、乱数のシードと、両者の全指し手の記録で保存しています。

struct GameRecord: Codable {
    var rounds: Int              // 3 / 6 / 12
    var difficulty: Difficulty   // easy / normal / hard / search
    var seed: UInt64             // 乱数のシード (配札もここから決まる)
    var moves: [Move]            // 打たれた手 (双方・順番どおり)
}

enum Move: Codable {
    case playHand(handID: Int, fieldChoiceID: Int?)  // 手札を出す
    case chooseDrawnField(fieldID: Int)              // 山札から引いた札の取り先
    case koikoi
    case shobu
}

開くときは、シードから同じ配札を作り直し、moves を頭から順に適用して、同じ局面まで戻しています。

フィードバックのお願い

不具合や機能要望は GitHub Issues までご連絡ください。