見出し画像

Pythonでマルバツゲーム(三目並べ)を実装して少し工夫した話

アルゴリズム力だとかプロダグラミングの基礎練習して、マルバツゲームの実装をよくやると思います。

マルバツゲームとは以下のような3x3の9マスの格子を用意して、二人のプレイヤーが交互に「○」と「×」を埋めていき、3つ並べるゲームのことです。

1|2|3
-----
4|5|6
-----
7|8|9

以下のように縦、横、斜めのどれかで3マス並べられた方が勝ち。

今回はこれを行うゲームをCUIベースでPythonで実装を行ってみます。

どのような手順で実装するか

ここで簡単な設計にあたるフローチャートを考えましょう。3目並べはお互い交互に○と×を埋めていきます。そのため先手と後手と順番が決まっており、どちらかが記号(○か×)を埋め終わった後に勝敗を一度考える必要があります。

そのため、ゲームは以下のような簡単なフロー(流れ)になるはずです。

1.最初のプレイヤーが記号を書き込む(最初なら先手が書き込む)
2.その時点で記号が3つ並んでいるかを確認
3.次のプレイヤーが記号を書き込む

3以降は2〜3をひたすら繰り返す

ある程度の簡単なゲームの流れがわかりました。それでは次にもう少しプログラムレベルでフローを考えてみましょう。

1.マスにどのプレイヤーが記号を書いたのかの配列を用意(pythonならリスト)1-9の値を並べた配列を用意する
2.先行のプレイヤーの入力値(書き込む座標)を標準入力で受け取る
3.先行プレイヤーの入力値の座標がマスに置けるかを判定し、可能ならマスの配列の値をプレイヤーの記号に置き換える
4.先行プレイヤーが今まで書き込んだ座標の配列に値を追加していく
5.記号が3つ並んでいるかの勝利判定を行う
6.後手のプレイヤーの入力値(書き込む座標)を標準入力で受け取る
7.後手プレイヤーの入力値の座標がマスに置けるかを判定し、可能ならマスの配列の値をプレイヤーの記号に置き換える
8.後手プレイヤーが今まで書き込んだ座標の配列に値を追加していく
9.記号が3つ並んでいるかの勝利判定を行う

2〜9をひたすら繰り返ししていく

上記を元にコードを実装していきます。

マスの配列の構造を考える

どの座標にどの記号が置かれているかの構造を考えていきましょう。今回はPythonを使用していくため、リスト型を用いてマスの配列を管理していきます。マスの座標は以下のように1-9の数値で表していきます。

1|2|3
-----
4|5|6
-----
7|8|9

Pythonのリスト型で表記するなら以下のようにします。ここでポイントとしては、座標の数値を数値型ではなく、文字列型にしていることです。

['1', '2', '3', '4', '5', '6', '7', '8', '9']

もし番地5番に○の記号を置く場合は、以下のように数値5番の位置に○を書き込んで入れ替えます。

1|2|3
-----
4|o|6
-----
7|8|9



['1', '2', '3', '4', 'o', '6', '7', '8', '9']

1. 勝利判定の処理を考える

マスのデーター構造に関しては考えたものの、記号が3つ並んだという判定処理を下記のデーター構造でどのようにして判定すればよろしいでしょうか。

['1', '2', '3', '4', '5', '6', '7', '8', '9']

一番簡単な方法としては、以下のように特定のパターンの数字がリストで並んでいるかを判定する方法です。

下記の関数の引数に入るcoordinate_mapには、プレイヤーが今まで書き込んできた座標のリストです。

def check_decision(coordinate_map):
    if [1, 2, 3] == list(set([1, 2, 3]) & set(coordinate_map)):
        return True
    if [4, 5, 6] == list(set([4, 5, 6]) & set(coordinate_map)):
        return True
    if [7, 8, 9] == list(set([7, 8, 9]) & set(coordinate_map)):
        return True
    if [1, 4, 7] == list(set([1, 4, 7]) & set(coordinate_map)):
        return True
    if [2, 5, 8] == list(set([2, 5, 8]) & set(coordinate_map)):
        return True
    if [3, 6, 9] == list(set([3, 6, 9]) & set(coordinate_map)):
        return True
    if [1, 5, 9] == list(set([1, 5, 9]) & set(coordinate_map)):
        return True
    if [3, 5, 7] == list(set([3, 5, 7]) & set(coordinate_map)):
        return True
    return False

上記の関数を見てみると、縦横斜めの記号が3つ並ぶ8通りのパターンに当てはまるかを判定しているのがわかります。

1|2|3
-----
4|5|6
-----
7|8|9

# 横のパターン 3通り
# 1-2-3
# 4-5-6
# 7-8-9

# 縦のパターン 3通り
# 1-4-7
# 2-5-6
# 3-6-9

# 斜めのパターン 2通り
# 1-5-9
# 3-5-7

