Browse Source

Add initial implementation

master
Kaj Björklund 8 years ago
parent
commit
abbf478c84
  1. 66
      README.md
  2. 20
      build/compile.sh
  3. 3
      build/export.js
  4. 2252
      build/extern-jquery-1.8.js
  5. 17
      build/extern-jquery-ui.js
  6. 52
      build/jsl.conf
  7. 11
      build/lint.sh
  8. 237
      chess.css
  9. 36
      chess.html
  10. BIN
      chess.ico
  11. 28
      chess.min.js
  12. 400
      src/ai.js
  13. 504
      src/bitboard.js
  14. 237
      src/chess.css
  15. 36
      src/chess.html
  16. BIN
      src/chess.ico
  17. 15
      src/chess.include.js
  18. 173
      src/chess.js
  19. 117
      src/move.js
  20. 125
      src/parser.js
  21. 940
      src/position.js
  22. 15
      src/test.html
  23. 190
      src/test.js
  24. 321
      src/ui.js
  25. 144
      src/zobrist.js

66
README.md

@ -1,2 +1,64 @@
chess
=====
# Javascript Chess
This program is a Javascript implementation of the board game [Chess](http://en.wikipedia.org/wiki/Chess), with a computer player opponent. All move types are supported, including en passant, castling and promotion.
Try it out [here](http://www.iki.fi/kbjorklu/chess/). Usage is shown below the chessboard. At least Internet Explorer (8 or later), Chrome and Firefox should work.
# Code Structure
Source files are placed in the `src` directory. Minification and linting files are placed in the `build` directory. Source file contents:
* `chess.js`: Constants, utilities and the Chess namespace.
* `bitboard.js`: 64-bit bit twiddling tools.
* `zobrist.js`: Game state hash calculator. Currently unused, but will be used in the transposition table implementation.
* `move.js`: Piece movement representation.
* `position.js`: Chess game state and mutation.
* `parser.js`: Parser for various Chess notations.
* `ai.js`: Artificial intelligence, i.e. computer opponent. Basic alpha-beta pruned minimax with a simple evaluation function.
* `ui.js`: User interface code.
* `chess.include.js`: Includes all of the above files.
* `chess.css`: User interface style.
* `chess.ico`: Icon.
* `chess.html`: Main game file.
* `test.js`: Automated tests.
* `test.html`: Automated test runner.
# Building
To compile the minified version using `compile.sh` in the `build` directory, you need [bash](http://git-scm.com/download/win) and the [Closure compiler](https://developers.google.com/closure/compiler/). You may need to adjust the `.jar` location in `compile.sh`. Compiled files are placed in the top-level directory.
To lint using `lint.sh` in the build directory, you need bash and [JavaScript Lint](http://www.javascriptlint.com/). You may need to adjust lint's path in `lint.sh`.
To run the tests, open `test.html` in the `src` directory.
# TODO
* Static exchange evaluation
* Transposition table
* Iterative deepening
* Negamax formulation
* AI randomness
* Take game phase into account in evaluation
* Take mobility into account in evaluation
* Killer heuristic
* Late-check castling legality
* Tie-detection
* Move pieces without drag and drop
* Underpromotion
* Show captured pieces in the UI
* Don't hardcode board target div to UI
* UI for loading game state from parsable Chess notation(s)
* More tests
# License
The Chess implementation is distributed under the MIT license. See accompanying file LICENSE for details.
Third-party components are distributed/used under their respective license:
* jQuery: [MIT](https://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt) (used via jQuery CDN)
* jQuery UI: [MIT](https://github.com/jquery/jquery-ui/blob/master/MIT-LICENSE.txt) (used via jQuery CDN)
* jQuery UI Touch Punch: [MIT](https://github.com/furf/jquery-ui-touch-punch/blob/master/jquery.ui.touch-punch.min.js) (used via CloudFlare CDN)
* Augment.js: [MIT](https://github.com/olivernn/augment.js/blob/master/LICENSE) (used via CloudFlare CDN)
* QUnit: [MIT](https://github.com/jquery/qunit/blob/master/MIT-LICENSE.txt) (used via jQuery CDN)
* The jQuery extern file in the build directory: [Apache 2](http://www.apache.org/licenses/LICENSE-2.0)

20
build/compile.sh

@ -0,0 +1,20 @@
#!/bin/bash
COMPILER_JAR="/c/Program Files/closure-compiler/compiler.jar"
src=`sed -E -n "s/^.*\"(\w+\.js)\".*$/--js ..\/src\/\1/p" ../src/chess.include.js`
java -jar "$COMPILER_JAR" \
--language_in ECMASCRIPT5_STRICT \
--compilation_level ADVANCED_OPTIMIZATIONS \
--output_wrapper "(function(){%output%})();" \
--use_types_for_optimization \
--summary_detail_level 3 \
--warning_level VERBOSE \
--js_output_file ../chess.min.js \
--process_jquery_primitives \
--externs extern-jquery-1.8.js \
--externs extern-jquery-ui.js \
$src \
--js export.js
cp ../src/chess.css ..
cp ../src/chess.ico ..
sed "s/chess\.include\.js/chess.min.js/" ../src/chess.html > ../chess.html

3
build/export.js

@ -0,0 +1,3 @@
"use strict";
window["makeChessGame"] = makeChessGame;

2252
build/extern-jquery-1.8.js

File diff suppressed because it is too large

17
build/extern-jquery-ui.js

@ -0,0 +1,17 @@
/** @externs */
/**
* @param {(string|Object.<string, *>)} x
* @param {*=} y
* @param {*=} z
* @return {(!jQuery|*)}
*/
jQuery.prototype.draggable = function(x, y, z) {};
/**
* @param {(string|Object.<string, *>)} x
* @param {*=} y
* @param {*=} z
* @return {(!jQuery|*)}
*/
jQuery.prototype.droppable = function(x, y, z) {};

52
build/jsl.conf

@ -0,0 +1,52 @@
+no_return_value # function {0} does not always return a value
+duplicate_formal # duplicate formal argument {0}
+equal_as_assign # test for equality (==) mistyped as assignment (=)?{0}
+var_hides_arg # variable {0} hides argument
+redeclared_var # redeclaration of {0} {1}
+anon_no_return_value # anonymous function does not always return a value
+missing_semicolon # missing semicolon
+meaningless_block # meaningless block; curly braces have no impact
+comma_separated_stmts # multiple statements separated by commas (use semicolons?)
+unreachable_code # unreachable code
+missing_break # missing break statement
+missing_break_for_last_case # missing break statement for last case in switch
+comparison_type_conv # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==)
+inc_dec_within_stmt # increment (++) and decrement (--) operators used as part of greater statement
+useless_void # use of the void type may be unnecessary (void is always undefined)
+multiple_plus_minus # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs
+use_of_label # use of label
+block_without_braces # block statement without curly braces
+leading_decimal_point # leading decimal point may indicate a number or an object member
+trailing_decimal_point # trailing decimal point may indicate a number or an object member
+octal_number # leading zeros make an octal number
+nested_comment # nested comment
+misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma
+ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement
+empty_statement # empty statement or extra semicolon
+missing_option_explicit # the "option explicit" control comment is missing
+partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag
+dup_option_explicit # duplicate "option explicit" control comment
+useless_assign # useless assignment
+ambiguous_nested_stmt # block statements containing block statements should use curly braces to resolve ambiguity
+ambiguous_else_stmt # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent)
+missing_default_case # missing default case in switch statement
+duplicate_case_in_switch # duplicate case in switch statements
+default_not_at_end # the default case is not at the end of the switch statement
+legacy_cc_not_understood # couldn't understand control comment using /*@[email protected]*/ syntax
+jsl_cc_not_understood # couldn't understand control comment using /*jsl:keyword*/ syntax
+useless_comparison # useless comparison; comparing identical expressions
+with_statement # with statement hides undeclared variables; use temporary variable instead
+trailing_comma_in_array # extra comma is not recommended in array initializers
+assign_to_function_call # assignment to a function call
+parseint_missing_radix # parseInt missing radix parameter
+context
+lambda_assign_requires_semicolon
-legacy_control_comments
-jscript_function_extensions
+always_use_option_explicit
+define window
+define document
+define $
+define alert

11
build/lint.sh

@ -0,0 +1,11 @@
#!/bin/bash
LINT="/c/Program Files (x86)/jsl-0.3.0/jsl.exe"
src=`sed -E -n "s/^.*\"(\w+\.js)\".*$/..\/src\/\1/p" ../src/chess.include.js`
echo "\"use strict\";" > chess.max.js
sed "s/^.use strict..$//g" $src >> chess.max.js
sed -i "/^$/N;/^\n$/D" chess.max.js
sed -i "s/\t/ /g" chess.max.js
"$LINT" -nologo -nofilelisting -conf jsl.conf -process chess.max.js
rm chess.max.js

237
chess.css

@ -0,0 +1,237 @@
body {
width: 960px;
margin-left: auto;
margin-right: auto;
text-align: left;
font-size: 12pt;
font-family: Arial, sans-serif;
color: black;
background: #303030 repeat fixed url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAYAAABWKLW/AAAAFUlEQVQImWNgYGD4D8UMDEgMFIH/AGu7Bfvz8g82AAAAAElFTkSuQmCC");
}
h1 {
text-transform: lowercase;
text-shadow: 0 1px 0 white;
letter-spacing: 0.5ex;
font-weight: bold;
font-size: 100%;
background: white;
text-shadow: 0 0 5px black;
margin: 0;
padding: 14px;
border-top-left-radius: 14px;
border-top-right-radius: 14px;
}
#content {
background: white;
padding: 14px 40px;
}
#help {
cursor: help;
}
#dim {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
z-index: 10;
display: none;
cursor: wait;
}
#chessboard {
width: 700px;
height: 700px;
float: left;
padding: 0;
}
#moves {
border: 1px solid silver;
border-radius: 14px;
width: 138px;
/* max-width: 168px; */
padding: 5px;
padding-left: 15px;
float: right;
overflow: auto;
height: 688px;
}
#moves button {
width: 100%;
}
#clear {
clear: both;
width: 0;
height: 0;
}
#footer {
background: white;
margin: 0;
padding: 14px;
border-bottom-left-radius: 14px;
border-bottom-right-radius: 14px;
}
#chessboard table {
border-spacing: 0;
border-collapse: collapse;
border: none;
cursor: default;
/* see http://goo.gl/1dTy7 (css rule to disable text selection highlighting) */
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#chessboard table tr th, #chessboard table tr td {
padding: 0;
margin: 0;
text-align: center;
vertical-align: middle;
}
#chessboard table tr th {
background: silver;
font-size: small;
font-weight: normal;
}
#chessboard table tr th.file {
width: 80px;
height: 30px;
}
#chessboard table tr th.rank {
width: 30px;
height: 80px;
}
#chessboard table tr:first-child th:first-child {
border-top-left-radius: 14px;
}
#chessboard table tr:first-child th:last-child {
border-top-right-radius: 14px;
}
#chessboard table tr:last-child th:first-child {
border-bottom-left-radius: 14px;
}
#chessboard table tr:last-child th:last-child {
border-bottom-right-radius: 14px;
}
#chessboard table tr td {
width: 80px;
height: 80px;
}
#chessboard table tr td.light {
text-shadow: 0 0 10px black;
background: #E0E0E0;
background: -moz-linear-gradient(-45deg, #ffffff 0%, #c0c0c0 100%);
background: -webkit-gradient(linear, left top, right bottom, color-stop(0%, #ffffff), color-stop(100%, #c0c0c0));
background: -webkit-linear-gradient(-45deg, #ffffff 0%, #c0c0c0 100%);
background: -o-linear-gradient(-45deg, #ffffff 0%, #c0c0c0 100%);
background: -ms-linear-gradient(-45deg, #ffffff 0%, #c0c0c0 100%);
background: linear-gradient(135deg, white, silver);
}
#chessboard table tr td.dark {
text-shadow: 0 0 10px white;
background: #404040;
background: -moz-linear-gradient(-45deg, #808080 0%, #000000 100%);
background: -webkit-gradient(linear, left top, right bottom, color-stop(0%, #808080), color-stop(100%, #000000));
background: -webkit-linear-gradient(-45deg, #808080 0%, #000000 100%);
background: -o-linear-gradient(-45deg, #808080 0%, #000000 100%);
background: -ms-linear-gradient(-45deg, #808080 0%, #000000 100%);
background: linear-gradient(135deg, gray, black);
}
#chessboard table tr td div {
font-size: 50px;
}
#chessboard table tr td.white {
color: white;
}
#chessboard table tr td.black {
color: black;
}
#chessboard table tr td.from {
font-weight: bold;
}
#chessboard table tr td.to {
box-shadow: inset 0 0 10px 1px green;
}
#chessboard table tr td.to.capture {
box-shadow: inset 0 0 10px 1px red;
}
#chessboard table tr td.to.en-passant:after {
color: red;
content: "e.p.";
}
#chessboard table tr td.to.king-castle:after {
color: magenta;
content: "0-0";
}
#chessboard table tr td.to.queen-castle:after {
color: magenta;
content: "0-0-0";
}
#chessboard table tr td.to.positional:after, #chessboard table tr td.to.double-push:after {
color: gray;
content: "\2022";
}
#chessboard table tr td.turn {
cursor: move;
}
#chessboard table tr td div.turn:not(.can-move) {
cursor: not-allowed;
}
#chessboard table tr td.last-move {
box-shadow: inset 0 0 10px 1px yellow;
}
#moves a {
color: gray;
font-size: 8pt;
text-decoration: none;
}
#moves a.cannot {
color: silver;
pointer-events: none;
cursor: default;
}

