見出し画像

#50 サイコロ Shut the Box

● 問題

サイコロを使ったひとり遊びです。

<ルール>
1~9までのカードが1枚ずつ,サイコロ2つを使う
・サイコロ2つを振って,その目の合計を求める
・目の合計と同じ値になるように,カードを取る(カードの枚数は任意)
 目の合計が6の場合
 ・6のカード1枚取る
 ・1と5のカードを2枚取る(他の組み合わせでもよい)
・全部のカードがなくなったら上がり(shut the boxと言います)

すぐに終わってしまいます。
メッセージを表示している部分の前のコードを疑ってみましょう。

import random
import itertools

card = [str(i+1for i in range(9)]
select_card = []

def sum_not_match():
  global count
  for r in select_card:
    card.append(r)
  print('選択したカードの値の合計とサイコロの合計が一致しません')
  print('')
  select_card.clear()
  count = 0
  card.sort()  

clearflag = 0

while clearflag != 1:
  count = 0

  dice1 = random.randint(1,6)
  dice2 = random.randint(1,6)
  dice_sum = dice1+dice2

  print('')
  print(f'サイコロ1:{dice1},サイコロ2:{dice2},合計:{dice_sum}')
  print('')

  for n in range(1,5):
    for conb in itertools.combinations(card, n):
        sumflag = False
        conb_i = [int(i) for i in conb]
        if dice_sum == sum(conb_i):
          sumflag = True
          break

  if sumflag == False:
    print(f'カード:{"・".join(card)}')
    print(f'どのカードを組み合わせても{dice_sum}になりません')
    break

  while True:
    print(f'カード:{"・".join(card)}')
    cardNum = input('カード番号?')

    if cardNum in card:
      card.remove(cardNum)
      select_card.append(cardNum)
      count += 1
    else:
      print('そのカードはありません')

    select_card_i = [int(i) for i in select_card]
    if sum(select_card_i) == dice_sum:
      select_card.clear()
      break
    elif sum(select_card_i) > dice_sum:
      sum_not_match()

    if len(card) == 0:
      if sum(select_card_i) == dice_sum:
        clearflag = 1
        print('素晴らしい!!すべてのカードがなくなりました')
        break
      else:
        sum_not_match()

● 解答

if sumflag == False: 成り立つので,即終了となっています。使えるカードのすべてを組み合わせて,その合計を求め,2つのサイコロの和と同じ組み合わせがなければ,ゲーム終了となります。
例えば,使えるカードが1・2・3の場合,すべての組み合わせは,1,2,3,1+2,1+3,2+3,1+2+3の7通りになります。
配列の要素のすべての組み合わせを求めるアルゴリズムは再帰関数を使うなど,初心者にとっては結構ハードルが高いのですが,Pythonではitertoolsモジュールを使えば簡単に求めることができます。
それが,itertools.combinations(card, n)になります。たったこれだけでできるなんて魔法です。
itertools.combinations(card, 1)で,1,2,3,itertools.combinations(card, 2)で,1・2,1・3,2・3です。
for n in range(1,5):として,nを1~4まで変化させることで,1枚から4枚までの全組み合わせを求めています。
どんな場合でも4枚までの組み合わせにしているのは,2つのサイコロの目の和の合計は最大で12です。カードを最小の1から組み合わせた場合,12になるのは1,2,3,4のときなので,4枚より多いカードを組み合わせを考える必要がないのです。

こうして,カードの組み合わせから合計を求め,サイコロの目の和と同じかどうかを判断している部分が,if dice_sum == sum(conb_i):です。
条件が成り立てば,sumflag = Trueとして,breakでループを抜ける
一見するとよさそうですが,ここに落とし穴があります。
forループは2重ループなので,単純にbreakとしただけでは,1つめのループを抜けるだけなのですね。そのため,再度sumflag = Falseが実行されてしまうのです。
2重ループを抜けるためのコードを3行追加しました。
どのように動作するかコメントしてあるので,コードがどのような順に動作するのかを確認するとよいでしょう。
2重ループを抜ける方法には,様々な方法がありますが,今回はfor elseを採用しました。

import random
import itertools

card = [str(i+1for i in range(9)]
select_card = []

def sum_not_match():
  global count
  for r in select_card:
    card.append(r)
  print('選択したカードの値の合計とサイコロの合計が一致しません')
  print('')
  select_card.clear()
  count = 0
  card.sort()  

clearflag = 0

while clearflag != 1:
  count = 0

  dice1 = random.randint(1,6)
  dice2 = random.randint(1,6)
  dice_sum = dice1+dice2

  print('')
  print(f'サイコロ1:{dice1},サイコロ2:{dice2},合計:{dice_sum}')
  print('')

  for n in range(1,5):
    for conb in itertools.combinations(card, n):
        sumflag = False
        conb_i = [int(i) for i in conb]
        if dice_sum == sum(conb_i):
          sumflag = True
          break #実行されると★に飛ぶ 
    else# 追加
      continue # 追加
    break # 追加 ★ これが実行されるとループから完全に抜ける

  if sumflag == False:
    print(f'カード:{"・".join(card)}')
    print(f'どのカードを組み合わせても{dice_sum}になりません')
    break

  while True:
    print(f'カード:{"・".join(card)}')
    cardNum = input('カード番号?')

    if cardNum in card:
      card.remove(cardNum)
      select_card.append(cardNum)
      count += 1
    else:
      print('そのカードはありません')

    select_card_i = [int(i) for i in select_card]
    if sum(select_card_i) == dice_sum:
      select_card.clear()
      break
    elif sum(select_card_i) > dice_sum:
      sum_not_match()

    if len(card) == 0:
      if sum(select_card_i) == dice_sum:
        clearflag = 1
        print('素晴らしい!!すべてのカードがなくなりました')
        break
      else:
        sum_not_match()

実際にやってみるとわかりますが,shut the boxになるのはかなり難しいです。私は何度も挑戦しましたが,未だにできていません。
しかし,プログラムがきちんと動くか確かめるためには,成功できる場合も確認する必要があります。
そんなときは,以下のようにサイコロの目の和を自由に設定できるようにするとよいでしょう。

 # dice1 = random.randint(1,6)
 # dice2 = random.randint(1,6)
 # dice_sum = dice1+dice2

   dice_input = input('サイコロの目の和?')
   dice_sum = int(dice_input)

#Python  #プログラミング

この記事が気に入ったらサポートをしてみませんか?