上記関数のコード内のif文の条件式を見てみればわかりますが、わざわざ以下のような少しめんどくさいコードを書いていることがわかります。

これはcoordinate_mapが[2, 1, 3]や[3, 2, 1]とバラバラの順番で値が入ってくる可能性があるからです。coordinate_mapの中身のリストをset関数で[1, 2, 3]とソートさせ、もう一つのset([1, 2, 3])と共通部分を取り出し、それをリスト化させて、比較処理を行っています。

list(set([1, 2, 3]) & set(coordinate_map))

2. 勝利判定の処理を考える

上記では条件分岐が何回も走るため、少し冗長なコードかもしれません。もう少しシンプルな方法があるか考えてみましょう。

以下の3x3の9マスで縦、横、斜めのどれかが3つ並んだ際に、並んだそれぞれを足してみて、その合計値が特定のパターンの数だったら「3つ並んだ」と判定してよいかもしれません。

1|2|3
-----
4|5|6
-----
7|8|9

例えば[1, 2, 3]が並んだとして、1+2+3の合計である6が出れば、[1, 2, 3]が並んだと判定できます。[4, 5, 6]の合計である15が出れば、[4, 5, 6]が並んだと判定できるかもしれません。

しかし、これでは問題があります。合計値である15が出たとしても[4, 5, 6]以外にも[2, 5, 8]のパターンもあり得るからです。このように合計値が被ってしまいます。

そのため、1, 2, 3, 4, 5, 6, 7, 8, 9以外の数値を並べた配列を作成していきます。今回は2の累乗で並べた9個の座標を作成しました。

[1, 2, 4, 8, 16, 32, 64, 128, 256]

上記を3x3の座標にすると以下のようになります。

  1|  2|  4
-----------
  8| 16| 32
-----------
 64|128|256

上記の座標を縦、横、斜めの3目を足してみると以下のようになります。1~9と違い、値が被っているのがないことがわかります。

# 横列
# 1+2+4=7
# 8+16+32=56
# 64+128+256=448
   
# 縦列
# 1+8+64=73
# 2+16+128=146
# 4+32+256=292

# 斜め
# 1+16+256=273
# 4+16+64=84

上記を元に8パターンの配列を作成します。

[7, 56, 448, 73, 146, 292, 273, 84]

上記のリストを変数に格納し、判定処理の関数を作成します。最初のと違い、条件分岐が減ったため、かなりすっきりした形になりました。

def check_decision(coordinate_map):
    decision_coordinates = [7, 56, 448, 73, 146, 292, 273, 84]
    # 判定処理
    total_val = sum([int(i) for i in coordinate_map])
    if total_val in decision_coordinates:
        return True
    return False

勝利判定の変数を自動で生成

上記の3目を足した合計値である[7, 56, 448, 73, 146, 292, 273, 84]は手動で入力しました。このリスト(配列)を手動ではなく自動で生成してみます。
ここの章はオマケなため、先に飛ばしてもかまいません。

リストであるdecision_coordinatesの中身を自動生成するコードです。

candidates = [2**i for i in range(9)]
decision_coordinates = []

for i in range(3):
   # 横の合計を求める
   yoko_first_idx = i*3
   yoko_last_idx = (i+1)*3
   yoko_ans = sum(candidates[yoko_first_idx:yoko_last_idx])
   decision_coordinates.append(yoko_ans)

   # 縦の合計を求める
   tate_list = [candidates[i+3*j] for j in range(3)]
   tate_ans = sum(tate_list)
   decision_coordinates.append(tate_ans)

# 斜め
# 1+16+256=273
# 4+16+64=84

# 斜め 1
# 1+16+256=273
naname_1_list = [candidates[4*i] for i in range(3)]
naname_1_ans = sum(naname_1_list)
decision_coordinates.append(naname_1_ans)

# 斜め 2
# 4+16+64=84
naname_2_list = [candidates[2*(i+1)] for i in range(3)]
naname_2_ans = sum(naname_2_list)
decision_coordinates.append(naname_2_ans)

print(decision_coordinates)
# [7, 73, 56, 146, 448, 292, 273, 84]

全体のコード

上記をまとめたコード全体図になります。

#!/usr/bin/env python
# -*- coding:utf-8 -*-


