我用Java编写了一个简单的Tic Tac Toe控制台应用程序,使用了一个位板方法(只是为了好玩)。它适用于两个人类玩家。我的目标是找出minimax算法并实现计算机播放器。我之前做过这个,对于#n;" Nim"的(非常天真的)游戏,以及相同的一般面向对象方法。我想使用相同的结构。但在这种情况下,当计算机进行移动时,它会在搜索下一步移动时破坏整个板变量。它不应该这样做,因为makeMove方法创建了一个全新的Board对象。我的问题是,为什么会发生这种奇怪的事情?以下是直接来自NetBeans的代码,松散评论:
提前感谢任何有耐心看的人。我想提一下,我查看了Cloneable接口和clone()方法,但无济于事。然后我认为这不应该是原因,因为makeMove方法的工作方式。那么为什么电脑玩家会破坏电路板?
package tictactoe;
import java.util.*;
public class TicTacToe {
public static void main(String[] args) {
Game game = new Game();
game.start();
}
}
class Game {
ArrayList<Player> players = new ArrayList(); // An ArrayList for the players
public Game() { // Determine if players are to be human or CPU
Scanner input = new Scanner(System.in);
String answer;
System.out.printf("Would you like Player 1 to be CPU? [Yes/No] ");
answer = input.nextLine();
if(answer.toLowerCase().startsWith("y")) players.add(new ComputerPlayer(0, 3));
else players.add(new Player());
System.out.printf("Would you like Player 2 to be CPU? [Yes/No] ");
answer = input.nextLine();
if(answer.toLowerCase().startsWith("y")) players.add(new ComputerPlayer(1, 3));
else players.add(new Player());
}
public void start() {
Scanner input = new Scanner(System.in);
while(true) {
clearScreen();
Board board = new Board();
while(!board.isGameOver()) {
board = board.makeMove(players.get(board.getCurrentPlayer()).getMove(board));
}
board.display();
int winner = board.checkWinner();
if(winner >= 0) {
players.get(winner).addWin();
System.out.printf("Player %d wins. He has %d wins vs %d.\nRematch? [Yes/No] ", winner + 1, players.get(winner).getWins(), players.get(winner == 0 ? 1 : 0).getWins());
}
else {
System.out.printf("The game is a tie.\nRematch? [Yes/No] ");
}
String answer = input.nextLine();
if(answer.toLowerCase().startsWith("n")) break;
else {
Player temp = players.remove(0);
players.add(temp);
for(int i = 0; i < 2; i++) { // just to help the computer player track his own ID
players.get(i).flipID();
}
}
}
System.out.printf("Game aborted. Thank you for playing.");
}
public static void clearScreen() {
for(int i = 0; i < 30; i++) System.out.printf("\n");
}
}
class Board implements Cloneable {
private int[] board; // A two dimensional array for storing player X's and
// player O's moves separately. OR them together to get
// all moves made.
private final int[] map = {0, 1, 2, 4, 8, 16, 32, 64, 128, 256}; // A simple
// way of mapping the digits 1 -> 9 (like on the numpad) to
// the bits of the board. You just do bitwise operations
// against map[n] - n being the digit.
// The numpad-like mapping looks like this:
// 7 8 9 // In memory the bits are stored thus:
// 4 5 6 // 987654321
// 1 2 3 //
private final int[] win = {7, 56, 73, 84, 146, 273, 292, 448}; // A mapping
// of all possible winning combinations translated to decimal
// numbers. Listed in order: 1,2,3; 4,5,6; 1,4,7; 3,5,7;
// 2,5,8; 1,5,9; 3,6,9; 7,8,9.
private int currentPlayer; // The player whose turn it is. 0 for X, 1 for O.
private int opponent; // The opponent. Will always be the opposite.
// The normal constructor. Takes as arguments the current state of the
// board, represented by a two dimensional integer, and the player whose
// turn it currently is, represtented by a 0 or 1
public Board(int[] theBoard, int player) {
board = theBoard;
currentPlayer = player;
opponent = player == 0 ? 1 : 0;
}
// If passed no arguments, construct the bord with default values,
// e.g. an empty board for both players and X's turn.
public Board() {
this(new int[2], 0);
}
// The usual suspects. Accesors for the attributes.
public int[] getBoard() {
return board;
}
public int getCurrentPlayer() {
return currentPlayer;
}
public int getOpponent() {
return opponent;
}
// First check against the win maps, for both players, to see if any of them
// got 3 symbols in a row. If not, check if the board is full.
public boolean isGameOver() {
for(int player = 0; player < 2; player++) {
for(int n: win) {
if((board[player] & n) == n) return true;
}
}
return (board[0] | board[1]) == 511;
}
// Returns -1 if nobody won, or returns 0 or 1 in case either of the
// players did.
public int checkWinner() {
for(int i = 0; i < 2; i++) {
for(int m: win) {
if((board[i] & m) == m) return i;
}
}
return -1;
}
// Find the possible moves on the board, returned in an array
public int[] getMoves() {
// Count the number of possible moves, prerequisite for initializing
// the array of moves that will later be returned.
int allMoves = (board[0] | board[1]);
int count = countBits(allMoves);
// Populate the array of possible moves and then return it
int[] moves = new int[9 - count];
int j = 0;
for(int i = 1; i < 10; i++) {
if((allMoves & map[i]) == 0) {
moves[j] = i;
j++;
}
}
return moves;
}
// Return the number of activated bits in an integer
// (in this case an 8 bit integer)
public static int countBits(int board) {
int count = 0;
for(int i = 1; i <= 256; i <<= 1) {
if((board & i) != 0) count++;
}
return count;
}
// The static evaluation function, used by the minmax algorithm.
// Returns 3 / -3 for victory, or the number of symbols the player
// has on any given line, if there's no opponent's symbol on it.
// Returns 0 otherwise
public int evaluate(int player) {
int allMoves = board[0] | board[1];
int ret = 0, max = 0, min = 0;
for(int p = 0; p < 2; p++) {
for(int w: win) {
int line = board[p] & w;
if(line == w) { // If victory condition found, return immediately
if(p == player) return 3;
else return -3;
}
if((line ^ allMoves) == 0) { // No moves on the line by the opp.
if(p == player) max = countBits(line) > max ? countBits(line) : max;
else min = -countBits(line) < min ? -countBits(line) : min;
}
}
}
if(Math.abs(min) != max) {
ret = Math.abs(min) > max ? min : max;
}
return ret;
}
// Now for the tricky part... this method returns a completely new
// board object. But when the minimax method calls it, it sure doesn't
// behave that way
public Board makeMove(int move) {
int[] newBoard = board;
newBoard[currentPlayer] |= map[move];
return new Board(newBoard, opponent);
}
// Tried to use something like this, at one point, but then I realized
// that it won't help me understand my problem. May use at a later time, tho
/*
public Board undoMove(int move) {
int[] newBoard = board;
newBoard[opponent] ^= map[move];
return new Board(newBoard, opponent);
}
*/
// The method to (very plainly) display the board
public void display() {
for(int i = 6; i >= 0; i -= 3) {
for(int j = 1; j <= 3; j++) {
if(((board[0] | board[1]) & map[i + j]) == 0) System.out.printf("%d", i + j);
else if((board[0] & map[i + j]) != 0) System.out.printf("X");
else System.out.printf("O");
}
System.out.printf("\n");
}
}
// Returns true/false whether a move is valid on the board
public boolean isValidMove(int move) {
if(move < 1 || move > 9) return false;
return ((board[0] | board[1]) & map[move]) == 0;
}
}
class Player {
int wins = 0; // Simple way of keeping track of the number of wins.
// Accessor for the win atr.
public int getWins() {
return wins;
}
// Add a win
public void addWin() {
wins++;
}
public void flipID() {
// To be overridden by the ComputerPlayer class
}
// Query the user for a valid move
public int getMove(Board board) {
Scanner input = new Scanner(System.in);
int move;
board.display();
do {
System.out.printf("Input a valid move: ");
move = input.nextInt();
} while(!board.isValidMove(move));
//Game.clearScreen();
return move;
}
}
class ComputerPlayer extends Player {
int self; // Keep track of his own place in the players array
int maxSearchDepth; // Seach depth setting for the minimax algorithm
public ComputerPlayer(int n, int m) { // Constructor
self = n;
maxSearchDepth = m;
}
@Override
public void flipID() {
self = self == 0 ? 1 : 0;
}
// The implementation of the minimax algorithm
@Override
public int getMove(Board board) {
int[] temp = minimax(board, 0, maxSearchDepth);
return temp[1];
}
public int[] minimax(Board mmBoard, int depth, int maxDepth) {
int[] ret = new int[2]; //ret[0] = bestScore, ret[1] = bestMove
int currentScore, bestScore, bestMove;
if(mmBoard.isGameOver() || depth == maxDepth) {
ret[0] = mmBoard.evaluate(mmBoard.getCurrentPlayer());
ret[1] = 0;
return ret;
}
bestMove = 0;
bestScore = mmBoard.getCurrentPlayer() == self ? -4 : 4;
for(int move: mmBoard.getMoves()) {
// System.out.printf("Board: %s, Depth: %d. Moves: %s. Trying: %d\n", Arrays.toString(mmBoard.getBoard()), depth, Arrays.toString(mmBoard.getMoves()), move);
Board newBoard = mmBoard.makeMove(move); // The problem call...
// System.out.printf("Original: %s New: %s", mmBoard, newBoard);
int[] temp = minimax(newBoard, depth + 1, maxDepth);
currentScore = temp[0];
if(mmBoard.getCurrentPlayer() == self) {
if(currentScore > bestScore) {
bestScore = currentScore;
bestMove = move;
}
}
else {
if(currentScore < bestScore) {
bestScore = currentScore;
bestMove = move;
}
}
}
ret[0] = bestScore;
ret[1] = bestMove;
return ret;
}
}
答案 0 :(得分:2)
注意,我没有阅读所有代码,因为没有最小的例子,但我在这里看到了一个问题:
public Board makeMove(int move) {
int[] newBoard = board;
// ^^^^^
newBoard[currentPlayer] |= map[move];
return new Board(newBoard, opponent);
}
您实际上并未在此处制作新主板,new Board(...)
对旧主板int[]
有引用。
通过调用语句int[] newBoard = board;
,您将board
的引用分配给新的整数数组,而不是实际复制,换句话说:两个板对象现在都指向同一个{{ 1}}
要制作实际副本,您需要使用System.arraycopy();
所以新方法看起来像这样:
int[]
请注意,我没有读完您的所有代码,但您在该方法中所做的假设不正确
答案 1 :(得分:0)
尝试将此添加到您的makeMove()
方法:
int[] newBoard = Arrays.copyOf(board, board.length);
在您的代码中,您只需将newBoard
引用指向board
引用的现有整数数组。
上面的行创建了新的整数数组,并复制了board
引用的数组内容。
HTH