65.9K
CodeProject 正在变化。 阅读更多。
Home

国际象棋入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (43投票s)

2013年8月31日

CPOL

9分钟阅读

viewsIcon

79916

downloadIcon

1093

使用 Paper JS 的等距国际象棋游戏。

目录

引言

作为一名程序员,我一直认为开发一个国际象棋游戏会是一个重要的里程碑,我会为此感到非常自豪。现在,我在这里向大家展示一个国际象棋游戏。是的,我确实为此感到有点自豪。但等等。我真的开发了一个国际象棋游戏吗?

首先,创建一个国际象棋游戏意味着要开发国际象棋引擎,这是坐在棋盘后面、试图想出最佳走法并假装是人类的那部分智能软件。我一直是一个蹩脚的国际象棋玩家,我的第一个想法是,我能做的最好的事情就是从某个现有的、可用的 C 语言国际象棋引擎移植过来。目标语言将是我感觉舒服的几种之一:C# 或 JavaScript。

令我惊讶的是,最近我发现一位名叫Óscar Toledo Gutiérrez的才华横溢的墨西哥人,在开发了 C 和 Java 版本之后,已经开发了一个 JavaScript 国际象棋引擎。不仅如此:Toledo 成功地将他的 C 语言国际象棋引擎,名为Nano Chess,压缩到了仅仅 1257 个字符,成为了世界上最小的 C 语言国际象棋引擎。Toledo 进一步完善了他,他的Pico Chess C 代码少于 1KB。他曾五次赢得 IOCCC(国际混淆 C 代码竞赛)。经过一番考虑,我决定不自己编写国际象棋引擎。

Chesslings 是一个基于 Toledo 的Tiny Chess的国际象棋游戏。也就是说,字面意义上的基于它。Tiny Chess 实际上在后台静默运行,提供我们所需的所有游戏逻辑。Chesslings 真正做的是通过等距投影和每个走法的动画来提供不同的外观和感觉。

系统要求

本文包含运行网站所需的代码,除了 JavaScript 之外不依赖任何编程语言,因此无需编译。您只需下载源代码并在您喜欢的浏览器中打开 HTML 文件即可。

Paper JS

对于这个项目,我们使用Paper JS作为渲染框架。Paper.js 是一个开源的矢量图形脚本框架,运行在 HTML5 Canvas 之上。本项目不会使用 Paper JS 最引人注目的特性,即矢量图形,而是利用 Paper JS 内置的渲染循环和在 html5 canvas上渲染图像的能力。

在代码中,您会注意到许多对 Paper JS 框架定义的类型的引用。以下是对我们在项目中如何使用它们的简要说明:

  • paper.setup:为我们设置一个空项目。如果提供了 canvas,它还会为其创建一个 View,两者都链接到此作用域。所讨论的“项目”是 Paper JS 框架内部创建的project的实例。只要说它接受我们页面上的 canvas 作为参数,并管理所有输出到该 canvas(如渲染图像)以及控制用户交互(如键盘按键)即可。
  • paper.Point:一个二维对象,表示 canvas 上的一个点。它在我们的项目中被广泛用于存储和计算屏幕上的位置。
  • paper.Rectangle:
  • paper.view.centerpaper.view对象的中心,这也意味着我们 canvas 中心点的那个点。
  • paper.Key.isDown:一个布尔函数,用于检查键盘上的Down键在那一刻是否被按下。
  • paper.Raster:栅格是一种特殊的Item对象,代表 Paper JS 项目中的图像。我们使用它为我们的每个国际象棋棋子创建独立的图像。
  • paper.view.onFrame:在动画的每一帧被调用时调用的 Item 级别处理函数。此函数提供有关事件被触发的次数、自第一个帧事件以来经过的总时间以及自上一帧发生以来经过的时间的信息。它被我们的国际象棋动画广泛使用。
  • paper.tool.onMouseUp:在鼠标按钮在 Item 上释放时调用的函数。该函数接收一个 MouseEvent 对象,其中包含有关鼠标事件的信息。

