Edited at

C言語でオブジェクト指向:「○✕ゲーム」をリファクタリングし、Pythonにも移植させていただいた


きっかけ

@kei011 さんの「○✕ゲーム」を拝見しました。

初心者の目的は自分で考えながら動くプログラムを完成させることなので、それを達成し、その成果を投稿したことは素晴らしいことです。

ただ、やはり、初心者にありがちなプログラムコードになっているのが残念に思いました。


  • 同じような処理を繰り返して書く

  • 意味が伝わらない変数名を付ける

  • 変数名を省略して書く

  • 1関数内の行数が長い

  • 処理の流れを把握しにくい


リファクタリング

オブジェクト指向を意識しつつリファクタリングさせていただきました。

私自身も試行錯誤しながらリファクタリングしている状況なので、他にも良いアイデアなどありましたらコメントいただけたると有難いです。


  • 意味の伝わる変数名にする

  • 変数名は省略しない

  • 1関数は25行以内にする

  • intやcharなどの基本型を役割名に型定義(typedef)してから使う

まずは、「○✕ゲーム」に登場する物・役者をリストアップします。


  • プレイヤー:ユーザ(人)、コンピューター

  • ゲーム盤(○✕表)

  • 石(○と✕)

それぞれをtypedefします。


  • 石はstone_t、char型

  • ゲーム盤はboard_t、石の2次元配列

  • プレイヤーはplayer_t、名前、石、先行・後攻、戦略など複数要素を持つ構造体

ほかにも、文字列string_t、石を置く位置point_tをtypedefしました。


C言語ソースコード

リファクタリングしたC言語ソースコードを示します。


tictactoe.c

#include <stdio.h>

#include <stdlib.h>
#include <stdbool.h>
#include <time.h>

#ifdef __GNUC__
#define scanf_s scanf
#endif

#define SIZE (3) // 盤サイズ(縦=横)
typedef enum {
SPACE = '-', // 盤に石がないときの値
STONE_A = 'A',
STONE_B = 'B',
} stone_t; // 盤に置く石
typedef stone_t board_t[SIZE][SIZE]; // 盤
typedef int point_t; // 盤のxy位置 (0 ~ STEP-1)
typedef int step_t; // 更新差分
typedef const char *string_t; // 文字列

typedef struct player player_t; // プレイヤー(人とコンピュータ)
struct player {
string_t name;
stone_t stone;
string_t order;
void (*play)(player_t *player, board_t board);
player_t *opponent;
};

// 盤表示
void show(board_t board)
{
printf("★現在の表★\n"
"\n x");
for (point_t x = 0; x < SIZE; x++) {
printf(" %2d", x);
}
printf("\n y -");
for (point_t x = 0; x < SIZE; x++) {
printf("---");
}
for (point_t y = 0; y < SIZE; y++) {
printf("\n%2d| ", y);
for (point_t x = 0; x < SIZE; x++) {
printf(" %c ", board[y][x]);
}
}
printf("\n");
}

// 盤にあいてる場所があれば真
bool space(board_t board)
{
for (point_t y = 0; y < SIZE; y++) {
for (point_t x = 0; x < SIZE; x++) {
if (board[y][x] == SPACE) {
return true;
}
}
}
return false;
}

// 指定した石が盤上に後続して列が完成していたら真を返す
bool follow(board_t board, stone_t stone, point_t y, point_t x, step_t dy, step_t dx)
{
for (step_t i = 1; i < SIZE; i++) {
y = (y + dy + SIZE) % SIZE;
x = (x + dx + SIZE) % SIZE;
if (board[y][x] != stone) {
return false;
}
}
return true;
}