36
chess.html

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="x-ua-compatible" content="IE=edge">
<meta charset="utf-8">
<title>Chess</title>
<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.0/themes/base/jquery-ui.css">
<link rel="stylesheet" href="chess.css">
<link rel="shortcut icon" href="chess.ico">
</head>
<body>
<h1>Chess</h1>
<div id="content">
<div id="chessboard" role="main"></div>
<div id="moves"></div>
<div id="clear"></div>
<div id="help">
<ul>
<li>Moves can be made by dragging the pieces on the board, or by clicking the move links in the right panel.</li>
<li>Pawns dragged to the last rank are promoted to queens. Use the move links in the right panel to underpromote.</li>
<li>Castle by moving the king two squares towards a rook. The rook moves automatically.</li>
</ul>
</div>
</div>
<div id="footer"></div>
<div id="dim"></div>
<script src="http://code.jquery.com/jquery-1.9.0.min.js"></script>
<script src="http://code.jquery.com/ui/1.10.0/jquery-ui.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.2/jquery.ui.touch-punch.min.js"></script>
<!--[if lt IE 9]><script src="http://cdnjs.cloudflare.com/ajax/libs/augment.js/1.0.0/augment.min.js"></script><![endif]-->
<script src="chess.min.js"></script>
<script>
$(makeChessGame);
</script>
</body>
</html>

BIN
chess.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

28
chess.min.js

