Tic tac toe是一个非常简单的游戏,能够让你编程一台电脑来玩。你可以编写代码告诉它如果可用的话进入井字中心,看看对手是否有两个连在一起,如果是这样的话就封锁它,或者如果有一个可用的就连接到一个角落,让自己的两个连在一起等等。
但是这不是你学会玩的方式。有人把网格放在你的面前,并开始把Xs和Os放在它上面。过了一会儿,你为自己想出了策略。
那么,我们如何让电脑模仿人类呢?计算机非常擅长的一件事是记住事情,为什么不创建一个应用程序,让电脑记住它是如何输了一场井字游戏,然后避免再次做同样的事情。
这将如何实现?首先,考虑游戏棋盘:它有九个单元格,每个单元格有三个状态:空,O和X。可以用一个九位数的三位数表示。所以,例如一块空棋是000000000,中间有一个X(给出X的值为2)的棋是000020000等等。这个可以很容易地转换成一个整数,这个整数可以是散列表中的关键字。所以,当电脑输了这场游戏,它可以看看棋子是什么时候做了最后一步,评估,并设置一个hashmap(哈希映射)的值。将来在做一个动作之前,可以先看看棋盘的状态,如果它做了一个特定的动作,并且如果它出现在HashMap中,它会知道它上次输过这场游戏, 所以这次应该做点别的。
使用这种方法,不会有其他的策略,我们可以建立一个应用程序,迅速学习如何玩井字游戏。不仅如此,当你完成后,hashmap很容易转移,即如何玩这个游戏的“记忆”可以给另一台计算机,然后它会立即知道如何玩这个游戏。这个算法太天真了,它只会在第一个可用空间中移动。起初,它会失去很多,但是随着时间的推移,它将记录失败的地方,并遵循避免策略。你会发现,它很快就学会了如何玩一个井字游戏,就像人类一样。
以下是游戏的实际操作视频——游戏中我拿X,电脑是O。它总是天真地走到第一个可用的位置,除非这个位置以前已经不能用了。当我在中心开始的时候,它总是往右走,我不断地打击电脑,直到它找出错误,然后迫使我陷入困境。当我改变我的策略,电脑已经学习到了:
实现这一机器学习的学习代码是非常简单的。这里有一个片段,显示计算机评估棋子的位置,然后倒退导致丢失状态的人为操作,将棋子状态存储在HashMap中:
public void learnFromLosing(){
int losingPosition = calcBoardValue();
losingPosition-= HUMAN_VALUE * Math.pow(3, lastHumanMove);
losingGamePositions.put(losingPosition, true);
}
public int calcBoardValue(){
int boardValue = 0;
for(int nIndex=0; nIndex<9; nIndex++){
boardValue += boardValues[nIndex] * Math.pow(3, nIndex);
}
return boardValue;
}
当轮到电脑移动时,它会循环通过棋盘,直到它找到一个空的位置(这是天真的部分!),然后调用isOKToMove,如果它返回true,将使计算机移动到该位置。
boolean computer_moved=false;
for(int nIndex=0; nIndex<9; nIndex++){
if(boardValues[nIndex]==EMPTY_VALUE){
if(isOKToMove(nIndex)){
boardValues[nIndex]=COMPUTER_VALUE;
computer_moved=true;
totalMoves++;
drawBoard();
break;
}
}
}
public boolean isOKToMove(int thisIndex){
int boardValue = calcBoardValue();
boardValue+=COMPUTER_VALUE * Math.pow(3, thisIndex);
if(losingGamePositions.containsKey(boardValue)){
return false;
} else {
return true;
}
}
接下来的步骤和思考:
import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import java.util.HashMap;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
int[] buttonIDs = new int[] {R.id.btn1, R.id.btn2, R.id.btn3, R.id.btn4, R.id.btn5, R.id.btn6, R.id.btn7, R.id.btn8, R.id.btn9};
Button[] buttons = new Button[9];
int[] boardValues = new int[9];
int lastHumanMove=0;
int totalMoves=0;
public static final int EMPTY_VALUE=0;
public static final int COMPUTER_VALUE=1;
public static final int HUMAN_VALUE=2;
public static final String COMPUTER_CHARACTER="O";
public static final String HUMAN_CHARACTER="X";
public static final String EMPTY_CHARACTER="";
public static final String NOBODY="NOBODY";
HashMap losingGamePositions = new HashMap<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Button tmpButton;
for(int nIndex=0; nIndex<9; nIndex++) {
buttons[nIndex] = (Button) findViewById(buttonIDs[nIndex]);
buttons[nIndex].setOnClickListener(this);
}
drawBoard();
}
@Override
public void onClick(View v){
if(v instanceof Button){
Button thisButton = (Button) v;
int index = Integer.parseInt(thisButton.getTag().toString());
if(boardValues[index]==EMPTY_VALUE){
boardValues[index]=HUMAN_VALUE;
lastHumanMove=index;
drawBoard();
totalMoves++;
if(checkWinner(HUMAN_VALUE)){
learnFromLosing();
showWinner(HUMAN_CHARACTER);
} else {
if(totalMoves==9)
{
showWinner(NOBODY);
} else {
doComputerTurn();
}
}
}
}
}
public void showWinner(String playerID){
AlertDialog alertDialog = new AlertDialog.Builder(MainActivity.this).create();
alertDialog.setTitle("Game Over");
if(playerID==NOBODY){
alertDialog.setMessage("It's a tie!");
} else {
alertDialog.setMessage("The Winner is " + playerID);
}
alertDialog.setButton(AlertDialog.BUTTON_NEUTRAL, "OK",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
for(int nIndex=0; nIndex<9; nIndex++){
buttons[nIndex].setText(EMPTY_CHARACTER);
boardValues[nIndex]=EMPTY_VALUE;
totalMoves=0;
}
}
});
alertDialog.show();
}
public boolean checkWinner(int playerID){
if((boardValues[0]==playerID && boardValues[1]==playerID && boardValues[2]==playerID) ||
(boardValues[0]==playerID && boardValues[3]==playerID && boardValues[6]==playerID) ||
(boardValues[0]==playerID && boardValues[4]==playerID && boardValues[8]==playerID) ||
(boardValues[1]==playerID && boardValues[4]==playerID && boardValues[7]==playerID) ||
(boardValues[2]==playerID && boardValues[4]==playerID && boardValues[6]==playerID) ||
(boardValues[2]==playerID && boardValues[5]==playerID && boardValues[8]==playerID) ||
(boardValues[3]==playerID && boardValues[4]==playerID && boardValues[5]==playerID) ||
(boardValues[6]==playerID && boardValues[7]==playerID && boardValues[8]==playerID))
return true;
else
return false;
}
public void doComputerTurn(){
boolean computer_moved=false;
for(int nIndex=0; nIndex<9; nIndex++){
if(boardValues[nIndex]==EMPTY_VALUE){
if(isOKToMove(nIndex)){
boardValues[nIndex]=COMPUTER_VALUE;
computer_moved=true;
totalMoves++;
drawBoard();
break;
}
}
}
if (checkWinner(COMPUTER_VALUE)) {
showWinner(COMPUTER_CHARACTER);
} else {
if(!computer_moved) {
// There are no moves, so let's flag this as a bad board position
learnFromLosing();
// Just do any move, and lose
for(int nIndex=0; nIndex<9; nIndex++){
if(boardValues[nIndex]==EMPTY_VALUE){
boardValues[nIndex]=COMPUTER_VALUE;
computer_moved=true;
drawBoard();
break;
}
}
}
}
}
public boolean isOKToMove(int thisIndex){
int boardValue = calcBoardValue();
boardValue+=COMPUTER_VALUE * Math.pow(3, thisIndex);
if(losingGamePositions.containsKey(boardValue)){
return false;
} else {
return true;
}
}
public void learnFromLosing(){
int losingPosition = calcBoardValue();
losingPosition-= HUMAN_VALUE * Math.pow(3, lastHumanMove);
losingGamePositions.put(losingPosition, true);
}
public int calcBoardValue(){
int boardValue = 0;
for(int nIndex=0; nIndex<9; nIndex++){
boardValue += boardValues[nIndex] * Math.pow(3,nIndex);
}
return boardValue;
}
public void drawBoard(){
for(int nIndex=0; nIndex<9; nIndex++){
switch(boardValues[nIndex]){
case HUMAN_VALUE:
buttons[nIndex].setText(HUMAN_CHARACTER);
break;
case COMPUTER_VALUE:
buttons[nIndex].setText(COMPUTER_CHARACTER);
break;
default:
buttons[nIndex].setText(EMPTY_CHARACTER);
}
}
}
}