LoginSignup
1
6

More than 1 year has passed since last update.

【初心者向け】Pythonで三目並べ(マルバツゲーム)を作る

Posted at

はじめに

この記事はプログラミング初心者さんがPythonで三目並べを作成するために作成されています。
学校での課題で出たよとか、とりあえずPythonで何か作ってみたいよって方を想定しています。

せっかくなのでエンジニア気分を味わってもらうべく、ちょっとカッコつけた(エンジニアっぽい)書き方を随所でしています。
ですが基本的には省略しすぎずにコードを書いていきます。
(全体的に、図少なめです。ご了承ください。)

システム要件

このゲームは下記に示す様な、ターミナル上で動きます。
使用環境はPython3です。

tictactoe.gif

よくある三目並べと比べると、かなり簡易化されています。
具体的には、今回紹介するゲームは以下の要件を満たします。

  • CPUとの対戦形式
  • 自分が先攻 (o) でCPUが後攻 (x)
  • CPUの手はランダムで確定
  • 勝ち負け・引き分け判定がある

設計する

コーディングを始める前に、設計をします。
初心者さんは特に設計に時間かけないとコードを書いている内に迷子になります。
ゲームの流れや細かい機能を考えていきます。

盤面管理

ゲームに利用する盤面はどのように管理する必要があると思いますか?
この記事では盤面(gameBoard)は以下のように定義します。

gameBoard = [0,1,2,3,4,5,6,7,8] # ゲームに利用する盤面

普通のリストです。
番号と座標が対応しており、例えばプレイヤーが 1 座標に o を入力した場合は以下のように盤面が変化します。

gameBoard = [0,'o',2,3,4,5,6,7,8] # プレイヤーが座標1に'o'を入力

ゲームフローを考える

初めに「マルバツゲームを進行するのにどんな手順が必要だったっけ?」と考えます。
最初はざっくりとで大丈夫です。
例えば今回だと、

  1. 3x3の盤面を表示する
  2. ユーザーに入力をさせる
  3. 入力を盤面に反映させる
  4. 盤面表示
  5. 勝利判定をする
  6. CPUに入力をさせる
  7. 入力を盤面に反映させる
  8. 勝利判定をする
  9. 盤面表示

との具合で進めると、とりあえずプレイヤーとCPUがそれぞれ1手ずつ指すことができます。

機能の抽出

続いてはフローに基づいて「機能」を抽出していきます。
前節でのゲームフローでは9つのステップを表示しました。
ですが、必要な機能はもっとシンプルです。

  • 3x3 盤面を表示
  • ユーザーまたはCPUに入力をさせて盤面に反映
  • 勝利判定

これら 3つの機能を繰り返すことでゲームが成立させます

以上、設計終わり!

実装

それではお待ちかね、実装をしていきます。
前節でピックアップした3つの機能の実装 についてそれぞれ見ていきます!

3x3 盤面の表示

以下のリストがあったとすると、

gameBoard = [0,'o',2,3,4,5,6,7,8] # プレイヤーが座標1に'o'を入力

以下のように表示する機能を実装します。

0o2
345
678

(※補足) リストへのアクセス方法は下記を参考にしてください。

gameBoard = [0,'o',2,3,4,5,6,7,8] # こんな盤面の時
print(gameBoard[1]) # ← こうすると o が表示される

実装の方針は gameBoard[0]gameBoard[1] ...と1マスずつ表示をさせ、 2番目と5番目 のときは表示後に改行させます。
言い換えると 2番目と5番目以外は改行しないで表示する ということになります。

ここはエンジニアっぽい書き方をしてみます。
とりあえず、コードを読んでみて下さい。
解説はコードの下に。

for i in range(0, len(gameBoard)):
    if(i%3 == 2): # 3回に1回は改行
        print(gameBoard[i]) # 改行する
    else :
        print(gameBoard[i], end="") # 改行しない

for で1マスずつ表示を行います。
i は0から始まり、ループのたびに1ずつ増え、9になったら forが終了します。
(つまり i は 0,1,2,3... と変化していく)