@ -0,0 +1,28 @@
(function(){'use strict';var f=!0,j=null,m=!1,aa="pawn knight bishop rook queen king".split(" ");function ba(a,b){return"abcdefgh"[b]+"12345678"[a]}function ca(a){return"abcdefgh".indexOf(a[0])+8*"12345678".indexOf(a[1])}function q(a){return ba(a>>>3,a&7)};function da(a,b){this.a=a>>>0;this.b=b>>>0}function ea(a){a>>>=0;a-=a>>>1&1431655765;a=(a&858993459)+(a>>>2&858993459);return 16843009*(a+(a>>>4)&252645135)>>>24}function fa(a){a>>>=0;return(a&a-1)>>>0}function ga(a){a>>>=0;return ea((a&-a)-1)}function r(a){return ea(a.a)+ea(a.b)}function ha(a){return a.a?ga(a.a):32+ga(a.b)}function s(a){var b=ha(a);a.a?a.a=fa(a.a):a.b=fa(a.b);return b}function t(a){return!a.a&&!a.b}function u(a,b){b>>>=0;return 32>b?!(a.a&1<<b):!(a.b&1<<b-32)}
function ia(a,b){return!u(a,b)}function y(a,b){b>>>=0;32>b?a.a=(a.a|1<<b)>>>0:a.b=(a.b|1<<b-32)>>>0;return a}function ja(a,b){b>>>=0;32>b?a.a=(a.a&~(1<<b))>>>0:a.b=(a.b&~(1<<b-32))>>>0}function A(a,b){a.a=(a.a&b.a)>>>0;a.b=(a.b&b.b)>>>0;return a}function B(a,b){a.a=(a.a&~b.a)>>>0;a.b=(a.b&~b.b)>>>0;return a}function C(a,b){a.a=(a.a|b.a)>>>0;a.b=(a.b|b.b)>>>0;return a}function ka(a,b){a.a=(a.a^b.a)>>>0;a.b=(a.b^b.b)>>>0}function la(a){a.a=~a.a>>>0;a.b=~a.b>>>0;return a}
function D(a,b){b>>>=0;31<b?(a.b=a.a<<b-32>>>0,a.a=0):0<b&&(a.b=(a.b<<b|a.a>>>32-b)>>>0,a.a=a.a<<b>>>0);return a}function E(a,b){b>>>=0;31<b?(a.a=a.b>>>b-32,a.b=0):0<b&&(a.a=(a.a>>>b|a.b<<32-b)>>>0,a.b>>>=b);return a}function F(a,b){63<b||-63>b?a.a=a.b=0:0<b?D(a,b):0>b&&E(a,-b);return a}function ma(a,b){return a.a===b.a&&a.b===b.b}function G(a){return I(a.a,a.b)}function I(a,b){return new da(a,b)}function J(a){return y(I(0,0),a)}
function na(){var a=oa;return F(A(I(270549120,16909320),F(I(4294967295,4294967295),8*a)),a)}function pa(){var a=qa;return F(A(I(134480385,2151686160),F(I(4294967295,4294967295),8*-a)),a)}function ra(){var a=y(I(0,0),sa),b=B(E(G(a),1),K[7]),c=B(B(E(G(a),2),K[7]),K[6]),d=B(D(G(a),1),K[0]),a=B(B(D(G(a),2),K[0]),K[1]),c=C(c,a),b=C(b,d);return C(C(C(D(G(c),8),E(c,8)),D(G(b),16)),E(b,16))}
function ta(){var a=y(I(0,0),ua),b=C(B(E(G(a),1),K[7]),B(D(G(a),1),K[0])),c=E(C(G(a),b),8),a=D(C(G(a),b),8);return C(C(b,c),a)}for(var va=I(4294967295,4294967295),wa=I(1437226410,1437226410),xa=I(2857740885,2857740885),ya=[],za=0;8>za;++za)ya.push(D(I(16843009,16843009),za));for(var K=ya,L=[],Aa=0;8>Aa;++Aa)L.push(D(I(255,0),8*Aa));for(var Ba=[],oa=-7;8>oa;++oa)Ba.push(na());for(var Ca=[],qa=-7;8>qa;++qa)Ca.push(pa());for(var Da=[],sa=0;64>sa;++sa)Da.push(ra());for(var Ea=[],ua=0;64>ua;++ua)Ea.push(ta());function Fa(){this.a=this.b=0}for(var Ga=[],Ha=0;1586>Ha;++Ha)Ga.push(1+4294967295*Math.random()>>>0);function M(a,b){a.b=(a.b^Ga[b])>>>0;a.a=(a.a^Ga[b+1])>>>0}function N(a,b){0<=b&&M(a,1570+(b&7))};function Ia(a,b,c,d,g){this.a=b&63|(a&63)<<6|(c&15)<<12|(d&7)<<16|((g|0)&7)<<19}function O(a){return a.a>>>6&63}function P(a){return a.a>>>12&15}function Q(a){return a.a>>>16&7}function Ja(a){return 2===P(a)||3===P(a)}function Ka(a){return 5!==P(a)?a.a&63:(a.a&63)+(O(a)<(a.a&63)?-8:8)}function La(a){return!Ja(a)?" NBRQK".charAt(Q(a))+q(O(a))+(P(a)&4?"x":"-")+q(a.a&63)+(5===P(a)?"e.p.":"")+(P(a)&8?" NBRQK".charAt(P(a)&8?1+(P(a)&3):0):""):"0-0"+(3===P(a)?"-0":"")};function Ma(){this.c=new Fa;this.a=[C(G(L[1]),L[6]),C(C(C(J(1),J(6)),J(57)),J(62)),C(C(C(J(2),J(5)),J(58)),J(61)),C(C(C(J(0),J(7)),J(56)),J(63)),C(J(3),J(59)),C(J(4),J(60)),C(G(L[0]),L[1]),C(G(L[6]),L[7])];this.f=[];this.b=0;this.e=15;this.d=-1;this.h=0;this.g=[];this.i=[];for(var a=this.f.length=0;64>a;++a){var b;a:{for(b=0;5>=b;++b)if(!u(this.a[b],a))break a;b=j}this.f.push(b)}this.c=new Fa;this.b&&M(this.c,0);for(a=0;1>=a;++a)for(b=0;5>=b;++b)for(var c=this.c,d=b,g=a,e=R(this,b,a),e=G(e);!t(e);){var h=
s(e);M(c,2+d+6*g+12*h)}M(this.c,1538+this.e);N(this.c,this.d)}var S=[7,63,0,56],Na=[la(D(I(16843009,16843009),7)),va,la(D(I(16843009,16843009),0))];
function T(a,b,c){function d(a,b,c){for(;!t(b);){var d=s(b);u(ib,d)&&h.push(new Ia(a,d,!u(v,d)?4:0,c,H.f[d]))}}function g(a,b,c){e(G(a),b,c?15:11);e(G(a),b,c?14:10);e(G(a),b,c?13:9);e(G(a),b,c?12:8)}function e(a,b,c){for(;!t(a);){var d=s(a);h.push(new Ia(d-b,d,c,0,H.f[d]))}}c=!!c;var h=[],k=a.b,v=a.a[6+(k^1)],l=U(a),H=a,n=1-2*k,x=8*n,w=R(a,0,k),p=L[k?0:7];if(!c){var z=B(B(F(A(G(w),L[k?6:1]),2*x),l),F(G(l),x));e(z,2*x,1);z=B(F(G(w),x),l);e(B(G(z),p),x,0);g(A(G(z),p),x,m)}var Ra=K[k?7:0],z=x-n,Y=A(F(B(G(w),
Ra),z),v);e(B(G(Y),p),z,4);g(A(G(Y),p),z,f);var Y=K[k?0:7],x=x+n,Sa=A(F(B(G(w),Y),x),v);e(B(G(Sa),p),x,4);g(A(G(Sa),p),x,f);0<=a.d&&(p=F(B(A(J(a.d+n),w),Ra),z),e(p,z,5),n=F(B(A(J(a.d-n),w),Y),x),e(n,x,5));for(var ib=a.a[6+k],n=c?v:va,w=G(R(a,1,k));!t(w);)p=s(w),d(p,A(G(Da[p]),n),1);for(w=G(R(a,4,k));!t(w);)p=s(w),d(p,A(C(Oa(J(p),l),Pa(J(p),l)),n),4);for(w=G(R(a,2,k));!t(w);)p=s(w),d(p,A(Oa(J(p),l),n),2);for(w=G(R(a,3,k));!t(w);)p=s(w),d(p,A(Pa(J(p),l),n),3);l=ha(R(a,5,k));d(l,A(G(Ea[l]),n),5);c||
(Qa(a,k,f)&&h.push(new Ia(l,l+2,2,5,j)),Qa(a,k,m)&&h.push(new Ia(l,l-2,3,5,j)));c=h;return b?c:c.filter(Ma.prototype.j,a)}function R(a,b,c){return A(G(a.a[b]),a.a[6+c])}function U(a){return C(G(a.a[6]),a.a[7])}function V(a){return Ta(a,a.b^1,ha(R(a,5,a.b)))}function Ua(a,b){var c=0===a,d=F(B(G(b),K[0]),c?7:-9),c=F(B(G(b),K[7]),c?9:-7);return C(d,c)}function W(a,b,c,d){var g=I(0,0);c=8*c+d;d=Na[1+d];for(F(a,c);!t(A(a,d));F(B(a,b),c))C(g,a);return g}
function Oa(a,b){return C(C(C(W(G(a),b,1,1),W(G(a),b,1,-1)),W(G(a),b,-1,1)),W(G(a),b,-1,-1))}function Pa(a,b){return C(C(C(W(G(a),b,0,1),W(G(a),b,0,-1)),W(G(a),b,1,0)),W(G(a),b,-1,0))}function Ta(a,b,c){var d=R(a,0,b);if(ia(Ua(b,d),c))return f;d=R(a,1,b);if(!t(A(G(Da[c]),d)))return f;d=R(a,5,b);if(!t(A(G(Ea[c]),d)))return f;var d=U(a),g=R(a,4,b),e=C(G(R(a,2,b)),g);if(ia(Oa(e,d),c))return f;a=C(G(R(a,3,b)),g);return ia(Pa(a,d),c)?f:m}
function X(a,b,c){M(a.c,1538+a.e);a.e&=~(1<<b+(c?0:2));M(a.c,1538+a.e)}function Qa(a,b,c){if(0===(a.e&1<<b+(c?0:2)))return m;var d=c?1:-1,g=0===b?4:60,e=U(a);if(!(d=!u(e,g+d)||!u(e,g+2*d)||!c&&!u(e,g+3*d)))d=b^1,c=c?1:-1,b=0===b?4:60,d=!(!Ta(a,d,b)&&!Ta(a,d,b+c)&&!Ta(a,d,b+2*c));return d?m:f}function Va(a){if(!t(a.a[0])||!t(a.a[3])||!t(a.a[4]))return m;if(4>r(a.a[6])+r(a.a[7]))return f;if(!t(a.a[1]))return m;a=a.a[2];return ma(A(G(a),wa),a)||ma(A(G(a),xa),a)?f:m}
function Wa(a,b,c,d,g){var e=C(J(d),J(g));ka(a.a[b],e);ka(a.a[6+c],e);a.f[d]=j;a.f[g]=b;M(a.c,2+b+6*c+12*d);M(a.c,2+b+6*c+12*g)}function Xa(a,b,c,d,g){ja(a.a[b],g);y(a.a[c],g);a.f[g]=c;M(a.c,2+b+6*d+12*g);M(a.c,2+c+6*d+12*g)}function Ya(a,b){if(P(b)&4){var c=b.a>>>19&7,d=a.b^1,g=Ka(b);ja(a.a[c],g);ja(a.a[6+d],g);a.f[g]=j;M(a.c,2+c+6*d+12*g)}Ja(b)&&(c=a.b,d=2===P(b),g=S[c+(d?0:2)],Wa(a,3,c,g,g+(d?-2:3)));Wa(a,Q(b),a.b,O(b),b.a&63);P(b)&8&&Xa(a,0,P(b)&8?1+(P(b)&3):0,a.b,b.a&63)}
function Za(a,b){P(b)&8&&Xa(a,P(b)&8?1+(P(b)&3):0,0,a.b,b.a&63);Wa(a,Q(b),a.b,b.a&63,O(b));if(Ja(b)){var c=a.b,d=2===P(b),g=S[c+(d?0:2)];Wa(a,3,c,g+(d?-2:3),g)}P(b)&4&&(c=b.a>>>19&7,d=a.b^1,g=Ka(b),y(a.a[c],g),y(a.a[6+d],g),a.f[g]=c,M(a.c,2+c+6*d+12*g))}Ma.prototype.j=function(a){Ya(this,a);var b=V(this);Za(this,a);return!b};
function Z(a,b){Ya(a,b);if(V(a))return Za(a,b),m;a.g.push(b);a.i.push(a.d);a.i.push(a.e);a.i.push(a.h);N(a.c,a.d);a.d=1===P(b)?b.a&63:-1;N(a.c,a.d);var c=a.b;5===Q(b)?(X(a,c,f),X(a,c,m)):3===Q(b)&&(O(b)===S[c+0]?X(a,c,f):O(b)===S[c+2]&&X(a,c,m));c^=1;3===(b.a>>>19&7)&&(Ka(b)===S[c+0]?X(a,c,f):Ka(b)===S[c+2]&&X(a,c,m));P(b)&4||0===Q(b)?a.h=0:++a.h;a.b=c;M(a.c,0);return f}
function $a(a){if(a.g.length){var b=a.g.pop();a.b^=1;M(a.c,0);Za(a,b);a.i.pop();M(a.c,1538+a.e);a.e=a.i.pop();M(a.c,1538+a.e);N(a.c,a.d);a.d=a.i.pop();N(a.c,a.d)}};var ab=[100,300,300,500,900,2E4],bb=[[0,0,0,0,0,0,0,0,50,50,50,50,50,50,50,50,10,10,20,30,30,20,10,10,5,5,10,25,25,10,5,5,0,0,0,20,20,0,0,0,5,-5,-10,0,0,-10,-5,5,5,10,10,-20,-20,10,10,5,0,0,0,0,0,0,0,0],[-50,-40,-30,-30,-30,-30,-40,-50,-40,-20,0,0,0,0,-20,-40,-30,0,10,15,15,10,0,-30,-30,5,15,20,20,15,5,-30,-30,0,15,20,20,15,0,-30,-30,5,10,15,15,10,5,-30,-40,-20,0,5,5,0,-20,-40,-50,-40,-30,-30,-30,-30,-40,-50],[-20,-10,-10,-10,-10,-10,-10,-20,-10,0,0,0,0,0,0,-10,-10,0,5,10,10,5,0,-10,-10,5,5,10,10,
5,5,-10,-10,0,10,10,10,10,0,-10,-10,10,10,10,10,10,10,-10,-10,5,0,0,0,0,5,-10,-20,-10,-10,-10,-10,-10,-10,-20],[0,0,0,0,0,0,0,0,5,10,10,10,10,10,10,5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,0,0,0,5,5,0,0,0],[-20,-10,-10,-5,-5,-10,-10,-20,-10,0,0,0,0,0,0,-10,-10,0,5,5,5,5,0,-10,-5,0,5,5,5,5,0,-5,0,0,5,5,5,5,0,-5,-10,5,5,5,5,5,0,-10,-10,0,5,0,0,0,0,-10,-20,-10,-10,-5,-5,-10,-10,-20],[-30,-40,-40,-50,-50,-40,-40,-30,-30,-40,-40,-50,-50,-40,-40,-30,-30,
-40,-40,-50,-50,-40,-40,-30,-30,-40,-40,-50,-50,-40,-40,-30,-20,-30,-30,-40,-40,-30,-30,-20,-10,-20,-20,-20,-20,-20,-20,-10,20,20,0,0,0,0,20,20,20,30,10,0,0,10,30,20]],cb=ab[0]/2;function db(a,b){for(var c=0,d=0;5>d;++d)c+=r(R(a,d,b))*ab[d];1<r(R(a,2,b))&&(c+=cb);return c}function eb(a,b){for(var c=0,d=0;5>=d;++d)for(var g=G(R(a,d,b));!t(g);)var e=s(g),c=c+bb[d][b?e:56^e];return c}
function fb(a,b){var c=a.a[6+b],d=a.a[6+(b^1)],g=U(a),e=la(U(a)),h;h=0;var k,v=R(a,0,b),l=0===b;k=A(F(G(v),l?8:-8),e);e=A(A(F(A(G(v),L[l?1:6]),l?16:-16),e),F(G(e),l?8:-8));k=C(k,e);h=h+r(k);h+=r(A(Ua(b,R(a,0,b)),d));for(d=G(R(a,1,b));!t(d);)h+=r(B(G(Da[s(d)]),c));h+=r(B(G(Ea[ha(R(a,5,b))]),c));d=R(a,4,b);k=C(G(R(a,2,b)),d);h+=r(B(Oa(k,g),c));d=C(G(R(a,3,b)),d);h+=r(B(Pa(d,g),c));return h*ab[0]/100};function gb(){this.a=new Ma}function hb(){$("#chessboard table tr td, #chessboard table tr td div").removeClass("from to positional capture double-push en-passant promotion castle king-castle queen-castle")}function jb(){$("#chessboard table tr td div.ui-draggable").draggable("destroy");$("#chessboard table tr td.ui-droppable").droppable("destroy")}
function kb(a){$("#moves").html("");var b=$("#dim");b.fadeIn(function(){function c(a,b,e,h){if(1>b)return d(a,e,h);for(var p=g(T(a,f,m)),k=0===a.b,l=m,n=0;n<p.length;++n)if(Z(a,p[n])){var l=f,z=c(a,b-1,e,h);$a(a);k?e=z>e?z:e:h=z<h?z:h;if(h<=e)break}if(!l){if(!V(a))return 0;a=ab[5];return k?-a:a}return 100<=a.h||Va(a)?0:k?e:h}function d(a,b,c){if(100<=a.h||Va(a))return 0;var e=db(a,0)-db(a,1)+(eb(a,0)-eb(a,1));++k;var h=0===a.b;if(h){if(e>=c)return c;b=e>b?e:b}else{if(e<=b)return b;c=e<c?e:c}for(var e=
g(T(a,f,!V(a))),p=0;p<e.length;++p)if(Z(a,e[p])){var n=d(a,b,c);$a(a);if(h){if(n>=c)return c;b=n>b?n:b}else{if(n<=b)return b;c=n<c?n:c}}return h?b:c}function g(a){function b(a){var c=P(a)&4?(1+(a.a>>>19&7))/(1+Q(a)):0,c=6*c+Q(a),c=16*c+P(a),c=64*c+(a.a&63);return c=64*c+O(a)}a.sort(function(a,c){return b(c)-b(a)});return a}var e,h=a.a,k=0;e=j;for(var v=-Infinity,l=Infinity,H=g(T(h,f)),n=0;n<H.length;++n)if(Z(h,H[n])){var x=c(h,3,v,l);$a(h);0===h.b?x>v&&(v=x,e=H[n]):x<l&&(l=x,e=H[n])}window.console.log("Evaluations: "+
k+", result move: "+La(e)+", alpha: "+v+", beta: "+l);if(!e)throw Error("Move not found");Z(a.a,e);h=$("#"+q(O(e)));e=$("#"+q(e.a&63));var w=e.offset().left-h.offset().left,p=e.offset().top-h.offset().top,z=h.children("div");z.css({position:"relative",top:"0px",left:"0px"});b.fadeOut(function(){z.animate({top:p+"px",left:w+"px"},function(){lb(a)})})})}
function lb(a){window.console.log("Moves: "+a.a.g.length+", white material: "+db(a.a,0)+", black material: "+db(a.a,1)+", white mobility: "+fb(a.a,0)+", black mobility: "+fb(a.a,1)+", white location: "+eb(a.a,0)+", black location: "+eb(a.a,1));hb();jb();$("#chessboard table tr td div").remove();$("#chessboard table tr td").removeClass("white black turn last-move "+aa.join(" "));for(var b=a.a.a[6],c=a.a.a[7],d=0;64>d;++d)for(var g=$("#"+q(d)),e=0;5>=e;++e)if(!u(a.a.a[e],d)){var h=0===a.a.b?!u(b,d):
!u(c,d),k=$("<div>");k.attr("title",g.attr("title")+"\nPiece: "+aa[e]+"\nColor: "+(!u(b,d)?"white":"black"));k.text("\u2659\u265f\u2658\u265e\u2657\u265d\u2656\u265c\u2655\u265b\u2654\u265a".charAt(2*e+(!u(b,d)?0:1)));var v=k.add(g);v.addClass(aa[e]);v.toggleClass("white",!u(b,d));v.toggleClass("black",!u(c,d));v.toggleClass("turn",h);g.append(k);break}b=!a.a.g.length?j:a.a.g[a.a.g.length-1];b!==j&&($("#"+q(O(b))).addClass("last-move"),$("#"+q(b.a&63)).addClass("last-move"));b=!T(a.a).length?V(a.a)?
1:2:100<=a.a.h?3:Va(a.a)?5:0;if(0===b&&1===a.a.b)kb(a);else{var l=T(a.a);$("#moves").html('<a href="#" id="undo" class="'+(a.a.g.length?"can":"cannot")+'">undo</a><br/><a href="#" id="auto" class="'+(0<l.length?"can":"cannot")+'">auto</a><br/>'+l.map(function(a,b){return'<a href="#" id="'+b+'">'+La(a)+"</a><br/>"}).join(""));$("#chessboard table tr td, #chessboard table tr td div").removeClass("can-move");l.forEach(function(a){a=$("#"+q(O(a)));a.add(a.children()).addClass("can-move")});var H=m;$("#chessboard table tr td div.can-move").mouseenter(function(){if(!H){var b=
$(this),c=b.parent(),d=ca(""+c.attr("id")),c=c.add(b);c.toggleClass("from",l.some(function(a){return O(a)===d}));c.hasClass("from")&&(l.forEach(function(a){if(O(a)===d){var b=$("#"+q(a.a&63)),b=b.add(b.children());b.addClass("to");b.addClass(0===P(a)?"positional":"");b.addClass(P(a)&4?"capture":"");b.addClass(1===P(a)?"double-push":"");b.addClass(5===P(a)?"en-passant":"");b.addClass(P(a)&8?"promotion":"");b.addClass(Ja(a)?"castle":"");b.addClass(2===P(a)?"king-castle":"");b.addClass(3===P(a)?"queen-castle":
"")}}),jb(),$("#chessboard table tr td.to").droppable({drop:function(){var b=ca(""+$(this).attr("id")),c=l.filter(function(a){return O(a)===d&&(a.a&63)===b});0<c.length?(Z(a.a,c[0]),lb(a)):(hb(),jb())}}),b.draggable({start:function(){H=f},stop:function(){H=m},containment:"#chessboard table",zIndex:10,revert:"invalid"}))}}).mouseleave(function(){H||hb()});$("#moves a").click(function(){var b=$(this).attr("id");"undo"===b?($a(a.a),$a(a.a),lb(a)):"auto"===b?kb(a):(Z(a.a,l[parseInt(b,10)]),lb(a))});$("#dim").css({display:"none"});
1===b?$("#moves").append("&#35;<br/>"+(a.a.b?"1-0":"0-1")):0!==b&&$("#moves").append("&frac12;-&frac12;")}};window.makeChessGame=function(){var a=$("<table>"),b="<tr><th></th>"+"abcdefgh".split("").map(function(a){return'<th class="file">'+a+"</th>"}).join("")+"<th></th></tr>";a.append(b);for(var c=0;8>c;++c){var d=7-c,g=$("<tr>");a.append(g);var e='<th class="rank">'+(8-c)+"</th>";g.append(e);for(var h=0;8>h;++h){var k=$("<td>"),v=(d+h)%2?"light":"dark";k.attr("id",ba(d,h));k.attr("title","Algebraic: "+ba(d,h)+"\nRank: "+d+"\nFile: "+h+"\nIndex: "+(h+8*d)+"\nColor: "+v);k.addClass(v);g.append(k)}g.append(e)}a.append(b);
$("#chessboard").append(a);lb(new gb)};})();