// 指定した石で盤上に列が完成したら *py, *px を設定して真を返す
bool line(board_t board, stone_t first, stone_t other, point_t *py, point_t *px) {
const step_t INCRESE_Y = 1, INCRESE_X = 1, DECRESE_X = -1, STAY_Y = 0, STAY_X = 0;
for (point_t y = 0; y <SIZE ; y++) {
for (point_t x = 0; x < SIZE; x++) {
if (board[y][x] == first &&
(follow(board, other, y, x, STAY_Y, INCRESE_X) || // 横
follow(board, other, y, x, INCRESE_Y, STAY_X))) { // 縦
*py = y, *px = x;
return true;
}
}
point_t x = y;
if (board[y][x] == first &&
follow(board, other, y, x, INCRESE_Y, INCRESE_X)) { //右下並び
*py = y, *px = y;
return true;
}
x = SIZE - 1 - y;
if (board[y][x] == first &&
follow(board, other, y, x, INCRESE_Y, DECRESE_X)) { // 左下並び
*py = y, *px = x;
return true;
}
}
return false;
}

// リーチ状態なら *px, *py を設定して真を返す
bool reach(board_t board, stone_t stone, point_t *py, point_t *px)
{
return line(board, SPACE, stone, py, px);
}

// ビンゴ状態なら真を返す
bool bingo(board_t board, stone_t stone)
{
point_t y, x;
return line(board, stone, stone, &y, &x);
}

// 位置入力
point_t input(player_t *player, string_t target)
{
printf("%s:%s(%c)の%sを入力: ", player->order, player->name, player->stone, target);
point_t point;
switch (scanf_s("%d", &point)) {
case EOF:
exit(1);
case 1:
if (0 <= point && point < SIZE) {
return point;
}
break;
default:
scanf("%*s"); // 入力破棄
}
printf("値が正しくありません。\n\n");
return -1;
}

// 人がプレイする(一手打つ)
void human(player_t *player, board_t board)
{
while (true) {
point_t x = input(player, "横(x)");
if (x < 0) continue;
point_t y = input(player, "縦(y)");
if (y < 0) continue;
if (board[y][x] == SPACE) {
board[y][x] = player->stone;
return;
}
printf("指定の位置が正しくありません。\n\n");
}
}

// コンピュータがプレイする(一手打つ)
void computer(player_t *player, board_t board)
{
point_t y, x;
if (reach(board, player->stone, &y, &x)) {
// 自分のリーチを選択
} else if (reach(board, player->opponent->stone, &y, &x)) {
// 相手のリーチを妨害
} else {
y = 1, x = 1; // 真ん中
while (board[y][x] != SPACE) {
y = rand() % SIZE, x = 0;
while (x < SIZE - 1 && board[y][x] != SPACE) {
x++;
}
}
}
board[y][x] = player->stone;
}

// SIZE目並べゲーム
void game()
{
player_t player1 = { "あなた", STONE_A, "先攻", human },
player2 = { "コンピュータ", STONE_B, "後攻", computer },
*player = &player1;
player1.opponent = &player2;
player2.opponent = &player1;
board_t board;
for (point_t y = 0; y < SIZE; y++) {
for (point_t x = 0; x < SIZE; x++) {
board[y][x] = SPACE;
}
}
show(board);
while (space(board)) {
player->play(player, board);
show(board);
if (bingo(board, player->stone)) {
printf("%sの勝利!\n", player->name);
return;
}
player = player->opponent;
}
printf("引き分け!\n");
}

int main(void)
{
srand((unsigned int)time(NULL));

game();

return 0;
}


「盤さん、どっかあいてます?」

「盤さん、この石、三つ並んでます?」

「あなた、一手打ってください」

「コンピュータよ、一手打ちたまえ」

のように、「誰」に対してお願いしているかを考えて、その「誰」を変数名にし、関数の第一引数にしています。


Python移植

C言語ソースコードを、オブジェクト指向言語のPythonに移植してみました。C言語が読めるならPythonもそれなりに読めると思います。

「誰」をクラス(class)として定義しています。

各関数が「誰」の仕事かを考えてクラスに振り分けています。

クラス内部から見て「誰」は「自分自身」なので、第一引数をselfという変数名にしています。

クラスの中の関数を呼びたいときは、「誰.関数名(引数)」の順番で書くと、「対応クラスの中の関数(誰, 引数)」に変換されて呼び出されます。なので、selfを第一引数にします。