ポイントは

if(i%3 == 2):

です。 i%3 は 「 i を3で割った数の余り」を示しています。
例えば、 i=2 のとき、 i%3 = 2i=4 のとき、 i%3 = 1 となります。

つまり、 for の中で
i0,1,2,3,4,5,6,7,8 と増えるのに対して、
i%30,1,2,0,1,2,0,1,2, となります。

もうお分かり頂けたでしょうか?
i%3 == 2の時というのは i が2,5,8の時を表しています。

これで、本節の冒頭で書いた

2番目と5番目 のときは表示後に改行させます。

という処理が書けました!

ちなみに改行と改行しないの区別はそれぞれ print(表示するもの)print(表示するもの, end="") の違いです。
後者が改行をさせない書き方になります

とりあえず実行

以下のコードを実行してみて下さい。

game.py
gameBoard = [0,1,2,3,4,5,6,7,8] # ゲームに利用する盤面

for i in range(0, len(gameBoard)):
    if(i%3 == 2): # 3回に1回は改行
        print(gameBoard[i]) # 改行する
    else :
        print(gameBoard[i], end="") # 改行しない

以下のように結果が出ましたか!?

012
345
678

関数定義

さて、これらの機能を関数として定義します。

関数とは、何かの入力に対して出力があるカタマリのことを指します。
よく自動販売機で例えられますね。
お金を入れる(入力)と飲み物が出てくる(出力)感じです。

とりあえず、以下を実行!

game.py
gameBoard = [0,1,2,3,4,5,6,7,8] # ゲームに利用する盤面

# 盤面を3x3で表示する関数
def displayBoard():
    for i in range(0, len(gameBoard)):
        if(i%3 == 2): # 3回に1回は改行
            print(gameBoard[i]) # 改行する
        else :
            print(gameBoard[i], end="") # 改行しない

# ここが本命
displayBoard() # 盤面表示
print('Hello!') # 無駄にHelloを表示
displayBoard() # 盤面表示

以下の一文を書くだけで、

def displayBoard():

さっきの for が入った処理を displayBoard() として呼び出せるようになっています!
2回for文を書かなくても、下記のように盤面が2回表示されるはずです!

012
345
678
Hello!!
012
345
678

ターンを進める

続いて、ターンを進めるためにinputBoard という関数を作ります。
ターンを進めるのには3つのステップが考えられます。

  1. 0~8の座標を入力
  2. 入力座標に ox が入っていたら再入力
  3. それ以外ならgameBoardに反映

これだけ!簡単!

ちなみに inputBoard

def inputBoard(playerType):

とし、

inputBoard('o') とやるとプレイヤー、
inputBoard('x') とやるとCPU
といった具合に、引数である playerTypeox が入る想定です。

とりあえず完成形。

# ターンを進めるための関数
def inputBoard(playerType):
    # 1.座標を入力させる
    if(playerType == "o"):  # o が渡されたら座標を入力
        tgt = int(input("0~8の座標を入れてください: "))
    else: # xが渡されたらランダムで座標を入力
        tgt = random.randint(0,8)

    # 2.入力座標に 'o'か 'x' が入っていたら再入力
    if(gameBoard[tgt] == 'o' or gameBoard[tgt] == 'x'):
        inputBoard(playerType)

    # 3.gameBoardに反映
    else:
        gameBoard[tgt] = playerType


inputBoard('o') # これでユーザーに入力をさせることができる
inputBoard('x') # これでCPUに入力をさせることができる

Step1: 0~8の座標を入力

プレイヤーかCPUに0~8の座標を入力させます。

プレイヤーからの座標入力を受け付ける

それでは、プレイヤーが入力した座標を tgt という変数に格納します。
input() は文字列を受け取るので int() というので括って、入力値を数字に変換しています。

tgt = int(input("0~8の座標を入れてください: ")) # プレイヤーの入力座標受付

CPUはランダムな値を入力する

プレイヤーと同様に、CPUの座標も tgt という変数に格納しましょう。
これもかなりシンプル。

