机器学习|手把手教你构建一个学习如何玩游戏的AI应用程序

Tic tac toe是一个非常简单的游戏,能够让你编程一台电脑来玩。你可以编写代码告诉它如果可用的话进入井字中心,看看对手是否有两个连在一起,如果是这样的话就封锁它,或者如果有一个可用的就连接到一个角落,让自己的两个连在一起等等。

但是这不是你学会玩的方式。有人把网格放在你的面前,并开始把Xs和Os放在它上面。过了一会儿,你为自己想出了策略。

那么,我们如何让电脑模仿人类呢?计算机非常擅长的一件事是记住事情,为什么不创建一个应用程序,让电脑记住它是如何输了一场井字游戏,然后避免再次做同样的事情。

这将如何实现?首先,考虑游戏棋盘:它有九个单元格,每个单元格有三个状态:空,O和X。可以用一个九位数的三位数表示。所以,例如一块空棋是000000000,中间有一个X(给出X的值为2)的棋是000020000等等。这个可以很容易地转换成一个整数,这个整数可以是散列表中的关键字。所以,当电脑输了这场游戏,它可以看看棋子是什么时候做了最后一步,评估,并设置一个hashmap(哈希映射)的值。将来在做一个动作之前,可以先看看棋盘的状态,如果它做了一个特定的动作,并且如果它出现在HashMap中,它会知道它上次输过这场游戏, 所以这次应该做点别的。

使用这种方法,不会有其他的策略,我们可以建立一个应用程序,迅速学习如何玩井字游戏。不仅如此,当你完成后,hashmap很容易转移,即如何玩这个游戏的“记忆”可以给另一台计算机,然后它会立即知道如何玩这个游戏。这个算法太天真了,它只会在第一个可用空间中移动。起初,它会失去很多,但是随着时间的推移,它将记录失败的地方,并遵循避免策略。你会发现,它很快就学会了如何玩一个井字游戏,就像人类一样。

以下是游戏的实际操作视频——游戏中我拿X,电脑是O。它总是天真地走到第一个可用的位置,除非这个位置以前已经不能用了。当我在中心开始的时候,它总是往右走,我不断地打击电脑,直到它找出错误,然后迫使我陷入困境。当我改变我的策略,电脑已经学习到了:

Tic-Tac-Toe(三连棋游戏)机器学习演示视频

实现这一机器学习的学习代码是非常简单的。这里有一个片段,显示计算机评估棋子的位置,然后倒退导致丢失状态的人为操作,将棋子状态存储在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;
}

boardValues[]数组只保留0、1、2为空、O和X,所以calcBoardValue通过在它们之间循环并将它们乘以它们的索引来将其转换为整数——有效地将棋子转换为整数。在learnFromLosing中,将最后一个人的移动的值从中减去,以使棋盘恢复到预失败状态,然后失败的位置存储在loseGamePositions的哈希映射(hashmap)中。

当轮到电脑移动时,它会循环通过棋盘,直到它找到一个空的位置(这是天真的部分!),然后调用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;
        }
    }
}

然后isOKToMove函数会查看棋盘,如果计算机执行此操作,并检查该棋盘位置是否在失败位置的hashmap中。如果是,那么就不能移动了。如果不是,那么电脑会做这个动作:

public boolean isOKToMove(int thisIndex){
    int boardValue = calcBoardValue();
    boardValue+=COMPUTER_VALUE * Math.pow(3, thisIndex);
    if(losingGamePositions.containsKey(boardValue)){
        return false;
    } else {
        return true;
    }
}

这就是它!为了您的方便,以下是实现此代码的完整Android活动的源代码(也就是您在上述视频中看到的Android应用程序)。

接下来的步骤和思考:

  1. 如何扩展应用程序,以便哈希映射中的“false”值表示棋子位置的失败,“true”表示胜利? 这样,计算机不但可以避免记忆失效,还可以记住以前赢得的方式,从而更快地学习。
  2. 如何将hashmap的结果序列化到Firebase,然后用这些结果初始化应用程序,从而从一个应用程序到另一个应用程序进行内存转储?
  3. 你将如何将这个概念延伸到一个更复杂的游戏,如国际象棋?
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<integer, boolean=""> 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);
            }
        }
    }
}
</integer,>

本文原作者:Laurence Moroney
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
欢迎体验AI1.0 1 ------------ 运行本软件需要JRE(java运行时)1.5.0_08以上版本。 运行方法:在program目录下,右键点击AI10.jar,选打开方式为“java platform standard edition binary”即可。 2 ------------ 界面介绍: 界面左侧是一个树(load数据后显示树结构),右侧上方是您在树上点选的节点的信息的显示与编辑区,右侧中间是命令的输入区,右侧下方是输出区。 您会发现界面上有很多按钮,其实他们都是我调试程序用的,您只需了解几个按钮即可。 File菜单的“Set default dir”是设置选择载入、导出文件的默认目录, “save as”是将树保存为文件,“load”则是将文件中保存的树装入到程序中来(会覆盖原有树的呦~~,记得先save)。 Script菜单的“save script as”是将命令区的内容存入文件,“load script”是将文件读入命令区(覆盖原有的命令)。 右侧中部的“Execute”按钮可以执行命令区的命令,可以用鼠标选择一部分命令为高亮,而只执行选择的命令。 ***注意***需要人工将右下的输出区的滚动条滚到最下!(作者在此道歉了,但作者毕竟是技艺不精啊……) 右侧中部的“CLO”是清除输出区的按钮,“—”则是在输出区加横线的按钮。 3 ------------ 功能介绍: ***注意***使用前需LOAD目录import_me下的template.dat。(或LOAD您以前保存的树,但要确认模板的存在。) 之后就可以运行各种命令了。 但是命令是有限的。 ***注意***从那张源代码截图可看出本软件支持的命令…… 还有就是import_me目录下的developing.txt是我开发时积累的命令脚本,大家可以导入玩一玩。 4 ------------ 欢迎批评与建议。 作者:cmpltrtok 电邮:cmpltrtok@sina.com 5 ------------ 命令列表: 作者还是不放心,把命令列表写一下: 删除分类……。 删除……属于……。 ……是一种……不是一种……。 ……是一种……。 ……是一个……不是一个……。 ……是一个……。 ……是一种……(吗)? ……是一个……(吗)? ……是什么? ……是……吗?

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值