M.Hiroi's Home Page
http://www.geocities.jp/m_hiroi/

Puzzle DE Programming

三目並べ

[ Home | Puzzle ]

問題の説明

三目並べは、皆さんお馴染みの二人で対戦するゲームです。ひとりが○側でもうひとりが×側を受け持ち、3 行 3 列のマス目に○×を書いて、3 つ並べた方が勝ちというゲームです。

 ┌─┬─┬─┐ 
 │×│○│○│ 
 ├─┼─┼─┤ 
 │○│○│×│ 
 ├─┼─┼─┤ 
 │×│×│○│ 
 └─┴─┴─┘ 

  図:三目並べ

上図は○側が先手で引き分けになった例です。三目並べは、両者が最善を尽くすと引き分けになることが知られています。本当に引き分けになるのか、プログラムを作って確かめてみましょう。

三目並べは簡単なゲームなので、ゲーム終了まで読み切ることができます。局面の状態は、○側の勝ち、×側の勝ち、引き分けの 3 通りしかありません。あとは、ミニマックス法により最善手を選択させ、その結果を求めればいいわけです。

とりあえず、プログラムでは指し手を保存しないで、評価値の結果だけを出力することにします。初手をどこに選んでも、引き分けの評価値が出力されるはずです。


●プログラム

それではプログラムを作ります。最初にマクロとグローバル変数を定義します。

 ┌─┬─┬─┐ 
 │0│1│2│ 
 ├─┼─┼─┤ 
 │3│4│5│ 
 ├─┼─┼─┤ 
 │6│7│8│ 
 └─┴─┴─┘ 

 図:盤面の番号
リスト:マクロの定義

#define SIZE  9
#define FREE  0
#define MARU  1
#define BATU (-1)
#define LINE  8
#define MARU_WIN 1
#define BATU_WIN (-1)
#define DRAW  0
#define MAX_VALUE 2
#define MIN_VALUE (-2) 
リスト:グローバル変数の定義

/* 盤面 */
char board[SIZE];

/* 直線 */
const char line[LINE][3] = {
  0, 1, 2,   3, 4, 5,   6, 7, 8,
  0, 3, 6,   1, 4, 7,   2, 5, 8,
  0, 4, 8,   2, 4, 6,
};

盤面は 1 次元配列 board で表します。盤面の位置と配列の添字の対応は、左図のように定義します。すると、駒が 3 つ並ぶ直線は配列 line で表すことができます。勝敗を判定するプログラムは、line を使えば簡単にプログラムできます。

リスト:勝敗の判定

int check_winner( void )
{
  int i;
  for( i = 0; i < LINE; i++ ){
    int piece = board[ line[i][0] ];
    if( piece != FREE && piece == board[ line[i][1] ] && piece == board[ line[i][2] ] ){
      return (piece == MARU ? MARU_WIN : BATU_WIN);
    }
  }
  return DRAW;
}

check_winner は 8 本の直線を調べ、同じ駒が 3 つ並んでいないか調べます。直線の最初の位置に MARU か BATU があり、残り 2 つの場所に同じ駒があれば 3 つ並んでいることがわかります。MARU が 3 つ並んでいれば評価値 MARU_WIN (1) を、BATU であれば BATU_WIN (-1) を返します。MARU も BATU も 3 つ並んでいなければ、DRAW (0) を返します。

ミニマックス法を使う場合、局面の評価値が必要になります。局面から評価値を計算する関数を「評価関数」といいます。評価関数は、先手が有利 (後手が不利) な局面ほど大きな値 (正の値)、逆に後手が有利 (先手が不利) なほど小さな値 (負の値)、互角の場合は 0 になるように作るのが一般的です。

三目並べの場合、ゲーム終了まで読み切ることができるので、評価値は、勝ち、負け、引き分けの 3 つでいいわけです。ただし、この関数はゲームの途中でも DRAW を返しますので、引き分けの判定には注意が必要です。これはあとで説明します。

