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

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

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

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

![tictactoe.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255846/dd11cec6-c299-b1d0-5fc2-f51511ebbf59.gif)

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

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

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


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

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

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

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

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

1. 3x3の盤面を表示する
1. ユーザーに入力をさせる
1. 入力を盤面に反映させる
1. 盤面表示
1. 勝利判定をする
1. CPUに入力をさせる
1. 入力を盤面に反映させる
1. 勝利判定をする
1. 盤面表示

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

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

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

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


以上、設計終わり！

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

## 3x3 盤面の表示

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

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

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

```zsh
0o2
345
678
```

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

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

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

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

```python
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... と変化していく)

ポイントは

```python
if(i%3 == 2):
```

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

つまり、 `for` の中で 
`i` が `0,1,2,3,4,5,6,7,8` と増えるのに対して、
`i%3` は `0,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="") # 改行しない
```

以下のように結果が出ましたか！？

```zsh
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() # 盤面表示

```

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

```py
def displayBoard():
```

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

```zsh
012
345
678
Hello!!
012
345
678
```

## ターンを進める

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

1. 0~8の座標を入力
1. 入力座標に `o` か `x` が入っていたら再入力
1. それ以外ならgameBoardに反映

これだけ！簡単！

ちなみに `inputBoard` は

```py
def inputBoard(playerType):
```
とし、

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

とりあえず完成形。

```py
# ターンを進めるための関数
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()` というので括って、入力値を数字に変換しています。

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

#### CPUはランダムな値を入力する
プレイヤーと同様に、CPUの座標も `tgt` という変数に格納しましょう。
これもかなりシンプル。

```py
tgt = random.randint(0,8)
```

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

```game.py
import random
``` 
を入れる必要があります。

#### プレイヤーかCPUか

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

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

### Step2: 入力座標に `o` か `x` が入っていたら再入力
続いては、入力座標に `o` か `x` が入っていたら再入力機能を実装します。
Step２は以下の一文だけ。

```py
# 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に反映
これもとてもシンプル。

```py
# 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がランダムで入力をしてくれます。
ちなみに最後の方にある、

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

を

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

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

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

```py
# 勝利判定
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()` を呼び出すと、 `o` か `x` か `None` が returnされます。
linesに入っている勝ち手を全てチェックして、同一の記号が入っていないかチェックしているだけです。

１つ補足をすると、

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

は

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

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

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

以上で機能は全て完成！

## ゲームを進行する

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

```py
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
```

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

```py
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
```