はじめに
Rubyでトランプゲーム「戦争」を実装する課題に取り組んだ際、ループ処理のロジックで複数回ハマりました。
この記事では「なぜそう書いてはいけないか」という原因と対策をセットで残しておきます。
トランプゲームの戦争のルール概要
- 52枚のカードを2人に26枚ずつ配る
- 強さの順:A > K > Q > J > 10 > ... > 2
- お互いに手札の一番上を出して比較、強い方がすべての場札を獲得する
- 引き分けの場合、もう1枚出して勝負を続ける(決着がつくまで繰り返す)
- 誰かの手札が0枚になったらゲーム終了
この「引き分けのたびにカードを積み上げる」処理がループの肝になります。
ハマりポイント① ループ変数を使わないまま書いてしまう
問題のコード(例)
for i in players
if name == "player1"
puts "Yes"
end
end
何が起きるか
ループ変数 i を条件式の中で一切使っていないため、players の要素数だけ puts "Yes" が繰り返し実行される。
# players が2要素なら
Yes
Yes
原因
ループを書いた目的(「コレクションの中から条件に合うものを探す」)と、実際のコード(「ループ変数を使わずに固定条件を繰り返す」)がズレている。
対策
ループが本当に必要かどうかを問い直す。要素が含まれているか調べるだけなら include? で十分。
# ループ不要
if players.include?("player1")
puts "Yes"
end
ループを使う場合は、必ずループ変数を条件式の中で活用する。
for player in players
if player == "player1" # ← 変数 player を使う
puts "Yes"
end
end
ハマりポイント② field と played を分けずに管理する
問題のコード(例)
field = []
loop do
# 両プレイヤーがカードを出す
field << player1.draw
field << player2.draw
if field[0].strength > field[1].strength # ← インデックスがズレる!
player1.robcards(field)
field = []
elsif field[0].strength < field[1].strength
player2.robcards(field)
field = []
else
puts "引き分けです。"
# field はリセットしない → 次のループで field が増えていく
end
end
何が起きるか
引き分けが続くと field に以下のようにカードが蓄積する。
1回目:field = [p1のカード, p2のカード]
2回目:field = [p1のカード, p2のカード, p1のカード, p2のカード]
3回目:field = [p1のカード, p2のカード, p1のカード, p2のカード, p1のカード, p2のカード]
field[0] と field[1] は常に「最初に出したカード」を指してしまい、今回出したカードで比較ができない。
原因
「場に蓄積するカード(field)」と「今回の比較に使うカード(played)」を同じ変数で管理しようとしたため。
対策
役割を分けて2つの変数で管理する。
field = [] # loop の外で初期化(引き分けカードの蓄積用)
loop do
played = [] # loop の中で初期化(今回出したカードの比較用)
played << player1.draw
played << player2.draw
field.concat(played) # 今回のカードを field に追加
if played[0].strength > played[1].strength # ← played で比較
player1.robcards(field)
field = [] # 勝負がついた時だけリセット
elsif played[0].strength < played[1].strength
player2.robcards(field)
field = []
else
puts "引き分けです。"
# field はリセットしない(次のループへ)
end
end
| 変数 | 役割 | 初期化タイミング |
|---|---|---|
field |
引き分け時も含めた全場札 |
loop の外で一度だけ |
played |
今回のターンで出したカード(比較用) |
loop の中で毎回 |
ハマりポイント③ concat の参照問題(dup を使う)
問題のコード(例)
def robcards(field)
@cards.concat(field)
field.clear # field をクリアしたら @cards の中身も消える!
end
何が起きるか
concat は参照を追加するため、field と @cards の末尾が同じオブジェクトを指す状態になる。
field.clear を呼ぶと @cards 内の要素も消えてしまう。
原因
concat はオブジェクトへの参照を追加する。元の配列とコピー先の配列が同じオブジェクトを共有している。
対策
dup で別オブジェクトとしてコピーしてから concat する。
def robcards(field)
@cards.concat(field.dup.shuffle) # dup で独立したコピーを作る
end
また、勝者がもらったカードを shuffle してから手札に加えることで、同じパターンが繰り返されて無限ループになるリスクも防げる。
ハマりポイント④ 集計変数を puts の中で足し算してしまう
問題のコード(例)
total = 0
for i in numbers
puts total += i # ← ループのたびに出力してしまう
end
何が起きるか
ループのたびに途中経過が出力される。最終的な合計だけ出したい場合には使えない。
対策
集計とアウトプットを分ける。
total = 0
for i in numbers
total += i # 集計だけ
end
puts total # ループの後に出力
まとめ:ループを書く前のチェックリスト
| チェック項目 | NG例 | OK例 |
|---|---|---|
| ループ変数を使っているか |
for i in arr でも i を使わない |
条件式で必ず i を参照する |
| ループが本当に必要か | 要素の有無確認に for を使う |
include? などを使う |
| 比較用・蓄積用の変数を分けているか |
field だけで両方兼ねる |
field(蓄積)と played(比較)を分ける |
concat 後に元配列を変更していないか |
concat 後に field.clear する |
field.dup でコピーしてから concat
|
| 集計とアウトプットを分けているか | ループ内で puts total += i
|
ループ後に puts total
|
おわりに
ループ処理でバグが起きる多くのケースは、「ループが何をしているか」と「自分がやりたいこと」のズレから発生します。
コードを書く前に「このループは何を繰り返しているのか」「変数の役割はひとつに絞れているか」を言葉にする習慣が大切だと実感しました。
※ この記事はClaude(AI)を使って執筆・整理しました。