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

花札こいこいを遊べるアプリ Koikoi を、iPhone / iPad / Mac / Apple Vision Pro 向けにリリースしました。
広告もアプリ内課金もなく、オフラインで遊べます。
モチベーション
ターミナルで動く花札アプリ を作ったり、子供達に幼少のときに英才教育するぐらい、花札を愛しています。
普段使っている 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 にサポートしており、同じアプリとして提供しています。
対応 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 |
KoikoiUI | SwiftUI のビューとビューモデル。全プラットフォームで共有 | 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 までご連絡ください。