Toledo 的 Tiny Chess

正如我在文章引言中提到的,Óscar Toledo 在赢得 IOCCC(国际混淆 C 代码竞赛)奖项后不久就将他的 C 代码“Nano Chess”移植到了 JavaScript,这产生了了他称之为Tiny Chess的东西。正如预期的那样,代码是高度混淆的,并且对于一个国际象棋引擎来说非常短。这是 Tiny Chess 的完整 JavaScript 源代码。

//(c)2009 Oscar Toledo G.
var B,i,y,u,b,I=[],G=120,x=10,z=15,M=1e4,l=[5,3,4,6,2,4,3,5,1,1,1,1,1,1,1,1,9,9
,9,9,9,9,9,9,13,11,12,14,10,12,11,13,0,99,0,306,297,495,846,-1,0,1,2,2,1,0,-1,-
1,1,-10,10,-11,-9,9,11,10,20,-9,-11,-10,-20,-21,-19,-12,-8,8,12,19,21];function
X(w,c,h,e,S,s){var t,o,L,E,d,O=e,N=-M*M,K=78-h<<x,p,g,n,m,A,q,r,C,J,a=y?-x:x;
y^=8;G++;d=w||s&&s>=h&&X(0,0,0,21,0,0)>M;do{if(o=I[p=O]){q=o&z^y;if(q<7){A=q--&
2?8:4;C=o-9&z?[53,47,61,51,47,47][q]:57;do{r=I[p+=l[C]];if(!w|p==w){g=q|p+a-S?0
:S;if(!r&(!!q|A<3||!!g)||(r+1&z^y)>9&&q|A>2){if(m=!(r-2&7))return y^=8,I[G--]=
O,K;J=n=o&z;E=I[p-a]&z;t=q|E-7?n:(n+=2,6^y);while(n<=t){L=r?l[r&7|32]-h-q:0;if(
s)L+=(1-q?l[(p-p%x)/x+37]-l[(O-O%x)/x+37]+l[p%x+38]*(q?1:2)-l[O%x+38]+(o&16)/2:
!!m*9)+(!q?!(I[p-1]^n)+!(I[p+1]^n)+l[n&7|32]-99+!!g*99+(A<2):0)+!(E^y^9);if(s>h
||1<s&s==h&&L>z|d){I[p]=n,I[O]=m?(I[g]=I[m],I[m]=0):g?I[g]=0:0;L-=X(s>h|d?0:p,L
-N,h+1,I[G+1],J=q|A>1?0:p,s);if(!(h||s-1|B-O|i-n|p-b|L<-M))return W(),G--,u=J;
J=q-1|A<7||m||!s|d|r|o<z||X(0,0,0,21,0,0)>M;I[O]=o;I[p]=r;m?(I[m]=I[g],I[g]=0):
g?I[g]=9^y:0;}if(L>N||s>1&&L==N&&!h&&Math.random()<.5){I[G]=O;if(s>1){if(h&&c-L
<0)return y^=8,G--,L;if(!h)i=n,B=O,b=p;}N=L;}n+=J||(g=p,m=p<O?g-3:g+2,I[m]<z|I[
m+O-p]||I[p+=p-O])?1:0;}}}}while(!r&q>2||(p=O,q|A>2|o>z&!r&&++C*--A));}}}while(
++O>98?O=20:e-O);return y^=8,G--,N+M*M&&N>-K+1924|d?N:0;}B=i=y=u=0;while(B++<
120)I[B-1]=B%x?B/x%x<2|B%x<2?7:B/x&4?0:l[i++]|16:7;for(a=
"<table cellspacing=0 align=center>",i=18;i<100;a+=++i%10-9?
"<th width=40 height=40 onclick=Y("+i+") style='border:2px solid #aae' id=o"+i+
" bgcolor=#"+(i*.9&1?"9090d0>":"c0c0ff>"):(i++,"<tr>"));
a+="<th colspan=8><select id=t><option>Q<option>R<option>B";
document.write(a+"<option>N</select></table>");
function W(){B=b;for(p=21;p<99;++p)if(q=document.getElementById("o"+p)){q.
innerHTML="<img width=40 src="+(I[p]&z)+".gif>";q.
style.borderColor=p==B?"#ff0":"#aae";}}W();
function Y(s){i=(I[s]^y)&z;if(i>8){b=s;W();}else if(B&&i<9){b=s;i=I[B]&z;if((i&
7)==1&(b<29|b>90))i=14-document.getElementById("t").selectedIndex^y;X(0,0,0,21,
u,1);if(y)setTimeout("X(0,0,0,21,u,2/*ply*/),X(0,0,0,21,u,1)",250);}}
        