tgt = random.randint(0,8)

整数の乱数はrandom.randint(最小値,最大値) という具合に使用します。
randomを使うために、コードの一番上に

game.py
import random

を入れる必要があります。

プレイヤーかCPUか

これら2つの機能が if 文で分割されており、 playerType を見ることで、どちらを呼び出すかを判定しています。
以下、Step1の完成形

def inputBoard(playerType):
    # 1.座標を入力させる
    if(playerType == "o"):  # o が渡されたら座標を入力
        tgt = int(input("0~8の座標を入れてください: "))
    else: # xが渡されたらランダムで座標を入力
        tgt = random.randint(0,8)

Step2: 入力座標に ox が入っていたら再入力

続いては、入力座標に ox が入っていたら再入力機能を実装します。
Step2は以下の一文だけ。

# 2.入力座標に 'o'か 'x' が入っていないことを確認
if(gameBoard[tgt] == 'o' or gameBoard[tgt] == 'x'): inputBoard(playerType)

Step1で tgt という座標をユーザーかCPUが入力しました。
そのため、 gameBoard[tgt] に何が入っているのかを if で検証します。
if では条件を or で繋ぐことできます。

if( 条件A or 条件B): 実行するもの 

を日本語に訳すなら もしも「条件A」または「条件B」を満たしていたら「実行するもの」を実行する といった具合です。

今回の場合は inputBoard をもう一度呼び出すことで実装しています。

Step3: 何も入ってないならgameBoardに反映

これもとてもシンプル。

# 3.gameBoardに反映
else: gameBoard[tgt] = playerType

Step2の if と繋がっていることに注意して下さい。

ここまでで、表示と入力ができました。
全てをつなぎ合わせると、以下のようになります。

game.py
import random

gameBoard = [0,1,2,3,4,5,6,7,8] # ゲームに利用する盤面

# 盤面を3x3で表示する
def displayBoard():
    # gameBoardの要素を1つずつ表示
    for i in range(0, len(gameBoard)):
        if(i%3 == 2): # 3回に1回は改行
            print(gameBoard[i]) # 改行する
        else :
            print(gameBoard[i], end="") # 改行しない

# ターンを進める
def inputBoard(playerType):
    # 1.座標を入力させる
    if(playerType == "o"):  # o が渡されたら座標を入力
        tgt = int(input("0~8の座標を入れてください: "))
    else: # xが渡されたらランダムで座標を入力
        tgt = random.randint(0,8)

    # 2.入力座標に 'o'か 'x' が入っていないことを確認
    if(gameBoard[tgt] == 'o' or gameBoard[tgt] == 'x'):
        inputBoard(playerType)

    # 3.gameBoardに反映
    else:
        gameBoard[tgt] = playerType

# ゲームの進行
displayBoard()
inputBoard('o') # ユーザーのターン
displayBoard()
inputBoard('x') # CPUのターン
displayBoard()

これを実行すると、自分の入力をしたあとに、CPUがランダムで入力をしてくれます。
ちなみに最後の方にある、

# ゲームの進行
displayBoard()
inputBoard('o') # ユーザーのターン
displayBoard()
inputBoard('x') # CPUのターン
displayBoard()

# ゲームの進行
while(True):
    displayBoard()
    inputBoard('o') # ユーザーのターン
    displayBoard()
    inputBoard('x') # CPUのターン

とすると while(True) の中が無限ループするので、盤面が埋まるまで入力をし続けることができます。

Step:3 勝敗判定

最後に勝敗判定を行います。
とりあえず、勝敗判定を行う winner() という関数の完成形をお見せします。

# 勝利判定
def winner():
    # 勝ち手を列挙
    lines = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      [0, 4, 8],
      [2, 4, 6],
    ]
    # forで勝ち手を1パターンずつ見ていく
    for i in range(0, len(lines)):
        [a, b, c] = lines[i]
        # 勝ち手の場所に同じ記号が入っていないかを確認
        if gameBoard[a] and gameBoard[a] == gameBoard[b] and gameBoard[a] == gameBoard[c]: 
            # 同じ記号が入っていたら、入っている記号を返す
            return gameBoard[a]
    # どちらも勝っていない場合はNoneを返す
    return None

