web-dev-qa-db-ja.com

三目並べゲームのどのアルゴリズムを使用して、AIの「ベストムーブ」を決定できますか?

三目並べの実装では、マシンでプレイするのに最適な動きを決定するのが難しい部分だと思います。

追求できるアルゴリズムは何ですか?単純なものから複雑なものまで、実装を検討しています。問題のこの部分にどのように取り組みますか?

61

完璧なゲームをプレイするためのウィキペディアの戦略(毎回勝つか引き分けるか)は、簡単な擬似コードのように見えます。

Wikipedia(Tic Tac Toe#Strategy)からの引用

ニューエルとサイモンの1972年の三目並べで使用されているように、プレイヤーは次のリストから最初の利用可能な動きを毎ターン選択すれば、チックタックトーの完璧なゲームをプレイできます(勝つ、または少なくとも引きます)プログラム。[6]

  1. 勝つ:2人が連続している場合、3番目をプレイして3人を連続して獲得します。

  2. ブロック:対戦相手が連続して2人いる場合、3番目をプレイしてブロックします。

  3. フォーク:2つの方法で勝つ機会を作成します。

  4. ブロックの相手のフォーク:

    オプション1:フォークを作成したり勝利したりしない限り、連続して2つ作成して、対戦相手を防御させます。たとえば、「X」にコーナーがあり、「O」にセンターがあり、「X」に反対側のコーナーがある場合、「O」が勝つためにコーナーをプレーしてはいけません。 (このシナリオでコーナーをプレイすると、「X」が勝つための分岐点が作成されます。)

    オプション2:相手がフォークできる構成がある場合、そのフォークをブロックします。

  5. センター:センターを再生します。

  6. 反対側のコーナー:相手がコーナーにいる場合、反対側のコーナーをプレイします。

  7. 空のコーナー:空のコーナーを再生します。

  8. 空のサイド:空のサイドをプレイします。

「分岐」状況がどのように見えるかを認識することは、提案されたように総当たり的に行うことができます。

注:「完璧な」対戦相手はナイスなエクササイズですが、最終的に「プレイ」する価値はありません。ただし、上記の優先順位を変更して、敵の人格に特徴的な弱点を与えることができます。

55
bkane

必要なのは(三目並べまたはチェスのようなはるかに難しいゲームの場合) ミニマックスアルゴリズム 、またはその少し複雑なバリアント、 アルファベータプルーニング です。ただし、通常の単純なミニマックスは、三目並べと同じくらい小さな検索スペースでのゲームには適しています。

簡単に言えば、あなたがしたいことは、あなたにとって最良の結果をもたらす動きを検索することではなく、可能な限り最悪の結果が可能な動きを検索することです。対戦相手が最適にプレーしていると仮定する場合、彼らはあなたにとって最悪の動きを取ると仮定する必要があるため、最大ゲインを最小化する動きをとる必要があります。

38
Nick Johnson

可能な限りのすべてのボードを生成し、後で生成されるボードに基づいてスコアリングするブルートフォース方式は、特に90度のボード回転が冗長であると認識した場合、垂直方向のフリップと同様に、多くのメモリを必要としません。水平軸および対角軸。

その時点に達すると、結果を説明するためのツリーグラフに1k未満のデータが存在するため、コンピューターにとって最適な動きになります。

-アダム

14
Adam Davis

Tic-tac-toeの典型的なアルゴリズムは次のようになります。

Board:ボードを表す9要素のベクトル。 2(空白を示す)、3(Xを示す)、または5(Oを示す)を格納します。ターン:ゲームのどの動きがプレイされようとしているのかを示す整数。最初の動きは1で示され、最後は9で示されます。

アルゴリズム

メインアルゴリズムは3つの関数を使用します。

Make2:ボードの中央の正方形が空白の場合、つまり_board[5]=2_の場合は5を返します。それ以外の場合、この関数は角のない正方形_(2, 4, 6 or 8)_を返します。

Posswin(p):プレーヤーpが次の動きで勝てない場合、0を返します。それ以外の場合、勝ちの動きを構成する正方形の番号を返します。この機能により、プログラムが勝ち、対戦相手の勝ちをブロックできるようになります。この関数は、行、列、および対角線のそれぞれをチェックすることにより動作します。各正方形の値を行全体(または列または対角線)に掛けることにより、勝ちの可能性を確認できます。製品が_18_(_3 x 3 x 2_)の場合、Xが勝つことができます。製品が_50_(_5 x 5 x 2_)の場合、Oが勝ちます。勝った行(列または対角線)が見つかった場合、その中の空白の正方形を決定でき、この関数によってその正方形の数が返されます。

Go (n):正方形nに移動します。この手順は、Turnが奇数の場合、ボード_[n]_を3に設定し、Turnが偶数の場合、5に設定します。また、ターンを1ずつ増やします。

アルゴリズムには、移動ごとに戦略が組み込まれています。 Xをプレイすると奇数の動き、Oをプレイすると偶数の動きになります。

_Turn = 1    Go(1)   (upper left corner).
Turn = 2    If Board[5] is blank, Go(5), else Go(1).
Turn = 3    If Board[9] is blank, Go(9), else Go(3).
Turn = 4    If Posswin(X) is not 0, then Go(Posswin(X)) i.e. [ block opponent’s win], else Go(Make2).
Turn = 5    if Posswin(X) is not 0 then Go(Posswin(X)) [i.e. win], else if Posswin(O) is not 0, then Go(Posswin(O)) [i.e. block win], else if Board[7] is blank, then Go(7), else Go(3). [to explore other possibility if there be any ].
Turn = 6    If Posswin(O) is not 0 then Go(Posswin(O)), else if Posswin(X) is not 0, then Go(Posswin(X)), else Go(Make2).
Turn = 7    If Posswin(X) is not 0 then Go(Posswin(X)), else if Posswin(X) is not 0, then Go(Posswin(O)) else go anywhere that is blank.
Turn = 8    if Posswin(O) is not 0 then Go(Posswin(O)), else if Posswin(X) is not 0, then Go(Posswin(X)), else go anywhere that is blank.
Turn = 9    Same as Turn=7.
_

私はそれを使用しました。皆さんの気持ちを教えてください。

7
Kaushik

可能な場所の3x3マトリックスのみを扱っているため、計算能力に負担をかけることなく、あらゆる可能性を検索するだけで簡単に記述できます。各オープンスペースについて、そのスペースをマークした後(再帰的に、私は言います)、すべての可能な結果を​​計算し、勝つ可能性が最も高い動きを使用します。

これを最適化することは、本当に無駄です。簡単なものもありますが:

  • 最初に他のチームの勝ちの可能性を確認し、最初に見つけたチームをブロックします(とにかく2ゲームオーバーの場合)。
  • センターが開いている場合は常にセンターを使用します(前のルールには候補がありません)。
  • 側面よりも先に角を取ります(前のルールが空の場合も)
6
billjamesdev

学習するサンプルゲームでAIを再生させることができます。教師付き学習アルゴリズムを使用して、支援します。

3
J.J.

プレイフィールドを使用しないでの試み。

  1. 勝つ(あなたのダブル)
  2. そうでない場合、失うことはありません(相手のダブル)
  3. そうでない場合、あなたはすでにフォークを持っていますか(ダブルダブルを持っています)
  4. そうでない場合、相手がフォークを持っている場合
    1. 可能性のあるダブルとフォークをブロックポイントで検索します(究極の勝利)
    2. ブロッキングポイントでフォークを検索しない場合(これにより、相手に最も負けやすい可能性が与えられます)
    3. ブロックポイントだけではない場合(失うことはありません)
  5. doubleとforkを検索しない場合(最終的な勝利)
  6. 対戦相手に最も負けている可能性を与えるフォークだけを検索しない場合
  7. 倍精度のみを検索しない場合
  8. 行き止まりでない場合、ネクタイ、ランダム。
  9. そうでない場合(最初の動きを意味します)
    1. ゲームの最初の動きである場合;
      1. 相手に最も負けやすい可能性を与えます(アルゴリズムは、相手に7つの負け点の可能性を与えるコーナーのみをもたらします)
      2. または退屈を破壊するためだけにランダムに。
    2. ゲームの2番目の動きの場合;
      1. 負けていないポイントのみを見つける(もう少しオプションを与える)
      2. または、このリストで勝率が最も高いポイントを見つけます(退屈する可能性があります。これは、すべてのコーナーまたは隣接するコーナーまたはセンターのみになるためです)

注:ダブルとフォークがある場合、ダブルが対戦相手にdoubleを与えるかどうかを確認します。

3
Mesut Ergul

各正方形を数値スコアでランク付けします。正方形が取られた場合、次の選択肢に移動します(ランクの降順で並べ替えられます)。戦略を選択する必要があります(最初に行くには2つの主要なものがあり、2番目には3つ(と思う)があります)。技術的には、すべての戦略をプログラムし、ランダムに選択することができます。それは、予測しにくい相手になります。

0
Daniel Spiewak

この答えは、P1の完璧なアルゴリズムの実装を理解していることを前提とし、他の人よりもミスを頻繁に犯す通常の人間のプレーヤーに対して条件を勝ち取る方法について説明しています。

もちろん、両方のプレイヤーが最適にプレイすれば、ゲームは引き分けになります。人間レベルでは、コーナーでプレイするP1がはるかに頻繁に勝利を生み出します。心理的な理由が何であれ、P2はセンターでプレーすることはそれほど重要ではないと考えさせられますが、P1の勝利ゲームを生み出さない唯一の反応なので、彼らにとっては不幸なことです。

P2 doesが中央で正しくブロックされる場合、P1は反対側のコーナーをプレイする必要があります。これは、心理的理由が何であれ、P2はコーナーをプレイする対称性を好むため、再び負けたボードを生成するためです。

P1が開始の動きを行う可能性のある動きについては、その後P2が両方のプレイヤーが最適にプレーする場合にP1が勝利する動きがあります。その意味で、P1はどこでもプレイできます。エッジの動きは、この動きに対する可能な反応の大部分が引き分けになるという意味で最も弱いですが、P1の勝利を生み出す反応がまだあります。

経験的に(より正確には、逸話的に)、P1の最初の最高の動きは最初のコーナー、2番目の中心、最後のエッジのようです。

直接またはGUIを介して追加できる次の課題は、ボードを表示しないことです。人間はすべての状態を確実に覚えることができますが、追加された課題は対称的なボードを優先することにつながり、覚えるのに手間がかからず、最初のブランチで説明した間違いにつながります。

パーティーはとても楽しいです。

0
djechlin

最小最大アルゴリズムへの三目並べ適応

let gameBoard: [
    [null, null, null],
    [null, null, null],
    [null, null, null]
]

const SYMBOLS = {
    X:'X',
    O:'O'
}

const RESULT = {
    INCOMPLETE: "incomplete",
    PLAYER_X_WON: SYMBOLS.x,
    PLAYER_O_WON: SYMBOLS.o,
    tie: "tie"
}

結果を確認できる関数が必要です。この関数は、一連の文字をチェックします。ボードの状態がどうであれ、結果は4つのオプションのいずれかです。不完全、プレーヤーXが勝った、プレーヤーOが勝った、または引き分けのいずれかです。

function checkSuccession (line){
    if (line === SYMBOLS.X.repeat(3)) return SYMBOLS.X
    if (line === SYMBOLS.O.repeat(3)) return SYMBOLS.O
    return false 
}

function getResult(board){

    let result = RESULT.incomplete
    if (moveCount(board)<5){
        return result
    }

    let lines

    //first we check row, then column, then diagonal
    for (var i = 0 ; i<3 ; i++){
        lines.Push(board[i].join(''))
    }

    for (var j=0 ; j<3; j++){
        const column = [board[0][j],board[1][j],board[2][j]]
        lines.Push(column.join(''))
    }

    const diag1 = [board[0][0],board[1][1],board[2][2]]
    lines.Push(diag1.join(''))
    const diag2 = [board[0][2],board[1][1],board[2][0]]
    lines.Push(diag2.join(''))
    
    for (i=0 ; i<lines.length ; i++){
        const succession = checkSuccesion(lines[i])
        if(succession){
            return succession
        }
    }

    //Check for tie
    if (moveCount(board)==9){
        return RESULT.tie
    }

    return result
}

GetBestMove関数は、ボードの状態と、可能な限り最高の動きを決定したいプレーヤーのシンボルを受け取ります。この関数は、getResult関数を使用してすべての可能な動きをチェックします。勝ちの場合、スコアは1になります。ルーズの場合、スコアは-1になります。引き分けの場合、スコアは0になります。未決定の場合、新しい状態でgetBestMove関数を呼び出します。ボードと反対のシンボルの。次の動きは相手の動きなので、彼の勝利は現在のプレイヤーの負けであり、スコアは無効になります。最後に、可能性のある動きは1,0または-1のスコアを受け取り、動きを並べ替えて、最高スコアの動きを返すことができます。

const copyBoard = (board) => board.map( 
    row => row.map( square => square  ) 
)

function getAvailableMoves (board) {
  let availableMoves = []
  for (let row = 0 ; row<3 ; row++){
    for (let column = 0 ; column<3 ; column++){
      if (board[row][column]===null){
        availableMoves.Push({row, column})
      }
    }
  }
  return availableMoves
}

function applyMove(board,move, symbol) {
  board[move.row][move.column]= symbol
  return board
}
 
function getBestMove (board, symbol){

    let availableMoves = getAvailableMoves(board)

    let availableMovesAndScores = []

    for (var i=0 ; i<availableMoves.length ; i++){
      let move = availableMoves[i]
      let newBoard = copyBoard(board)
      newBoard = applyMove(newBoard,move, symbol)
      result = getResult(newBoard,symbol).result
      let score
      if (result == RESULT.tie) {score = 0}
      else if (result == symbol) {
        score = 1
      }
      else {
        let otherSymbol = (symbol==SYMBOLS.x)? SYMBOLS.o : SYMBOLS.x
        nextMove = getBestMove(newBoard, otherSymbol)
        score = - (nextMove.score)
      }
      if(score === 1)  // Performance optimization
        return {move, score}
      availableMovesAndScores.Push({move, score})
    }

    availableMovesAndScores.sort((moveA, moveB )=>{
        return moveB.score - moveA.score
      })
    return availableMovesAndScores[0]
  }

実行中のアルゴリズムGithubプロセスの詳細な説明

0
Ben Carp