就是这样。这就是在 JavaScript 中运行完整国际象棋游戏所需的一切。现在,我们的 Chesslings 游戏从哪里开始呢?首先,我们创建一个专用的div来放置 Tiny Chess,并将其 ID 设置为“toledoTable”。

            <div id="toledoTable" style="position: absolute; top: 0; display: none;"></<div>
        

现在我们稍微修改一下 Tiny Chess 的代码,足以在我们 Chesslings 游戏发生新的游戏走法时注入一些通知,并重命名一些函数和变量以提高可读性。这是必需的,因为实际上,所有的游戏走法都是由后台运行的 Tiny Chess 代码执行的。

            function renderChess() {
            i = "<table>";
            for (u = 18;
                u < 98;
                render()
            )
                B = selectedTileNum;

            if (game) {
                game.processMovement(I);
            }
        }
        renderChess()

        function render() {
            $('#toledoTable').html(i += ++u % x - 9 ?
                "<th width=30 height=30 onclick='clickTile(" + u + ")' style='font-size:25px'bgcolor=#" + (u - B ? u * .9 & 1 || 9 :
                "d") + "0f0e0>&#" + (I[u] ? 9808 + l[67 + I[u]] : 160) + ";" : u++ && "<tr>");
        }

        function clickTile(u) {
            game.onTileClick(u);
            I[selectedTileNum = u] > 8 ? renderChess() : X(0, 0, 1);
        }
        

在对原始代码进行这些小的调整后,我们可以继续我们的游戏开发。

国际象棋入门

Chesslings 是一个 JavaScript 游戏,它响应底层 Tiny Chess 代码的走法,如前所述。但是,我们不会讨论代码的每一个方面。相反,让我们关注它的关键特性。Chesslings 的第一个入口点是由game实例暴露的onTileClick函数。

                onTileClick: function (tileNum) {
                    //here goes the code that highlights the selected tile.
                },
        

下一个入口点是processMovement函数,负责获取当前棋盘状态并与之前的棋盘状态进行比较,以便我们找出移动了哪个棋子,以及哪个棋子可能在走法中丢失。

                processMovement: function (tableArray) {
                    //here goes the code that processes the game board's current state.
                },
        

棋盘状态由一个 10x12 位置的矩阵表示,该矩阵在其中心包含一个较小的 8x8 国际象棋棋盘。矩阵尺寸大于普通国际象棋棋盘的原因是,只有矩阵的中心区域被国际象棋棋盘本身占据,而边缘位置则用于方便 Tiny Chess 的走法计算。

正如我们所看到的,表格数组的每个位置都由一个具有不同数字来表示棋子类型的棋子占据,除了兵、空白格和棋盘外的(它们具有相同的棋子数字)。我们移植了棋子类型代码到我们的游戏中,以便之后根据其含义显示每个棋子。

            Chess.PieceTypes = {
                SPACE: 0,
                BLACK_PAWN: 1,
                BLACK_KING: 2,
                BLACK_HORSE: 3,
                BLACK_BISHOP: 4,
                BLACK_ROOK: 5,
                BLACK_QUEEN: 6,
                OFF_BOARD: 7,
                WHITE_PAWN: 9,
                WHITE_KING: 10,
                WHITE_HORSE: 11,
                WHITE_BISHOP: 12,
                WHITE_ROOK: 13,
                WHITE_QUEEN: 14
            };
        