def check_decision(coordinate_map):

   # candidates = [1, 2, 3, 4, 5, 6, 7, 8, 9]
   candidates = [2**i for i in range(9)]
   decision_coordinates = []
   for i in range(3):
       # 横の合計を求める
       yoko_first_idx = i*3
       yoko_last_idx = (i+1)*3
       yoko_ans = sum(candidates[yoko_first_idx:yoko_last_idx])
       decision_coordinates.append(yoko_ans)
       # 縦の合計を求める
       tate_list = [candidates[i+3*j] for j in range(3)]
       tate_ans = sum(tate_list)
       decision_coordinates.append(tate_ans)

   # 斜め
   # 1+16+256=273
   # 4+16+64=84

   # 斜め 1
   # 1+16+256=273
   naname_1_list = [candidates[4*i] for i in range(3)]
   naname_1_ans = sum(naname_1_list)
   decision_coordinates.append(naname_1_ans)

   # 斜め 2
   # 4+16+64=84
   naname_2_list = [candidates[2*(i+1)] for i in range(3)]
   naname_2_ans = sum(naname_2_list)
   decision_coordinates.append(naname_2_ans)

   # 横列
   # 1+2+4=7
   # 8+16+32=56
   # 64+128+256=448

   # 縦列
   # 1+8+64=73
   # 2+16+128=146
   # 4+32+256=292

   # 斜め
   # 1+16+256=273
   # 4+16+64=84
   # print('coordinate_map : ', coordinate_map)
   # print('decision_coordinates : ', decision_coordinates)

   # リスト内を全て足して、decision_coordinates内に
   # total_valがあるかを判定する
   # 判定処理
   total_val = sum([int(i) for i in coordinate_map])
   if total_val in decision_coordinates:
       return True
   return False

def marubatu_game():
   print('以下の数字で座標を指定してください')
   text = """
   1|2|3
   -----
   4|5|6
   -----
   7|8|9
   """
   print(text)
   coordinate_list = [str(i) for i in range(1, 10)]
   candidates = [2**i for i in range(9)]

   # print('candidates : ')
   # print(candidates)
   # print(coordinate_list)

   pre_user_input = str()
   pre_user_operations = []

   pos_user_input = str()
   pos_user_operations = []

   err_message = '正しい座標を入力する必要があります。'

   turn_user = 0
   turn_count = 0

   while True:
       if turn_user == 0:
           try:
               mes = 'pre_userの座標を入力'
               pre_user_input = input(mes)
           except Exception as e:
               print(err_message)
               continue
           if pre_user_input in coordinate_list:
               text = text.replace(str(pre_user_input), "o")
               idx = coordinate_list.index(pre_user_input)
               coordinate_list[idx] = "o"
               # coordinate_list.remove(pre_user_input)
               # pre_user_operations.append(pre_user_input)
               pre_user_operations.append(candidates[idx])
               print(text)
               if check_decision(pre_user_operations):
                   print('user0の勝ち')
                   break
           else:
               print(err_message)
               continue
           turn_user = 1
           turn_count += 1
       else:
           try:
               mes = 'pos_userの座標を入力'
               pos_user_input = input(mes)
           except Exception as e:
               print(err_message)
               continue
           if pos_user_input in coordinate_list:
               text = text.replace(str(pos_user_input), "x")
               idx = coordinate_list.index(pos_user_input)
               coordinate_list[idx] = "x"
               # coordinate_list.remove(pos_user_input)
               # pre_user_operations.append(pre_user_input)
               pos_user_operations.append(candidates[idx])
               print(text)
               if check_decision(pos_user_operations):
                   print('user1の勝ち')
                   break
           else:
               print(err_message)
               continue
           turn_user = 0
           turn_count += 1
       if turn_count == 9:
           print("引き分けです")
           break

marubatu_game()

実際に動かしてみると以下のようになります。

$ python marubatu.py
以下の数字で座標を指定してください

    1|2|3
    -----
    4|5|6
    -----
    7|8|9
    
coordinate_list :  ['1', '2', '3', '4', '5', '6', '7', '8', '9']
pre_userの座標を入力

実際に値を入力すると、マスに○の記号が埋まっているのがわかります。

$ python marubatu.py
pre_userの座標を入力1

    o|2|3
    -----
    4|5|6
    -----
    7|8|9
    
coordinate_list :  ['o', '2', '3', '4', '5', '6', '7', '8', '9']
pos_userの座標を入力

実際に値を交互に入力していき、1, 4, 7が並んだことで先手の○ユーザーが勝利しました。

pre_userの座標を入力7
   o|x|3
   -----
   o|x|6
   -----
   o|8|9
   
user0の勝ち

簡単ではありますが、三目並べであるマルバツゲームをPythonで実装してみました。プログラミング力などを鍛える目的でもマルバツゲームの実装はとても良い教材です。

一部分で説明が雑な部分があるため、後に修正を加えて再度投稿する予定です。

この記事を書いているのは平成最後である4/30の23:00ぐらいに書いています。平成までに何かしら技術のアウトプットをしたいと考えておりましたが、なんとか間に合いました。平成最後のしまかぜSoftの技術系記事になります。

生まれて初めての開元ですが、新しい令和になっても良いエンジニア人生を送られるようになっていきたいです。

また令和になってもお会いしましょう!令和になっても良いPythonライフを!良いソフトウェアエンジニア人生を!


参考資料


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