Python3 のインタープリタがあれば、コマンドプロンプトで python tictactoe.py のように入力して実行できます。

C言語の書き方をそのままにしておきたかったので、Pythonらしい書き方はできるだけ避けています。


tictactoe.py

from random import randint

SPACE = '-' # 盤に石がないときの値

class Board(list): # typedef char board_t[3][3]; (listは配列)

# 盤初期化(Pythonで決められている特殊な名前)
def __init__(self):
super().__init__([ [ SPACE, SPACE, SPACE ],
[ SPACE, SPACE, SPACE ],
[ SPACE, SPACE, SPACE ] ])

# 盤表示
def show(self): # void show(board_t board)
print("★現在の表★\n\n"
" x 0 1 2\n"
" y -------")
for y in range(3):
print(" %d| %c %c %c" % (y, self[y][0], self[y][1], self[y][2]))

# 盤にあいてる場所があれば真を返す
def space(self): # bool_t space(board_t board)
for y in range(3):
for x in range(3):
if self[y][x] == SPACE:
return True
return False

# 指定した三石が盤上に並んでいたら座標を返す、並んでいないならNone
def line(self, stone1, stone2, stone3): # Pythonでは復帰値で複数の値を返せるの>で引数削減
for y in range(3):
for x in range(3):
if (self[y][x] == stone1 and
((self[y][(x + 1) % 3] == stone2 and # 横並び
self[y][(x + 2) % 3] == stone3) or
(self[(y + 1) % 3][x] == stone2 and # 縦並び
self[(y + 2) % 3][x] == stone3))):
return (y, x)
if (self[(y + 0) % 3][(y + 0) % 3] == stone1 and # 右下並び
self[(y + 1) % 3][(y + 1) % 3] == stone2 and
self[(y + 2) % 3][(y + 2) % 3] == stone3):
return (y, y)
if (self[(y + 0) % 3][(2 - y + 3) % 3] == stone1 and # 左下並び
self[(y + 1) % 3][(1 - y + 3) % 3] == stone2 and
self[(y + 2) % 3][(0 - y + 3) % 3] == stone3):
return (y, (2 - y + 3) % 3)
return None

# リーチ状態なら位置を返す、違うならNone
def reach(self, stone):
return self.line(SPACE, stone, stone)

# ビンゴ状態なら真を返す
def bingo(self, stone):
return self.line(stone, stone, stone) is not None

class Player:

def __init__(self, name, stone, order):
self.name = name
self.stone = stone
self.order = order

class Human(Player):

# 位置入力
def input(self, target):
try:
point = int(input("%s:%s(%c)の%sを入力: " % (self.order, self.name, self.stone, target)))
if 0 <= point <= 2:
return point
except ValueError:
pass
print("値が正しくありません。\n")
return -1

# プレイする(一手打つ)
def play(self, board):
while True:
x = self.input("横(x)")
if x < 0: continue
y = self.input("縦(y)")
if y < 0: continue
if board[y][x] == SPACE:
board[y][x] = self.stone
return
print("指定の位置が正しくありません。\n")

class Computer(Player):

# プレイする(一手打つ)
def play(self, board):
position = board.reach(self.stone) # 自分のリーチ選択
if position is None:
position = board.reach(self.opponent.stone) # 相手のリーチ妨害
if position is None:
y, x = 1, 1 # 真ん中
while board[y][x] != SPACE:
y, x = randint(0, 2), 0
while x < 2 and board[y][x] != SPACE:
x += 1
position = (y, x)
y, x = position
board[y][x] = self.stone

# 三目並べゲーム
def main():
player1 = Human("あなた", 'A', "先攻")
player2 = Computer("コンピュータ", 'B', "後攻")
player = player1
player1.opponent = player2
player2.opponent = player1
board = Board()
board.show()
while board.space():
player.play(board)
board.show()
if board.bingo(player.stone):
print("%sの勝利!" % player.name)
return
player = player.opponent
print("引き分け!")
return

if __name__ == '__main__':
main()