次は、先手 (○) の指し手を決める think_maru を作ります。プログラムは次のようになります。

リスト:先手の指し手

int think_maru( int n )
{
  int i, v, value = MIN_VALUE;
  for( i = 0; i < SIZE; i++ ){
    if( board[i] != FREE ) continue;
    /* MARU を書く */
    board[i] = MARU;
    /* 勝敗の判定 */
    v = check_winner();
    /* 決着していなければ相手の手番へ */
    if( v == DRAW && n < SIZE - 1 ) v = think_batu( n + 1 );
    /* ミニマックス法 */
    if( v > value ) value = v;
    /* 元に戻す */
    board[i] = FREE;
  }
  return value;
}

think_maru は先手が有利になる指し手、つまり、評価値が大きい手を選べばいいわけです。引数 n は手数を表します。選んだ指し手の評価値は value に格納します。MIN_VALUE は BATU_WIN よりも小さな値にします。この値は、まだ指し手を選んでいないことを表します。

次に、盤面 board を調べて、空き場所に MARU を書き込みます。そして、関数 check_winner を呼び出して、勝敗の判定を行います。もしも、評価値 v が DRAW の場合、盤面にまだ駒が置ける状態であれば、ゲームは終了していません。盤面には n + 1 個の駒があるので、n < SIZE - 1 であればゲーム続行、そうでなければ引き分けです。ゲームを続行する場合は、関数 think_batu を呼び出して手番を相手に移します。

ミニマックス法による指し手の決定は簡単です。先手は評価値が大きい手を選べばいいのですから、v が value よりも大きな値であれば、それを指し手として選びます。value は MIN_VALUE に初期化されているので、最初に調べた指し手は必ず選ばれることになります。そして、盤面 board を元の値に戻して、次の指し手を調べます。最後に、選んだ指し手の評価値 value を返します。

次は、後手 (×) の指し手を決める think_batu を作ります。プログラムは次のようになります。

リスト:後手の指し手

int think_batu( int n )
{
  int i, v, value = MAX_VALUE;
  for( i = 0; i < SIZE; i++ ){
    if( board[i] != FREE ) continue;
    /* BATU を書く */
    board[i] = BATU;
    /* 勝敗の判定 */
    v = check_winner();
    /* 決着していなければ相手の手番へ */
    if( v == DRAW && n < SIZE - 1 ) v = think_maru( n + 1 );
    /* ミニマックス法 */
    if( v < value ) value = v;
    /* 元に戻す */
    board[i] = FREE;
  }
  return value;
}

関数 think_batu は think_maru とは逆に、後手が有利になる指し手 (評価値が小さな手) を選びます。value は MARU_WIN よりも大きな値 MAX_VALUE に初期化しておきます。そして、ミニマックス法では、v が value よりも小さければ、それを選べばいいわけです。ミニマックス法のプログラムはこれだけです。

最後にメインルーチンを作ります。

リスト:メインルーチン

int main()
{
  int v, i, j;
  for( i = 0; i < SIZE; i++ ){
    memset( board, FREE, SIZE );    /* 初期化 */
    board[i] = MARU;                /* 初手 */
    v = think_batu( 1 );            /* 相手の手番 */
    printf("初手 %d: 評価値 %d\n", i, v );
  }
  return 0;
}

最初に memset で board を FREE (0) に初期化します。そして、初手を指してから、think_batu を呼び出して手番を相手に移します。この返り値が勝敗の結果を表しているので、それを出力するだけです。

●実行結果

それでは実行結果を示しましょう。

初手 0: 評価値 0
初手 1: 評価値 0
初手 2: 評価値 0
初手 3: 評価値 0
初手 4: 評価値 0
初手 5: 評価値 0
初手 6: 評価値 0
初手 7: 評価値 0
初手 8: 評価値 0

初手がどこでも、結果は引き分けとなりました。これで、両者が最善を尽くすと引き分けになることが確かめられました。