不出所料,游戏开始时有一套 32 个棋子。下面的代码显示了我们如何实例化它们并填充棋子数组。为此,我们只考虑前两行和后两行(y < 2 || y > 5),并忽略棋盘的其余部分。每个棋子都有一个唯一的键,以及代表该棋子类别的精灵表的名称。

            var keys = ('BRL,BNL,BBL,BQ,BK,BBR,BNR,BRR,BP1,BP2,BP3,BP4,BP5,BP6,BP7,BP8,' +
                        'WP1,WP2,WP3,WP4,WP5,WP6,WP7,WP8,WRL,WNL,WBL,WQ,WK,WBR,WNR,WRR').split(',');

            for (var y = 0; y <= 7; y++) {
                for (var x = 0; x <= 7; x++) {
                    if (y < 2 || y > 5) {
                        var key = keys[self.pieces.length];
                        var ss;
                        if (y < 2)
                            ss = window['spriteSheetS_' + key[1]];
                        else
                            ss = window['spriteSheetN_' + key[1]];

                        self.pieces.push(new Chess.Piece(
                            key,
                            { x: x, y: y }, ss));
                    }
                }
            }
        

名称“spriteShetN”“spriteShetS”分别代表“白色”和“黑色”队伍,其中“N”和“S”表示“朝北的棋子”与“朝南的棋子”。每一类棋子([P]awn,[R]ook,[B]ishop,k[N]ight,[Q]ueen,[K]ing)都有一个“朝北”和一个“朝南”的版本,正如我们在下面的图像中看到的。

棋子 背面 正面

请注意,所有 chesslings 的颜色都相同。为了区分它们,我们假设背面朝的棋子是“白色”棋子,而正面朝的棋子是“黑色”棋子。这意味着当棋子向后走时,它们也会向后走。

注意:“Chesslings”也是我给游戏中棋子起的名字。就像“earthlings”是住在地球上的人一样,“chesslings”是生活在国际象棋世界里的这些奇特居民。

移动检测

每当底层的Tiny Chess代码调用processMovement函数时,Chesslings 代码就会将表格数组的状态与最后一个处理过的数组状态进行比较。一些循环用于检查更改的位置,另一些用于确定哪些棋子受到影响,然后将一个动画排入AnimationManager进行处理,逐个处理待处理的动画。processMovement函数结构如下所示。

    processMovement: function (tableArray) {
        var self = this;

        //Let's proceed only when something changed
        if (self.lastTableArray != tableArray) {
            var fromPosition; //the position from where some piece moved
            var toPosition; //the position to where the piece moved
            $(self.lastTableArray).each(function (index, item) {

                    //some code to set the "fromPosition" variable

            });

            //Let's proceed only if some movement was detected
            if (fromPosition) {
                $(tableArray).each(function (index, item) {
                    var row = parseInt(index / 10) - 2;
                    var col = (index % 10) - 1;

                    //If this position was last time empty but now is occupied by a piece,
                    //then the piece moved to here
                    if (self.lastTableArray[index] == Chess.PieceTypes.SPACE &&
                        tableArray[index] != Chess.PieceTypes.SPACE) {

                        //some code to set the "toPosition" variable

                    }
                    //If this position was last time occupied by a piece but now is replaced by another one,
                    //then this last piece was lost
                    else if (self.lastTableArray[index] != Chess.PieceTypes.SPACE &&
                        tableArray[index] != Chess.PieceTypes.SPACE &&
                        self.lastTableArray[index] != tableArray[index]) {
                        
                        //some code to set the "toPosition" variable

                    }
                });

                //This loop will decide which piece moved, and take action accordingly
                $(game.pieces).each(function (index, piece) {
                    if (piece.isActive &&
                        piece.currentTile.x == fromPosition.x &&
                        piece.currentTile.y == fromPosition.y) {

                        //The animation will move "piece" from "fromPosition" to "toPosition" in 1000 milliseconds,
                        //starting 200 milliseconds after the start of the animation
                        var animation = new Chess.PointAnimation(piece, fromPosition, toPosition, 200, 1000);
                        animation.onFinish = function () {

                            //When the animation ends, we inactivate (make invisible) the piece that
                            //may have been lost to the attacking one.
                            $(game.pieces).each(function (index2, piece2) {
                                if (piece2.isActive &&
                                    piece2.currentTile.x == toPosition.x &&
                                    piece2.currentTile.y == toPosition.y &&
                                    piece2.key != piece.key) {

                                    //setting a chess piece to "inactive" actually makes it invisible
                                    piece2.setInactive(false);
                                }
                            });
                        }
                        //we enqueue the animation, so that it will be processed only when required
                        self.animationManager.enqueueAnimation(animation);
                    }
                });

            }

        }

        self.lastTableArray = tableArray.slice();
        self.processCount++;
    },
        