winner() を呼び出すと、 oxNone が returnされます。
linesに入っている勝ち手を全てチェックして、同一の記号が入っていないかチェックしているだけです。

1つ補足をすると、

[a, b, c] = lines[i]

a = lines[i][0]
b = lines[i][1]
c = lines[i][2]

を省略しています。
lines[i] には [0, 1, 2][3, 4, 5] などの勝ち手のいずれか1つが格納されます。
もっと具体的に書くと lines[1][3, 4, 5] となり、 lines[1][0]3 です。

a,b,cに「勝ち手座標」が格納され、 gameBoard[a]gameBoard[b]gameBoard[c] に同じ記号が入ってたら、その記号のプレイヤーが勝利です。

以上で機能は全て完成!

ゲームを進行する

さて、上記で作成した機能をループで回していきましょう!
燃え尽きたので、細かい説明は省略!

displayBoard() # 実行時に初めに表示される盤面

for turn in range(0,9):
    # 誰のターンかを判定する
    if(turn %2 == 0) : # あなたのターン
        print("You")
        inputBoard("o")
    else: # CPUのターン
        print("CPU")
        inputBoard("x")

    displayBoard() # 盤面表示

    if winner(): # 勝敗判定
        print(winner() + "の勝ち")
        break

    if turn == 8: # 8手目で引き分け
        print("引き分け")
        break

完成

おめでとうございます!
以上で完成です!
下記ソースは説明に使ったもの以外に、デコレーションが入っています。
それでは、良いエンジニアライフを!

import random

gameBoard = [0,1,2,3,4,5,6,7,8] # ゲームに利用する盤面

# 盤面を3x3で表示する
def displayBoard():
    # gameBoardの要素を1つずつ表示
    print("--+---+--") # デコレーション
    for i in range(0, len(gameBoard)):
        if(i%3 == 2): # 3回に1回は改行
            print(gameBoard[i]) # 改行する
            print("--+---+--") # デコレーション

        elif(i%3 == 1):
            print(" | " + str(gameBoard[i]) + " | ", end="") # 改行しない
        else :
            print(gameBoard[i], end="") # 改行しない

    print()

# ターンを進める
def inputBoard(playerType):
    # 1.座標を入力させる
    if(playerType == "o"):  # o が渡されたら座標を入力
        tgt = int(input("0~8の座標を入れてください: "))
    else: # xが渡されたらランダムで座標を入力
        tgt = random.randint(0,8)

    # 2.入力座標に 'o'か 'x' が入っていないことを確認
    if(gameBoard[tgt] == 'o' or gameBoard[tgt] == 'x'):
        inputBoard(playerType)

    # 3.gameBoardに反映
    else:
        gameBoard[tgt] = playerType

# 勝利判定
def winner():
    # 勝ち手を列挙
    lines = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      [0, 4, 8],
      [2, 4, 6],
    ]
    # forで勝ち手を1パターンずつ見ていく
    for i in range(0, len(lines)):
        [a, b, c] = lines[i]
        # 勝ち手の場所に同じ記号が入っていないかを確認
        if gameBoard[a] and gameBoard[a] == gameBoard[b] and gameBoard[a] == gameBoard[c]: 
            # 同じ記号が入っていたら、入っている記号を返す
            return gameBoard[a]
    # どちらも勝っていない場合はNoneを返す
    return None

displayBoard() # 実行時に初めに表示される盤面
# ゲームを実行する
for turn in range(0,9):
    # 誰のターンかを判定する
    if(turn %2 == 0) : # あなたのターン
        print("You")
        inputBoard("o")
    else: # CPUのターン
        print("CPU")
        inputBoard("x")

    displayBoard() # 盤面表示

    if winner(): # 勝敗判定
        print(winner() + "の勝ち")
        break

    if turn == 8: # 8手目で引き分け
        print("引き分け")
        break
1
6
2

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
1
6