それでは、ルールを「3 つ並べた方が負け」に変更すると、結果はどうなるでしょうか。プログラムは簡単に改造できます。関数 check_winner では、MARU が 3 つ並んでいたら MARU_WIN を出力していましたが、これを BATU_WIN に変更します。逆に、BATU が 3 つ並んでいたら MARU_WIN を出力します。ようするに、勝敗の判定を逆にするだけです。

このルールでは先手が不利なように思いますが、それでも引き分けになるのでしょうか。さっそく実行してみましょう。

初手 0: 評価値 -1
初手 1: 評価値 -1
初手 2: 評価値 -1
初手 3: 評価値 -1
初手 4: 評価値 0
初手 5: 評価値 -1
初手 6: 評価値 -1
初手 7: 評価値 -1
初手 8: 評価値 -1

初手が中央の場合のみ引き分けで、あとは後手の勝ちとなりました。後手必勝にはなりませんでしたね。興味のある方は、実際にプレイして確かめてみてください。また、今回のプログラムは評価値を出力するだけでしたが、指し手を表示するように改造してみるのも面白いと思います。


●プログラムリスト

/*
 * tictactoe.c : 三目並べ
 *
 *               Copyright (C) 2001 Makoto Hiroi
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SIZE  9
#define FREE  0
#define MARU  1
#define BATU  2
#define LINE  8
#define MARU_WIN 1
#define BATU_WIN (-1)
#define DRAW  0
#define MAX_VALUE 2
#define MIN_VALUE (-2)

/* 関数宣言 */
int think_maru( int n );
int think_batu( int n );

/* 直線 */
const char line[LINE][3] = {
  0, 1, 2,  3, 4, 5,  6, 7, 8,
  0, 3, 6,  1, 4, 7,  2, 5, 8,
  0, 4, 8,  2, 4, 6,
};

char board[SIZE];  /* 盤面 */


/* 勝敗の判定 */
int check_winner( void )
{
  int i;
  for( i = 0; i < LINE; i++ ){
    int piece = board[ line[i][0] ];
    if( piece != FREE && piece == board[ line[i][1] ] && piece == board[ line[i][2] ] ){
      return (piece == MARU ? MARU_WIN : BATU_WIN);
    }
  }
  return 0;
}

/* 先手 */
int think_maru( int n )
{
  int i, v, value = MIN_VALUE;
  for( i = 0; i < SIZE; i++ ){
    if( board[i] != FREE ) continue;
    /* MARU を書く */
    board[i] = MARU;
    /* 勝敗の判定 */
    v = check_winner();
    /* 決着していなければ相手の手番へ */
    if( v == DRAW && n < SIZE - 1 ) v = think_batu( n + 1 );
    /* ミニマックス */
    if( v > value ) value = v;
    /* 元に戻す */
    board[i] = FREE;
  }
  return value;
}

/* 後手 */
int think_batu( int n )
{
  int i, v, value = MAX_VALUE;
  for( i = 0; i < SIZE; i++ ){
    if( board[i] != FREE ) continue;
    /* BATU を書く */
    board[i] = BATU;
    /* 勝敗の判定 */
    v = check_winner();
    /* 決着していなければ相手の手番へ */
    if( v == DRAW && n < SIZE - 1 ) v = think_maru( n + 1 );
    /* ミニマックス */
    if( v < value ) value = v;
    /* 元に戻す */
    board[i] = FREE;
  }
  return value;
}

int main()
{
  int v, i, j;
  for( i = 0; i < SIZE; i++ ){
    /* 初期化 */
    memset( board, FREE, SIZE );
    /* 初手 */
    board[i] = MARU;
    /* 相手の手番 */
    v = think_batu( 1 );
    /* 結果 */
    printf("初手 %d: 評価値 %d\n", i, v );
  }
  return 0;
}

Copyright (C) 2001-2003 Makoto Hiroi
All rights reserved.

[ Home | Puzzle ]