おはこんばんにちは!
1回目の記事 で CPU の心理戦、前回の記事 でシャッフルと難易度設計を書きました。
今回が シリーズ最終回 です。テーマは CLI の見た目とテンポ。
ロジックだけ動けばゲームにはなるけれど、ターミナルゲームは 表示がすべて です。
old_maid では、外部クレートを組み合わせて「なんとなく遊べる感じ」を作っています。
- リポジトリ: yoshitaka-k/old_maid
- 起動:
cargo run

▲ figlet-rs の ASCII アートタイトル。シアン太字で着色されている状態。
- 使っているクレート
- 表示の設計 — 色で「誰の情報か」を伝える
- 区切り線 — 画面の「章立て」
- figlet-rs — 最初のインパクト
- indicatif — 待ち時間を「見せる」
- rustyline — 入力を「ゲームっぽく」
- テンポ調整 — wait_for_dramatic_pause
- clap — 起動オプション
- リザルト画面 — 色のレイヤー
- 設計で意識したこと
- シリーズ振り返り
- 動かしてみる
- まとめ
- 参考リンク
使っているクレート
Cargo.toml の dependencies はシンプルです。
clap = { version = "4.6.1", features = ["derive"] } console = "0.16.3" figlet-rs = "1.0.0" indicatif = "0.18.4" rand = "0.8.5" rustyline = "18.0.0"
| クレート | 役割 |
|---|---|
figlet-rs |
起動時の ASCII アートタイトル |
console |
色付きテキスト・太字・薄字 |
indicatif |
スピナー・プログレスバー |
rustyline |
行入力(Enter 確定・履歴対応) |
clap |
コマンドライン引数(--mode maid/man) |
GUI フレームワークは使っていません。
ターミナル向けクレートの組み合わせ だけで UI を構成しています。
表示の設計 — 色で「誰の情報か」を伝える
src/cli/console.rs に表示用の関数を集約しています。
pub fn system(prompt: &str) { /* 緑 */ } pub fn system_bold(prompt: &str) { /* 緑・太字 */ } pub fn info(prompt: &str) { /* シアン */ } pub fn error(prompt: &str) { /* 赤 */ }
| 関数 | 色 | 用途 |
|---|---|---|
system / system_bold |
緑 | ゲーム進行のシステムメッセージ |
info |
シアン | 確認・お知らせ |
error |
赤 | 入力エラー |
ターン表示は 人間と CPU で色を分け ています。
fn player_color(is_human: bool) -> Style { if is_human { Style::new().green() } else { Style::new().magenta() } }
Round: 1 / Turn: 5 / Player (Human) が緑、
CPU 2 (Veteran) がマゼンタ — 一目で「誰のターンか」がわかります。
その他の色使い:
- 黄色 — 上がり通知(
cleard)、ラウンドリザルトのタイトル - 薄字(dim) — 総合リザルトのサブ見出し
- 黄色太字 —
Total Game Resultのタイトル
色のルールを決めておくと、後からメッセージを追加しても 見た目が散らからない です。
区切り線 — 画面の「章立て」
pub fn print_single_separator() { print_hr('-', 50); } pub fn print_double_separator() { print_hr('=', 50); }
- × 50 が小さな区切り、= × 50 が大きな区切りです。
main.rs では、設定入力のたびに print_single_separator、
ラウンド開始前後に print_double_separator を使い分けています。
ターミナルにはウィンドウ枠がないので、 区切り線 = 章タイトル の代わりにしています。
figlet-rs — 最初のインパクト
起動直後に ASCII アートのタイトルを出します。
let style = Style::new().cyan().bold(); let standard_font = FIGlet::standard().unwrap(); print!( "{}", style.apply_to( standard_font.convert(&format!("Old {}", capitalize(&args.mode))).unwrap() ) );
--mode maid なら Old Maid、--mode man なら Old Man と表示が変わります。
clap で受け取った引数をそのままタイトルに反映する、小さな連携です。
ゲームの中身より先に 「何のアプリか」 を伝える役割です。
indicatif — 待ち時間を「見せる」
スピナー — 処理中
シャッフルや CPU のカード選択など、一瞬で終わる処理を包みます。
pub fn execute_with_spinner<T, F>(set_message: &str, finish_message: &str, f: F) -> T where F: FnOnce() -> T, { let pb = mult.add(ProgressBar::new_spinner()); pb.enable_steady_tick(Duration::from_millis(100)); pb.set_style( ProgressStyle::with_template("{spinner:.green} {msg}") .unwrap() .tick_strings(&["|", "/", "-", "\\"]), ); pb.set_message(set_message.to_string()); let result = f(); // finish_message が空ならバーを消す ... result }
Deck setup and CPU 1 a shuffle... のように、誰が何をしているか をメッセージに入れています。

▲ 起家 CPU が山札を切っている間のスピナー。
プログレスバー — 進捗が数値でわかる処理
配札は 1 枚ずつ進むので、プログレスバー向きです。
execute_with_progress(deck_len as u64, "Deal the cards.", "Deal the cards end.", |pb| { for i in 0..deck_len { // 1 枚配る pb.inc(1); thread::sleep(Duration::from_millis(rand_range(10..=100))); } });
テンプレート:
{msg} [{wide_bar:.green}] {pos}/{len} ({eta})
{eta} で残り時間の目安も出ます(2 本目で書いた thread::sleep とセット)。
MultiProgress — 全員同時にスピナー
配札後の手札整理は、全プレイヤー分のスピナーを並べて表示 します。
fn set_progress_spinners(players: &mut Vec<Player>) -> Vec<ProgressBar> { let multi = MultiProgress::new(); for player in players.iter() { let pb = multi.add(ProgressBar::new_spinner()); pb.set_message(format!("{} Arrange my Hand (pair off)...", player.get_name())); spinners.push(pb); } spinners }
thread::scope で各プレイヤーのペア捨てを並列実行しつつ、
スピナーも同時に回る — 「全員が手札を整理している」 雰囲気が出ます。

▲ Player / CPU 1 / CPU 2 … のスピナーが縦に並んでいる状態。
rustyline — 入力を「ゲームっぽく」
数値入力は rustyline で行っています。
fn read_line(prompt: &str) -> rustyline::Result<String> { let styled_prompt = Style::new() .apply_to(format!("[INPUT] {} >> ", prompt)) .to_string(); let mut rl = rustyline::DefaultEditor::new()?; rl.readline(&styled_prompt) }
プロンプトは [INPUT] CPU Player Num ... >> の形式。
Enter だけ押せば デフォルト値 が使われます。
if line.is_empty() { Ok(default) } else { Ok(line.parse::<usize>()?) }
範囲外の入力はループで再入力を促します。
Ok(num) if (min_num..=max_num).contains(&num) => break num, Ok(_) => error(&format!("The input is not a number {}-{}.", min_num, max_num)),
Ctrl+C / Ctrl+D も明示的に処理しています。
Err(ReadlineError::Interrupted) => { system_bold("Pressing Ctrl+C. Ends the Game."); std::process::exit(0); }
ターミナルゲームでは 突然の終了 より、メッセージを出してから終わる方が親切です。

▲ [INPUT] 付きのプロンプト。CPU 人数や難易度を選んでいる画面。
テンポ調整 — wait_for_dramatic_pause
lib.rs に 200ms の待機関数があります。
pub fn wait_for_dramatic_pause() { std::thread::sleep(std::time::Duration::from_millis(200)); }
main.rs では設定確認のたびに呼んでいます。
let cpu_member = cpu_member_input(); info(&format!(" Confirmed: {} CPU Players Joining...", cpu_member)); wait_for_dramatic_pause();
入力 → 確認表示 → 200ms 待ち → 次の入力、というリズムです。
情報が一気に流れすぎないようにする 演出用の pause です。
シャッフル内の thread::sleep(2 本目)と役割が似ていますが、
こちらは メインループの UI テンポ 専用です。
clap — 起動オプション
#[derive(Parser)] struct Args { #[arg(short, long, default_value = "maid")] mode: String, }
cargo run -- --mode maid # ババ抜き(ジョーカー追加) cargo run -- --mode man # ジジ抜き(ミステリーカード除外)
-- 以降がバイナリに渡る点は、Rust 初学者がつまずきやすいところなので README にも明記しています。
リザルト画面 — 色のレイヤー
総合リザルト(game_result)は、色で情報の 重要度 を分けています。
// サブ見出し — 薄字 Style::new().dim().apply_to("各ラウンドの獲得ポイント") // メインタイトル — 黄色太字 Style::new().yellow().bold().apply_to("Total Game Result") // 順位 — 緑 Style::new().green().apply_to("総合順位(同点は参加順)")
メダル絵文字(🥇🥈🥉)は constants.rs で定義し、
ラウンドリザルトと総合順位の両方で使い回しています。
1 本目で書いた ジョーカーボーナス の表示も、 この dim / green / yellow のルールに乗っています。
設計で意識したこと
1. 表示ロジックを cli/console.rs に集約
ゲーム本体(game.rs)からは system() や execute_with_spinner() を呼ぶだけ。
「何を表示するか」 と 「どう表示するか」 を分離しています。
2. スピナーと sleep はセット
indicatif だけ入れても、処理が一瞬ならスピナーは見えません。
2 本目で書いたシャッフル内の sleep と、配札のランダム sleep は、
演出が見えるための前提 です。
3. CLI でも「フェーズ」がわかるように
起動 → 設定 → サイコロ → ラウンド → リザルト。
各区切りで separator・色・pause を変えることで、
今どのフェーズにいるか が伝わるようにしました。
シリーズ振り返り
3 記事で old_maid の設計を分解しました。
| 回 | テーマ | キーワード |
|---|---|---|
| 1 本目 | CPU の心理戦 | 履歴、joker_in_history_taken、難易度別の隠し方 |
| 2 本目 | シャッフルと CPU | ヒンズー / リフル / ディール、CpuStrategy、6 スロット抽選 |
| 3 本目(今回) | CLI 演出 | console / indicatif / figlet-rs / rustyline |
ゲームロジック・AI・演出 — ババ抜き 1 本から、Rust の勉強題材として 切り口を 3 つ 取れたのがこのシリーズのまとめです。
動かしてみる
git clone https://github.com/yoshitaka-k/old_maid.git cd old_maid cargo run
演出を意識して観察するポイント:
- 起動時の FIGlet タイトルと
--modeの連動 - 配札後の 複数スピナー が同時に回る様子
- 人間ターン(緑)と CPU ターン(マゼンタ)の色の違い
- 複数ラウンド後の
Total Game Resultの色分け
まとめ
consoleで 色のルール を決め、情報の種類とプレイヤー種別を視覚化したindicatifで スピナー・プログレスバー・MultiProgress を使い分けたfiglet-rsで起動時の 第一印象 を作ったrustylineで 入力・デフォルト値・終了処理 を整えたwait_for_dramatic_pauseと separator で テンポと章立て を調整した
ターミナルゲームは画面がない分、テキストと時間の使い方がすべてです。 old_maid くらいの規模でも、クレートをいくつか組み合わせるだけで だいぶ違う ので、CLI ゲームを作るときの参考になれば幸いです。