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?

# ループ処理のロジック備忘録 〜トランプゲームの戦争の実装でハマったこと〜

0
Posted at

はじめに

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

ハマりポイント② fieldplayed を分けずに管理する

問題のコード(例)

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)を使って執筆・整理しました。

0
0
3

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?