400
src/ai.js

@ -0,0 +1,400 @@
"use strict";
/**
* AI (artificial intelligence) is a computer player for chess.
* The implementation is an alpha-beta pruned minimax with a simple evaluation function.
* AI takes a chess position, evaluates possible moves up to a certain depth, and returns the move it considers best (or null if the game is lost).
* @constructor
* TODO: add some sort of randomness; per side so that two computers playing against each other act differently (and don't know how the other is acting).
* TODO: static exchange evaluation (see)
* TODO: transposition table
* TODO: iterative deepening
* TODO: negamax formulation
*/
Chess.AI = function() {
};
/**
* @const
* @type {!Array.<number>}
* @see http://goo.gl/zxAE9 (Chess piece relative value)
*/
Chess.AI.PIECE_VALUES = [100, 300, 300, 500, 900, 20000];
/**
* @const
* @type {!Array.<!Array.<number>>}
* @see http://goo.gl/X326e (Simplified evaluation function)
*/
Chess.AI.PIECE_SQUARE_TABLES = [
// pawn
[
0, 0, 0, 0, 0, 0, 0, 0,
50, 50, 50, 50, 50, 50, 50, 50,
10, 10, 20, 30, 30, 20, 10, 10,
5, 5, 10, 25, 25, 10, 5, 5,
0, 0, 0, 20, 20, 0, 0, 0,
5, -5, -10, 0, 0, -10, -5, 5,
5, 10, 10, -20, -20, 10, 10, 5,
0, 0, 0, 0, 0, 0, 0, 0
],
// knight
[
-50, -40, -30, -30, -30, -30, -40, -50,
-40, -20, 0, 0, 0, 0, -20, -40,
-30, 0, 10, 15, 15, 10, 0, -30,
-30, 5, 15, 20, 20, 15, 5, -30,
-30, 0, 15, 20, 20, 15, 0, -30,
-30, 5, 10, 15, 15, 10, 5, -30,
-40, -20, 0, 5, 5, 0, -20, -40,
-50, -40, -30, -30, -30, -30, -40, -50
],
// bishop
[
-20, -10, -10, -10, -10, -10, -10, -20,
-10, 0, 0, 0, 0, 0, 0, -10,
-10, 0, 5, 10, 10, 5, 0, -10,
-10, 5, 5, 10, 10, 5, 5, -10,
-10, 0, 10, 10, 10, 10, 0, -10,
-10, 10, 10, 10, 10, 10, 10, -10,
-10, 5, 0, 0, 0, 0, 5, -10,
-20, -10, -10, -10, -10, -10, -10, -20
],
// rook
[
0, 0, 0, 0, 0, 0, 0, 0,
5, 10, 10, 10, 10, 10, 10, 5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
0, 0, 0, 5, 5, 0, 0, 0
],
// queen
[
-20, -10, -10, -5, -5, -10, -10, -20,
-10, 0, 0, 0, 0, 0, 0, -10,
-10, 0, 5, 5, 5, 5, 0, -10,
-5, 0, 5, 5, 5, 5, 0, -5,
0, 0, 5, 5, 5, 5, 0, -5,
-10, 5, 5, 5, 5, 5, 0, -10,
-10, 0, 5, 0, 0, 0, 0, -10,
-20, -10, -10, -5, -5, -10, -10, -20
],
// king middle game
[
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-20, -30, -30, -40, -40, -30, -30, -20,
-10, -20, -20, -20, -20, -20, -20, -10,
20, 20, 0, 0, 0, 0, 20, 20,
20, 30, 10, 0, 0, 10, 30, 20
]/*,
// king end game
[
-50, -40, -30, -20, -20, -30, -40, -50,
-30, -20, -10, 0, 0, -10, -20, -30,
-30, -10, 20, 30, 30, 20, -10, -30,
-30, -10, 30, 40, 40, 30, -10, -30,
-30, -10, 30, 40, 40, 30, -10, -30,
-30, -10, 20, 30, 30, 20, -10, -30,
-30, -30, 0, 0, 0, 0, -30, -30,
-50, -30, -30, -30, -30, -30, -30, -50
]*/
];
/**
* @const
* @type {number}
* @see http://goo.gl/adkwe (Bishop pair)
*/
Chess.AI.BISHOP_PAIR_VALUE = Chess.AI.PIECE_VALUES[Chess.Piece.PAWN] / 2;
/**
* @param {!Chess.Position} chessPosition
* @param {!Chess.PieceColor} color
* @return {number}
*/
Chess.AI.getMaterialValue = function(chessPosition, color) {
var value = 0;
for (var piece = Chess.Piece.PAWN; piece < Chess.Piece.KING; ++piece) {
value += chessPosition.getPieceColorBitboard(piece, color).popcnt() * Chess.AI.PIECE_VALUES[piece];
}
if (chessPosition.getPieceColorBitboard(Chess.Piece.BISHOP, color).popcnt() > 1) {
value += Chess.AI.BISHOP_PAIR_VALUE;
}
return value;
};
/**
* @param {!Chess.Position} chessPosition
* @return {number}
*/
Chess.AI.evaluateMaterial = function(chessPosition) {
return Chess.AI.getMaterialValue(chessPosition, Chess.PieceColor.WHITE) - Chess.AI.getMaterialValue(chessPosition, Chess.PieceColor.BLACK);
};
/**
* @param {!Chess.Position} chessPosition
* @param {!Chess.PieceColor} color
* @return {number}
* TODO: game phase
*/
Chess.AI.getPieceSquareValue = function(chessPosition, color) {
var value = 0;
for (var piece = Chess.Piece.PAWN; piece <= Chess.Piece.KING; ++piece) {
var pieces = chessPosition.getPieceColorBitboard(piece, color).dup();
while (!pieces.isEmpty()) {
var index = pieces.extractLowestBitPosition();
value += Chess.AI.PIECE_SQUARE_TABLES[piece][color ? index : (56 ^ index)];
}
}
return value;
};
/**
* @param {!Chess.Position} chessPosition
* @return {number}
*/
Chess.AI.evaluateLocations = function(chessPosition) {
return Chess.AI.getPieceSquareValue(chessPosition, Chess.PieceColor.WHITE) - Chess.AI.getPieceSquareValue(chessPosition, Chess.PieceColor.BLACK);
};
/**
* @param {!Chess.PieceColor} color white = attacks by white pieces
* @param {!Chess.Bitboard} pawns
* @param {!Chess.Bitboard} empty
* @return {!Chess.Bitboard}
*/
Chess.AI.makePawnPositionalMask = function(color, pawns, empty) {
var white = (color === Chess.PieceColor.WHITE);
var positional = pawns.dup().shiftLeft(white ? 8 : -8).and(empty);
var doublePush = pawns.dup().and(Chess.Bitboard.RANKS[white ? 1 : 6]).shiftLeft(white ? 16 : -16).and(empty).and(empty.dup().shiftLeft(white ? 8 : -8));
return positional.or(doublePush);
};
/**
* @param {!Chess.Position} chessPosition
* @param {!Chess.PieceColor} color
* @return {number}
* TODO: it's easy to give bonuses for attack and defend here by and(us) or and(opponent)
* TODO: legality
* TODO: does not count all moves; e.g. two pawns can capture the same square, ditto two rooks, two queens
*/
Chess.AI.getMobilityValue = function(chessPosition, color) {
var us = chessPosition.getColorBitboard(color);
var opponent = chessPosition.getColorBitboard(Chess.getOtherPieceColor(color));
var occupied = chessPosition.getOccupiedBitboard();
var empty = chessPosition.getEmptyBitboard();
var mobility = 0;
mobility += Chess.AI.makePawnPositionalMask(color, chessPosition.getPieceColorBitboard(Chess.Piece.PAWN, color), empty).popcnt();
mobility += Chess.Position.makePawnAttackMask(color, chessPosition.getPieceColorBitboard(Chess.Piece.PAWN, color)).and(opponent).popcnt();
var knights = chessPosition.getPieceColorBitboard(Chess.Piece.KNIGHT, color).dup();
while (!knights.isEmpty()) {
mobility += Chess.Bitboard.KNIGHT_MOVEMENTS[knights.extractLowestBitPosition()].dup().and_not(us).popcnt();
}
mobility += Chess.Bitboard.KING_MOVEMENTS[chessPosition.getKingPosition(color)].dup().and_not(us).popcnt();
var queens = chessPosition.getPieceColorBitboard(Chess.Piece.QUEEN, color);
var bq = chessPosition.getPieceColorBitboard(Chess.Piece.BISHOP, color).dup().or(queens);
mobility += Chess.Position.makeBishopAttackMask(bq, occupied).and_not(us).popcnt();
var rq = chessPosition.getPieceColorBitboard(Chess.Piece.ROOK, color).dup().or(queens);
mobility += Chess.Position.makeRookAttackMask(rq, occupied).and_not(us).popcnt();
return mobility * Chess.AI.PIECE_VALUES[Chess.Piece.PAWN] / 100;
};
/**
* @param {!Chess.Position} chessPosition
* @return {number}
*/
Chess.AI.evaluate = function(chessPosition) {
return Chess.AI.evaluateMaterial(chessPosition) + Chess.AI.evaluateLocations(chessPosition);
};
/**
* @param {!Chess.Position} chessPosition
* @return {?Chess.Move}
*/
Chess.AI.prototype.search = function(chessPosition) {
/**
* @param {!Array.<!Chess.Move>} moves
* @return {!Array.<!Chess.Move>}
*/
function sortMoves(moves) {
/**
* @param {!Chess.Move} move
* @return {number}
* TODO: killer heuristic, history, etc
*/
function scoreMove(move) {
var score = move.isCapture() ? ((1 + move.getCapturedPiece()) / (1 + move.getPiece())) : 0;
score = 6 * score + move.getPiece();
score = 16 * score + move.getKind();
score = 64 * score + move.getTo();
score = 64 * score + move.getFrom();
return score;
}
/**
* @param {!Chess.Move} a
* @param {!Chess.Move} b
* @return {number}
*/
function compareMoves(a, b) {
return scoreMove(b) - scoreMove(a);
}
moves.sort(compareMoves);
return moves;
}
var evaluations = 0;
/**
* @param {!Chess.Position} chessPosition
* @param {number} alpha
* @param {number} beta
* @return {number}
*/
function quiescenceSearch(chessPosition, alpha, beta) {
if (chessPosition.isDraw()) {
// always assume the draw will be claimed
return 0;
}
var standPatValue = Chess.AI.evaluate(chessPosition);
++evaluations;
var white = (chessPosition.getTurnColor() === Chess.PieceColor.WHITE);
if (white) {
if (standPatValue >= beta) {
return beta;
}
alpha = (standPatValue > alpha) ? standPatValue : alpha;
} else {
if (standPatValue <= alpha) {
return alpha;
}
beta = (standPatValue < beta) ? standPatValue : beta;
}
var moves = sortMoves(chessPosition.getMoves(true, !chessPosition.isKingInCheck()));
for (var i = 0; i < moves.length; ++i) {
if (chessPosition.makeMove(moves[i])) {
var value = quiescenceSearch(chessPosition, alpha, beta);
chessPosition.unmakeMove();
if (white) {
if (value >= beta) {
return beta;
}
alpha = (value > alpha) ? value : alpha; // max player (white)
} else {
if (value <= alpha) {
return alpha;
}
beta = (value < beta) ? value : beta; // min player (black)
}
}
}
return /** @type {number} */(white ? alpha : beta);
}
/**
* @param {!Chess.Position} chessPosition
* @param {number} depth
* @param {number} alpha
* @param {number} beta
* @return {number}
*/
function alphaBeta(chessPosition, depth, alpha, beta) {
if (depth < 1) {
return quiescenceSearch(chessPosition, alpha, beta);
}
var moves = sortMoves(chessPosition.getMoves(true, false));
var white = (chessPosition.getTurnColor() === Chess.PieceColor.WHITE);
var legal = false;
for (var i = 0; i < moves.length; ++i) {
if (chessPosition.makeMove(moves[i])) {
legal = true;
var value = alphaBeta(chessPosition, depth - 1, alpha, beta);
chessPosition.unmakeMove();
if (white) {
alpha = (value > alpha) ? value : alpha; // max player (white)
} else {
beta = (value < beta) ? value : beta; // min player (black)
}
if (beta <= alpha) {
break;
}
}
}
if (!legal) {
// no legal moves
if (!chessPosition.isKingInCheck()) {
// stalemate, draw
return 0;
}
// checkmate, the player in turn loses
var mate = Chess.AI.PIECE_VALUES[Chess.Piece.KING];// - chessPosition.getMadeMoveCount(); TODO: punish longer games
return white ? -mate : mate;
}
// TODO: avoid the search above before checking this, just check for checkmate
if (chessPosition.isDraw()) {
// always assume the draw will be claimed
return 0;
}
return /** @type {number} */(white ? alpha : beta);
}
var bestMove = null;
var alpha = -Infinity;
var beta = Infinity;
var moves = sortMoves(chessPosition.getMoves(true));
for (var i = 0; i < moves.length; ++i) {
if (chessPosition.makeMove(moves[i])) {
var value = alphaBeta(chessPosition, 3, alpha, beta);
chessPosition.unmakeMove();
if (chessPosition.getTurnColor() === Chess.PieceColor.WHITE) {
// max player (white)
if (value > alpha) {
alpha = value;
bestMove = moves[i];
}
} else {
// min player (black)
if (value < beta) {
beta = value;
bestMove = moves[i];
}
}
// Notice that alpha is always smaller than beta here, because we only update one one them
// at the main level, the other stays infinite (+ or -)
}
}
window.console.log("Evaluations: " + evaluations + ", result move: " + bestMove.getString() + ", alpha: " + alpha + ", beta: " + beta);
return bestMove;
};