动画

动画是代码中一个非常重要的方面。我们不希望立即将棋子从 A 点移动到 B 点。相反,我们希望棋子在给定的 T 时间内从 A 移动到 B。这可以通过处理动画来实现。我们的AnimationManager处理PointAnimation类型的实例队列,这样我们就可以确保两个或更多动画同时发生。

PointAnimation类型的每个实例都用一组参数创建。

  • pieceChess.Piece类型的实例。
  • startPosition:具有动画开始位置 (x, y) 坐标的Paper.Point实例。
  • endPosition:具有动画结束位置 (x, y) 坐标的Paper.Point实例。
  • startTimeInMS:动画开始前的延迟时间,以毫秒为单位。
  • totalTimeInMS:动画的总持续时间,以毫秒为单位。

PointAnimation的主要函数onFrame是该对象的核心。该函数在动画的生命周期中被调用很多次。给定一个deltaTimeInSec参数,该函数会计算相对位置,同时考虑动画的总持续时间和已逝去的时间,以产生 delta 向量,即点 A (ax, ay) 和点 B (bx, by) 之间的 D (dx, dy) 向量差。该函数在动画总持续时间完成之前返回true

//********************
//Chess.PointAnimation
//********************

//piece: an instance of the Chess.Piece type
//startPosition: the Paper.Point instance with the (x, y) coordinates for the animation's start position
//endPosition: the Paper.Point instance with the (x, y) coordinates for the animation's end position
//startTimeInMS: the time delay time until the animation starts, in milliseconds
//totalTimeInMS: the animation duration in milliseconds
Chess.PointAnimation = function (piece, startPosition, endPosition, startTimeInMS, totalTimeInMS) {
    this.init(piece, startPosition, endPosition, startTimeInMS, totalTimeInMS)
}

$.extend(Chess.PointAnimation.prototype, {
    init: function (piece, startPosition, endPosition, startTimeInMS, totalTimeInMS) {
        this.piece = piece;
        this.startTimeInMS = startTimeInMS;
        this.startPosition = startPosition;
        this.endPosition = endPosition;
        this.totalTimeInMS = totalTimeInMS;
        this.ellapsedTimeInMS = 0; //
        this.isActive = true; //
        this.point = startPosition; //
    },
    onFrame: function (deltaTimeInSec) {
        var self = this;

        //the delta time, in seconds
        var deltaTimeInMS = deltaTimeInSec * 1000;

        //adds to the total ellapsed time for this animation instance
        self.ellapsedTimeInMS += deltaTimeInMS;

        if (self.ellapsedTimeInMS < self.startTimeInMS) {
            //the animation is active but still not started, so do nothing
            return true;
        }
        else if (self.ellapsedTimeInMS <= self.totalTimeInMS) {
            //the current point is calculated taking in consideration the total animation duration, the ellapsed time,
            //the start and the end position.
            var timeFraction = self.ellapsedTimeInMS / self.totalTimeInMS;
            var diffVector = new paper.Point(self.endPosition.x - self.startPosition.x, self.endPosition.y - self.startPosition.y);

            self.piece.currentTile =
            self.point = new paper.Point(self.startPosition.x + diffVector.x * timeFraction, self.startPosition.y + diffVector.y * timeFraction);
            return true;
        }
        else {
            //when the total animation duration is completed, we force the piece to assume the end position.
            //the onFinish callback function is invoked when needed.
            self.isActive = false;
            self.piece.currentTile = 
            self.point = self.endPosition;
            consoleLog('end of: ' + self.toString());
            if (self.onFinish)
                self.onFinish();
            return false;
        }
    },
    toString: function () {
        return 'pa (from: ' + this.startPosition + ' to:' + this.endPosition + ')';
    }
});
        

