文書の過去の版を表示しています。
目次
オセロを作ってみる(作成中)
2026-05-18
ChatGPTとタッグを組んで書いていくよ。ソースコードは筆者が書いたC#ソースをベースにPython用にリライトしていくよ。
第0章 なぜオセロ?
最初に
…これはホビーです。
実際のソフトウエア開発を模する必要はありません。あくまで考え方、やり方の一例として読んでみてください。
選択理由
オセロはルールが単純でありながら、考えるべき事項を多く含んでいます。
例えば以下のような内容を扱います。
- 二次元配列
- 状態管理
- 方向探索
- ループと条件分岐
- データ構造
- CPU思考
- 設計分離
- 拡張設計
また、単純なルールでありながら「それっぽく考えているCPU」を作りやすいという特徴もあります。
「ゲームを完成させる」だけではなく、
- コンピュータにどう考えさせるか
- 振る舞いをどう設計するか
- 仕様変更にどう耐えるか
といった事についても扱います。
AI時代に“手作り”する意味
現在では、AIにコードを書かせる事も珍しくなくなりました。
しかし、AIが生成したコードを理解し、修正し、改善するためには、
- データをどう扱うか
- なぜその処理になるのか
- どこに責務を持たせるのか
といった基礎的な考え方が必要になります。
AIに任せているだけでもコードは組み上がるかもしれませんが、それは「自分で書いた」訳ではないですよね?…つまらなくありません?
「自分で考えて組み立てる」事ができるようになれば、AIが生成するコードの要不要、問題に気づいて、適切に対処が可能になります。 これはホビーであれ実務であれ必要な事です。
第1章 盤面を作ろう
1-1. 完成イメージ
※これは仮イメージ
GUIで見栄えのいいものは後程作りましょう。基礎が無ければ作れませんから。
1-2. オセロの盤面をどう表現する?
オセロでは、盤面上のどこに駒が置かれているか等を管理する必要があります。
今回は、二次元配列を使って盤面を表現します。
配列の x行、y列、を盤面の X座標、Y座標 のマスの状態とする事で、「どこに何が置かれているか」を管理します。
配列
プログラムでは、大量のデータを扱う事があります。
例えばオセロでは、
- どこに石が置かれているか
- 空いている場所はどこか
- 黒い駒なのか白い駒なのか
といった情報を盤面全体について管理する必要があります。
このような「同じ種類のデータをまとめて管理する」ために使われるのが 配列 です。
Pythonでは、配列に近いものとして list を利用できます。
例えば以下のコードでは、数字を複数まとめて管理しています。
- sample101.py
numbers = [10, 20, 30, 40] print(numbers[0]) print(numbers[1])
実行結果:
10 20
[0] や [1] の数字を 添字(index) と呼びます。
Pythonでは添字は 0 から始まります。numbersの添字と値の関係は以下のようになります。
| 添字 | 値 |
|---|---|
| 0 | 10 |
| 1 | 20 |
| 2 | 30 |
| 3 | 40 |
オセロではさらに「縦」と「横」の情報を持つ必要があります。
そこで利用するのが 二次元配列 です。
- sample102.py
board = [ [0,0,0], [0,0,0], [0,0,0] ]
これは以下のようなイメージになります。
0 0 0 0 0 0 0 0 0
二次元配列では、
- 横方向 = X座標
- 縦方向 = Y座標
のように扱えます。
例えば:
- sample103.py
board[1][2] = 5 print(board[1][2])
実行結果:
5
盤面を表現する配列
ここで作るオセロでは、二次元配列を使って、
- 空ているマス
- 黒石の置かれたマス
- 白石の置かれたマス
を管理していきます。さっそく、まだ駒の置かれていない盤面を表現する二次元配列を定義してみましょう。
- sample004.py
board = [ [9,9,9,9,9,9,9,9,9,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,9,9,9,9,9,9,9,9,9], ]
盤外のマスが上下左右にあるので二次元配列は10×10のサイズで、盤外で囲まれた内側が8×8の、白黒の駒が置かれる面となります。
マスの状態を以下としています。
- 0 = 空ているマス
- 1 = 黒石の置かれたマス
- 2 = 白石の置かれたマス
- 9 = 盤外
しかし、この方法では 「この数字は何を意味しているのか」 が分かりにくくなります。
そこで利用するのが Enum (列挙型)です。
Enumを使うと、 値に意味のある名前を付けられます。
from enum import IntEnum class Cells(IntEnum): BLANK = 0 BLACK_CHIP = 1 WHITE_CHIP = 2 WALL = 9
これにより、
board[x][y] = Cells.BLACK_CHIP
のように、 「意味のある名称」で操作できるようになりプログラムの可読性が上がります。
1-3. 盤面を表示する
単純にコンソールへテキストで表示させてみましょう。
- sample005.py
from enum import IntEnum class Cells(IntEnum): BLANK = 0 BLACK_CHIP = 1 WHITE_CHIP = 2 WALL = 9 board = [ [9,9,9,9,9,9,9,9,9,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,1,2,0,0,0,9], [9,0,0,0,2,1,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,9,9,9,9,9,9,9,9,9], ] ## Display game board for y in range(0, 10): for x in range(0, 10): if board[x][y] == Cells.BLANK: print("・", end="") elif board[x][y] == Cells.BLACK_CHIP: print("●", end="") elif board[x][y] == Cells.WHITE_CHIP: print("○", end="") elif board[x][y] == Cells.WALL: print("■", end="") print()
range(0, 10) は開始の値をゼロ(0)として、10個の連続した値、つまり0~9の10個要素を持つ一次元配列を返します。
print()で文字列を表示すると改行されてしまうのですが end=“” を指定して改行を““に変更し改行を抑止しています。
- sample006.py
from enum import IntEnum class Cells(IntEnum): BLANK = 0 BLACK_CHIP = 1 WHITE_CHIP = 2 WALL = 9 board = [ [9,9,9,9,9,9,9,9,9,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,1,2,0,0,0,9], [9,0,0,0,2,1,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,9,9,9,9,9,9,9,9,9], ] ## Display game board for y in [0,1,2,3,4,5,6,7,8,9]: ## ← range(0, 10)と等価 for x in [0,1,2,3,4,5,6,7,8,9]: ## ← range(0, 10)と等価 if board[x][y] == Cells.BLANK: print("・", end="") elif board[x][y] == Cells.BLACK_CHIP: print("●", end="") elif board[x][y] == Cells.WHITE_CHIP: print("○", end="") elif board[x][y] == Cells.WALL: print("■", end="") print()
この記述と等価です。
実行結果はこうなります。
今は二次元配列の内容をテキストで表示させているけど、GUIで作る時には Cells.xxx 毎に表示するグラフィクスを選択する感じになりますね。
第2章 オセロのルールを実装しよう
2-1. ルールの簡単なおさらい
こちら 日本オセロ連盟競技ルール 等でルールの確認をしても良いですが、ほとんどの人は知っているんじゃないかと思いますので簡単に。
使用する駒は白黒の円盤を重ね合わせてあり、先手は黒の面、後手は白の面を使います。
盤面は8x8のマスでゲーム開始時には中央の4マスに4個の駒を置くので、置けるマスは60箇所になります。
- 相手の駒がないマスに自分の駒を置く事ができる。
- 自分の駒で空きマスが無い状態で相手の駒を挟む事で、挟まれた駒を裏返し自分の色に変更できる。
- 自分の手番で相手の駒を裏返す事ができるマスがある場合、必ず駒を置かねばならない。
- 自分の手番で相手の駒を裏返す事ができるマスがない場合、相手の手番になる。
- 先手と後手の双方が相手の駒を裏返す事が出来なくなった場合、ゲーム終了になる。
- 盤面の駒の色を数え、多い駒色の方、例えば黒が多ければ先手の勝ちとなる。
改めて説明するまでもない感じですが。
2-2. 相手の駒を裏返すとは?
もうわかりきっているとは思いますが確認です。以下は先手と後手の手番が1回ずつ廻った状態の例です。
矢印が指すマスが駒を置けるマスで、各々が水色の矢印で示すマスに自分の駒を置いて相手の駒を裏返して自分の駒の色にしていきます。
自分の駒を置いた場所から縦・横・斜め隣の駒が相手の駒で、1回以上連続して並び自分の駒まで達する場合、その間の駒を裏返して自分の駒の色にすることができます。
もう少し条件をはっきりさせるために、右方向に盤面を辿る事を考えてみます。
これは、相手の駒を裏返す例です。置かれた場所から右方向に辿ります。辿る条件は「隣が相手の駒の時」です。相手の駒だった時はさらにその隣を辿ります。そして自分の駒に辿り着いた時、今度は逆方向に戻りながらマスの駒を裏返して自分の色にしてしまいます。
これは、相手の駒を裏返す事ができない例です。裏返せないという事は駒を置く事ができない、となります。
- 上段は、辿っていったら盤外になってしまった例です。
- 中段は、辿っていったら空きのマスが存在していた例です。
- 下段は、辿ろうとしたら隣のマスが自分の駒だった例です。
これは右方向へ辿る例です。実際には左方向、上下方向、斜め方向、計8方向に対して同じ確認を行います。そして、8方向を確認して一切相手の駒を裏返す事ができなかったら、そのマスには駒を置く事ができません。
この確認手順を実装してみましょう。posXとposYに盤面の場所、mycolorに盤面に置く駒、を指定します。
- sample100.py
from enum import IntEnum class Cells(IntEnum): BLANK = 0 BLACK_CHIP = 1 WHITE_CHIP = 2 WALL = 9 posX = 3 posY = 5 mycolor = Cells.BLACK_CHIP board = [ [9,9,9,9,9,9,9,9,9,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,1,2,0,0,0,9], [9,0,0,0,2,1,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,0,0,0,0,0,0,0,0,9], [9,9,9,9,9,9,9,9,9,9], ] searchVectors = [ [ 0, -1], ## upper [ 1, -1], ## upper right [ 1, 0], ## right [ 1, 1], ## lower right [ 0, 1], ## lower [-1, 1], ## lower left [-1, 0], ## left [-1, -1] ## upper left ] print(f"put location: {posX}, {posY}") ## 8-direction search and piece reverseing process counter = 0 for sv in searchVectors: locatorX = posX + sv[0] locatorY = posY + sv[1] opponent = Cells.WHITE_CHIP if mycolor == Cells.BLACK_CHIP else Cells.BLACK_CHIP ## If the opponent's cells continue localCounter = 0 while board[locatorX][locatorY] == opponent : locatorX += sv[0] locatorY += sv[1] localCounter += 1 ## sandwiched by pieces if (board[locatorX][locatorY] == mycolor) and (localCounter != 0) : locatorX -= sv[0] locatorY -= sv[1] ## Reverse over your opponent's pieces counter += localCounter while (locatorX != posX) or (locatorY != posY) : board[locatorX][locatorY] = mycolor locatorX -= sv[0] locatorY -= sv[1] if counter != 0 : board[posX][posY] = mycolor ## Display game board print(f"Reversed: {counter}") for y in range(0, 10): for x in range(0, 10): if board[x][y] == Cells.BLANK: print("・", end="") elif board[x][y] == Cells.BLACK_CHIP: print("●", end="") elif board[x][y] == Cells.WHITE_CHIP: print("○", end="") elif board[x][y] == Cells.WALL: print("■", end="") print()
先手(BLACK_CHIP)で
- posX,posy = 3,5 の時

- posX,posy = 5,3 の時

- posX,posy = 4,6 の時

- posX,posy = 6,4 の時

2-3. 8方向検索
このサンプルでは8方向を検索する為に以下の配列を定義して使っています。
searchVectors = [ [ 0, -1], ## upper [ 1, -1], ## upper right [ 1, 0], ## right [ 1, 1], ## lower right [ 0, 1], ## lower [-1, 1], ## lower left [-1, 0], ## left [-1, -1] ## upper left ]
この値は座標に足し込むための増分をx,yの組で持っている配列です。
この増分を盤面の位置(座標)に足し込む事で、指定の方向に盤面上のマスの位置を1マスずつ進めたり、減ずる事で元に戻る事ができるようになります。
例えば盤面のマス(横5,縦3)の位置から、右方向に進めるときは[ 1, 0]の組の値を足し込みます。
| 盤面X位置 | 盤面Y位置 | 進んだ盤面のX位置 | 進んだ盤面のY位置 |
|---|---|---|---|
| 5 | 3 | 5 + 1 = 6 | 3 + 0 = 3 |
| 6 | 3 | 6 + 1 = 7 | 3 + 0 = 3 |
| 7 | 3 | 7 + 1 = 8 | 3 + 0 = 3 |
例えば盤面のマス(横6,縦4)の位置から、左下方向に進めるときは[-1, 1]の組の値を足し込みます。
| 盤面X位置 | 盤面Y位置 | 進んだ盤面のX位置 | 進んだ盤面のY位置 |
|---|---|---|---|
| 6 | 4 | 6 + (-1) = 5 | 4 + 1 = 5 |
| 5 | 5 | 5 + (-1) = 4 | 5 + 1 = 6 |
| 4 | 6 | 4 + (-1) = 3 | 6 + 1 = 7 |
| 3 | 7 | 3 + (-1) = 2 | 7 + 1 = 8 |
元に戻る時は逆にこの値を減じていきます。posX, posY の値になるまで減じ続ければ元の位置に戻った事になります。
この事を頭に入れてコードを読み進めます。
## 8-direction search and piece reverseing process counter = 0 for sv in searchVectors: locatorX = posX + sv[0] locatorY = posY + sv[1] opponent = Cells.WHITE_CHIP if mycolor == Cells.BLACK_CHIP else Cells.BLACK_CHIP ## If the opponent's cells continue localCounter = 0 while board[locatorX][locatorY] == opponent : locatorX += sv[0] locatorY += sv[1] localCounter += 1 ## sandwiched by pieces if (board[locatorX][locatorY] == mycolor) and (localCounter != 0) : locatorX -= sv[0] locatorY -= sv[1] ## Reverse over your opponent's pieces counter += localCounter while (locatorX != posX) or (locatorY != posY) : board[locatorX][locatorY] = mycolor locatorX -= sv[0] locatorY -= sv[1] if counter != 0 : board[posX][posY] = mycolor
このfor文は、searchVectorsに定義されている8個の組を一つずつ取り出しています。つまりループで8方向の処理を実施しようとしています。
for sv in searchVectors: locatorX = posX + sv[0] locatorY = posY + sv[1] opponent = Cells.WHITE_CHIP if mycolor == Cells.BLACK_CHIP else Cells.BLACK_CHIP
指定した盤面の位置にさっそく組の値を足し込んでいます。1つ位置を進めた状態がlocatorX, locatorYに格納されました。
locatorX = posX + sv[0] locatorY = posY + sv[1]
以下は相手の駒を決定しているだけです。
mycolor が自分の駒の色になるので、mycolorが Cells.BLACK_CHIP だったら相手は Cells.WHITE_CHIP、mycolor が Cells.WHITE_CHIP だったら相手は Cells.BLACK_CHIP、になります。
opponent = Cells.WHITE_CHIP if mycolor == Cells.BLACK_CHIP else Cells.BLACK_CHIP
このwhile文は、相手の駒が隣にある間、locatorX, locatorYを更新し続けます。相手の駒ではない時(自分の駒、空のマス、盤外)にループを抜けます。localCounterは相手の駒の個数になりますね。
localCounter = 0 while board[locatorX][locatorY] == opponent : locatorX += sv[0] locatorY += sv[1] localCounter += 1
先のループが終了した時、そこにあるのが自分の駒で、そこに至るまでに相手の駒だけが存在していたのであれば駒の置き直し(裏返し)を行います。
locatorX, locatorYから組の値を減じているのは、現在の位置には自分の駒があるので、その手前、つまり相手の駒の並びの終端に位置付けする為です。
if (board[locatorX][locatorY] == mycolor) and (localCounter != 0) : locatorX -= sv[0] locatorY -= sv[1]
ひたすら元の位置に戻りながら駒の置き直し(裏返し)を行います。
locatorX, locatorY が posX, posY と同じ値となったら置き換えは終了です。
counter += localCounter while (locatorX != posX) or (locatorY != posY) : board[locatorX][locatorY] = mycolor locatorX -= sv[0] locatorY -= sv[1]
最後にposX, posYの盤面の位置へ自分の駒を置きます。これで盤面の書換えが完了です。
if counter != 0 : board[posX][posY] = mycolor
2-6. パス判定とゲーム終了
- 置ける場所探索
- 全探索
- 終了条件
第3章 ゲームを設計しよう
動くコードから設計へ
3-1. なぜ分離が必要なのか
- UI
- ルール
- CPU思考
3-2. 盤面クラスを作る
- 状態の責務
- メソッド化
3-3. 審判ロジック
- ルール管理
- ターン管理
- 勝敗判定
3-4. UIを交換可能にする
- コンソールUI
- GUI化可能設計
- “表示が変わってもゲームは同じ”
3-5. CPUプレイヤーの分離
- 人間プレイヤー
- CPUプレイヤー
- インターフェース的発想
第4章 CPUに考えさせよう
ゲームロジックの入り口
4-1. 最初のCPU
- 最初に置ける場所へ置く
- なぜ弱いのか
4-2. 最も多く取れる場所
- 評価値
- スコアリング
4-3. thinkData の導入
- 角優先
- 危険マス
- ヒューリスティック
4-4. データ構造で知能を表現する
- 優先順位テーブル
- ルールベース思考
4-5. “強そう”に見える理由
- 人間の印象
- 完全探索しない設計
第5章 CPUに個性を与えよう
強さだけでは面白くならない
5-1. 同じ手ばかり打つCPU
- 固定順の問題
- パターン化
5-2. 行動に揺らぎを与える
- ランダム化
- 候補シャッフル
5-3. 性格を作る
- 攻撃型
- 守備型
- 気まぐれ型
5-4. “強さ”と“面白さ”
- 最善手だけが正義ではない
- ユーザー体験
5-5. ゲームのCPUらしさとは
- 人間っぽさ
- 読みにくさ
- 演出
第6章 現代AIへ繋げる
手作りロジックから学習型へ
6-1. thinkData は何をしている?
- 特徴量
- 人間の知識
6-2. 評価関数とは
- 盤面評価
- 点数化
6-3. minimax法
- 先読み
- 相手視点
6-4. αβ枝刈り
- 探索削減
- 実用化
6-5. 機械学習との違い
- 人間が特徴を書く時代
- 学習で獲得する時代
6-6. 現代AIとの接点
- NN
- AlphaGo/AlphaZero 的発想
- “延長線上”としてのAI
第7章 ルールを拡張してみよう
ゲームエンジン化への第一歩
7-1. 仕様変更はなぜ難しい?
- ベタ書き実装の限界
7-2. 白黒半分の駒
- ターン依存の意味
- 動的属性
7-3. 曲がる駒
- 探索方向変更
- 状態付き探索
7-4. 特殊効果を一般化する
- 駒の振る舞い
- ルールオブジェクト
7-5. “変更に強い設計”とは
- 抽象化
- 責務分離
- 拡張性
7-6. 自分だけのオセロを作ろう
- 課題
- 改造アイデア
- 発展テーマ