504
src/bitboard.js

@ -0,0 +1,504 @@
"use strict";
/**
* Chess.Bitboard is an unsigned 64 bit integer, each bit representing a boolean value on the corresponding chessboard square.
* The boolean values represent existence of a piece on the square.
* The 64 bit unsigned integer is implemented as combination of two 32 bit unsigned integers.
* @constructor
* @param {number} low Lower 32 bits of the 64 bit value
* @param {number} high Upper 32 bits of the 64 bit value
* TODO: test using three numbers here instead of two: 31 bit integers are faster than 32 bit ones in chrome (https://v8-io12.appspot.com/#35)
*/
Chess.Bitboard = function(low, high) {
/**
* Lower 32 bits of the 64 bit value
* @type {number}
*/
this.low = low >>> 0;
/**
* Upper 32 bits of the 64 bit value
* @type {number}
*/
this.high = high >>> 0;
};
/**
* @see http://goo.gl/pyzBq (Bit Twiddling Hacks)
* @see http://goo.gl/dnqDn (Bit-peeking bits of Javascript)
* @param {number} v 32 bit integer
* @return {number} 0-32 number of bits set in v
*/
Chess.Bitboard.popcnt32 = function(v) {
v >>>= 0;
v -= (v >>> 1) & 0x55555555;
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
return ((v + (v >>> 4) & 0xF0F0F0F) * 0x1010101) >>> 24;
};
/**
* @param {number} v 32 bit integer
* @return {number} v with its lowest bit cleared
*/
Chess.Bitboard.popLowestBit32 = function (v) {
v >>>= 0;
return (v & (v - 1)) >>> 0;
};
/**
* @param {number} v 32 bit integer, non-zero. Undefined behavior if v is zero.
* @return {number} 0-31 Position of first set bit
*/
Chess.Bitboard.getLowestBitPosition32 = function(v) {
v >>>= 0;
return Chess.Bitboard.popcnt32((v & -v) - 1);
};
/** @return {number} 0-64 number of bits set in this Chess.Bitboard */
Chess.Bitboard.prototype.popcnt = function() {
return Chess.Bitboard.popcnt32(this.low) + Chess.Bitboard.popcnt32(this.high);
};
/**
* Clears the lowest set bit.
* @return {!Chess.Bitboard} this with the lowest bit cleared
*/
Chess.Bitboard.prototype.popLowestBit = function() {
if (this.low) {
this.low = Chess.Bitboard.popLowestBit32(this.low);
} else {
this.high = Chess.Bitboard.popLowestBit32(this.high);
}
return this;
};
/** @return {number} 0-63 position of the first set bit. Undefined behavior if this Chess.Bitboard is empty. */
Chess.Bitboard.prototype.getLowestBitPosition = function() {
if (this.low) {
return Chess.Bitboard.getLowestBitPosition32(this.low);
}
return 32 + Chess.Bitboard.getLowestBitPosition32(this.high);
};
/**
* Clears the lowest set bit and returns its position.
* @return {number} 0-63 position of the first set bit. Undefined behavior if this Chess.Bitboard is empty.
*/
Chess.Bitboard.prototype.extractLowestBitPosition = function() {
var index = this.getLowestBitPosition();
this.popLowestBit();
return index;
};
/** @return {boolean} true if all the bits in this Chess.Bitboard are zero */
Chess.Bitboard.prototype.isEmpty = function() {
return !this.low && !this.high;
};
/**
* @param {number} index 0-63
* @return {boolean} true if the bit at index is 0
*/
Chess.Bitboard.prototype.isClear = function(index) {
index >>>= 0;
if (index < 32) {
return !(this.low & (1 << index));
}
return !(this.high & (1 << (index - 32)));
};
/**
* @param {number} index 0-63
* @return {boolean} true if the bit at index is 1
*/
Chess.Bitboard.prototype.isSet = function(index) {
return !this.isClear(index);
};
/**
* @param {number} index 0-63
* @return {!Chess.Bitboard} this or 1 << index
*/
Chess.Bitboard.prototype.setBit = function(index) {
index >>>= 0;
if (index < 32) {
this.low = (this.low | (1 << index)) >>> 0;
} else {
this.high = (this.high | (1 << (index - 32))) >>> 0;
}
return this;
};
/**
* @param {number} index 0-63
* @return {!Chess.Bitboard} this and not 1 << index
*/
Chess.Bitboard.prototype.clearBit = function(index) {
index >>>= 0;
if (index < 32) {
this.low = (this.low & ~(1 << index)) >>> 0;
} else {
this.high = (this.high & ~(1 << (index - 32))) >>> 0;
}
return this;
};
/**
* @param {!Chess.Bitboard} other
* @return {!Chess.Bitboard} this and other
*/
Chess.Bitboard.prototype.and = function(other) {
this.low = (this.low & other.low) >>> 0;
this.high = (this.high & other.high) >>> 0;
return this;
};
/**
* @param {!Chess.Bitboard} other
* @return {!Chess.Bitboard} this and not other
*/
Chess.Bitboard.prototype.and_not = function(other) {
this.low = (this.low & ~other.low) >>> 0;
this.high = (this.high & ~other.high) >>> 0;
return this;
};
/**
* @param {!Chess.Bitboard} other
* @return {!Chess.Bitboard} this or other
*/
Chess.Bitboard.prototype.or = function(other) {
this.low = (this.low | other.low) >>> 0;
this.high = (this.high | other.high) >>> 0;
return this;
};
/**
* @param {!Chess.Bitboard} other
* @return {!Chess.Bitboard} this xor other
*/
Chess.Bitboard.prototype.xor = function(other) {
this.low = (this.low ^ other.low) >>> 0;
this.high = (this.high ^ other.high) >>> 0;
return this;
};
/** @return {!Chess.Bitboard} not this */
Chess.Bitboard.prototype.not = function() {
this.low = (~this.low) >>> 0;
this.high = (~this.high) >>> 0;
return this;
};
/**
* Shifts this Chess.Bitboard left v bits. Undefined behavior if v is not in 0-63.
* @param {number} v 0-63 number of bits to shift
* @return {!Chess.Bitboard} this << v
*/
Chess.Bitboard.prototype.shl = function(v) {
v >>>= 0;
if (v > 31) {
this.high = (this.low << (v - 32)) >>> 0;
this.low = 0 >>> 0;
} else if (v > 0) {
this.high = ((this.high << v) | (this.low >>> (32 - v))) >>> 0;
this.low = (this.low << v) >>> 0;
}
return this;
};
/**
* Shifts this Chess.Bitboard right v bits. Undefined behavior if v is not in 0-63.
* @param {number} v 0-63 number of bits to shift
* @return {!Chess.Bitboard} this >>> v
*/
Chess.Bitboard.prototype.shr = function(v) {
v >>>= 0;
if (v > 31) {
this.low = this.high >>> (v - 32);
this.high = 0 >>> 0;
} else if (v > 0) {
this.low = ((this.low >>> v) | (this.high << (32 - v))) >>> 0;
this.high >>>= v;
}
return this;
};
/**
* Shifts this Chess.Bitboard left v bits, where v can be negative for right shift.
* @param {number} v number of bits to shift
* @return {!Chess.Bitboard} this << v
*/
Chess.Bitboard.prototype.shiftLeft = function(v) {
if (v > 63 || v < -63) {
this.low = this.high = 0 >>> 0;
} else if (v > 0) {
this.shl(v);
} else if (v < 0) {
this.shr(-v);
}
return this;
};
/**
* @param {!Chess.Bitboard} other
* @return {boolean} 'this' equals 'other'
*/
Chess.Bitboard.prototype.isEqual = function(other) {
return this.low === other.low && this.high === other.high;
};
/** @return {!Chess.Bitboard} copy of this */
Chess.Bitboard.prototype.dup = function() {
return Chess.Bitboard.make(this.low, this.high);
};
/**
* @param {number} low Lower 32 bits of the 64 bit value
* @param {number} high Upper 32 bits of the 64 bit value
* @return {!Chess.Bitboard}
*/
Chess.Bitboard.make = function(low, high) {
return new Chess.Bitboard(low, high);
};
/** @return {!Chess.Bitboard} bitboard of all zeros */
Chess.Bitboard.makeZero = function() {
return Chess.Bitboard.make(0, 0);
};
/** @return {!Chess.Bitboard} bitboard of all ones */
Chess.Bitboard.makeOne = function() {
return Chess.Bitboard.make(0xFFFFFFFF, 0xFFFFFFFF);
};
/** @return {!Chess.Bitboard} bitboard of ones in light (white) squares, zeros in dark (black) squares */
Chess.Bitboard.makeLightSquares = function() {
return Chess.Bitboard.make(0x55AA55AA, 0x55AA55AA);
};
/** @return {!Chess.Bitboard} bitboard of ones in dark squares, zeros in light squares */
Chess.Bitboard.makeDarkSquares = function() {
return Chess.Bitboard.make(0xAA55AA55, 0xAA55AA55);
};
/**
* @param {number} file
* @return {!Chess.Bitboard} bitboard of ones in file, zeros elsewhere
*/
Chess.Bitboard.makeFile = function(file) {
return Chess.Bitboard.make(0x01010101, 0x01010101).shl(file);
};
/** @return {!Array.<!Chess.Bitboard>} bitboard for each file */
Chess.Bitboard.makeFiles = function() {
var b = [];
for (var i = 0; i < 8; ++i) {
b.push(Chess.Bitboard.makeFile(i));
}
return b;
};
/**
* @param {number} rank
* @return {!Chess.Bitboard} bitboard of ones in rank, zeros elsewhere
*/
Chess.Bitboard.makeRank = function(rank) {
return Chess.Bitboard.make(0xFF, 0).shl(rank * 8);
};
/** @return {!Array.<!Chess.Bitboard>} bitboard for each rank */
Chess.Bitboard.makeRanks = function() {
var b = [];
for (var i = 0; i < 8; ++i) {
b.push(Chess.Bitboard.makeRank(i));
}
return b;
};
/**
* @param {number} index 0-63
* @return {!Chess.Bitboard} bitboard of 1 at index, zero elsewhere
*/
Chess.Bitboard.makeIndex = function(index) {
return Chess.Bitboard.makeZero().setBit(index);
};
/** @return {!Array.<!Chess.Bitboard>} bitboard for each index */
Chess.Bitboard.makeIndices = function() {
var b = [];
for (var i = 0; i < 64; ++i) {
b.push(Chess.Bitboard.makeIndex(i));
}
return b;
};
/**
* 0 diagonal is the main diagonal, positive numbers are superdiagonals, negative numbers subdiagonals.
* @param {number} diagonal (-7)-7
* @return {!Chess.Bitboard} bitboard with ones on diagonal, zeros elsewhere
*/
Chess.Bitboard.makeDiagonal = function(diagonal) {
return Chess.Bitboard.make(0x10204080, 0x01020408).and(Chess.Bitboard.makeOne().shiftLeft(diagonal * 8)).shiftLeft(diagonal);
};
/** @return {!Array.<!Chess.Bitboard>} bitboard for each diagonal */
Chess.Bitboard.makeDiagonals = function() {
var b = [];
for (var i = -7; i < 8; ++i) {
b.push(Chess.Bitboard.makeDiagonal(i));
}
return b;
};
/**
* 0 diagonal is the main antidiagonal, positive numbers are subantidiagonals (below the main antidiagonal on the chessboard), negative numbers superantidiagonals.
* @param {number} antidiagonal (-7)-7
* @return {!Chess.Bitboard} bitboard with ones on antidiagonal, zeros elsewhere
*/
Chess.Bitboard.makeAntidiagonal = function(antidiagonal) {
return Chess.Bitboard.make(0x08040201, 0x80402010).and(Chess.Bitboard.makeOne().shiftLeft(-antidiagonal * 8)).shiftLeft(antidiagonal);
};
/** @return {!Array.<!Chess.Bitboard>} bitboard for each antidiagonal */
Chess.Bitboard.makeAntidiagonals = function() {
var b = [];
for (var i = -7; i < 8; ++i) {
b.push(Chess.Bitboard.makeAntidiagonal(i));
}
return b;
};
/**
* @see http://goo.gl/MRA5s (Knight Pattern)
* @param {number} index 0-63 chessboard square of the knight
* @return {!Chess.Bitboard} knight target squares
*/
Chess.Bitboard.makeKnightMovement = function(index) {
var b = Chess.Bitboard.makeZero().setBit(index);
var l1 = b.dup().shr(1).and_not(Chess.Bitboard.FILES[7]);
var l2 = b.dup().shr(2).and_not(Chess.Bitboard.FILES[7]).and_not(Chess.Bitboard.FILES[6]);
var r1 = b.dup().shl(1).and_not(Chess.Bitboard.FILES[0]);
var r2 = b.dup().shl(2).and_not(Chess.Bitboard.FILES[