动画排序背后的逻辑在于Chess.AnimationManager代码。它基本上是Chess.PointAnimation类型实例的队列实现。

//********************
//Chess.AnimationManager
//********************
Chess.AnimationId = 1;
Chess.AnimationManager = function () {
    this.init();
}

$.extend(Chess.AnimationManager.prototype, {
    currentAnimation: null,
    queue: [],
    init: function () {
        this.id = Chess.AnimationId++;
    },
    enqueueAnimation: function (animation) {
        consoleLog('enqueueing ' + animation.piece.key + ':' + animation.toString());
        var self = this;
        //positioning the animation at the end of the queue
        self.queue.push(animation);
    },
    dequeueAnimation: function () {
        var self = this;
        //taking the first element of the queue
        var animation = self.queue[0];
        consoleLog('dequeueing ' + animation.piece.key + ':' + animation.toString());
        //deleting the first element
        self.queue = self.queue.slice(1, self.queue.length)
        self.currentAnimation = null;
        return animation;
    },
    getCurrentAnimation: function () {
        var self = this;
        if (!self.currentAnimation) {
            if (self.queue.length > 0) {
                self.currentAnimation = self.queue[0];
            }
        }
        return self.currentAnimation;
    }
})
        

回到processMovement代码,我们注意到Chess.AnimationManager的实例是如何被调用的。首先我们找出哪个棋子移动了,然后我们排入一个动画,从原始表格位置开始,到目标位置结束。

我们还订阅了一个onFinish事件,如果棋子被吃掉,它将使被吃掉的棋子变得不可见,并且该回调事件仅在动画结束时发生。这确保了处理过程遵守事件顺序。

    processMovement: function (tableArray) {

        *** HERE WE INSTANTIATE THE fromPosition AND toPosition VARIABLES ***

        //This loop will decide which piece moved, and take action accordingly
        $(game.pieces).each(function (index, piece) {
            if (piece.isActive &&
                piece.currentTile.x == fromPosition.x &&
                piece.currentTile.y == fromPosition.y) {

                //The animation will move "piece" from "fromPosition" to "toPosition" in 1000 milliseconds,
                //starting 200 milliseconds after the start of the animation
                var animation = new Chess.PointAnimation(piece, fromPosition, toPosition, 200, 1000);
                animation.onFinish = function () {

                    //When the animation ends, we inactivate (make invisible) the piece that
                    //may have been lost to the attacking one.
                    $(game.pieces).each(function (index2, piece2) {
                        if (piece2.isActive &&
                            piece2.currentTile.x == toPosition.x &&
                            piece2.currentTile.y == toPosition.y &&
                            piece2.key != piece.key) {

                            piece2.setInactive(false);
                        }
                    });
                }
                //we enqueue the animation, so that it will be processed only when required
                self.animationManager.enqueueAnimation(animation);
            }
        });

        *** SOME POST-PROCESSING CODE HERE ***
    },
        

最终思考

正如你所见,这个项目不是一个原创作品,如果没有 Óscar Toledo 以及他出色的国际象棋工作,项目就不可能实现。如果你读到了最后,非常感谢你的耐心。我希望这篇文章在游戏开发、等距投影和基于 JavaScript 的动画方面对你有所帮助。

历史

  • 2013-08-31:初始版本。
  • 2013-09-05:新的鼠标控件。
© . All rights reserved.