1
0
mirror of https://github.com/tiyn/yeschess.git synced 2026-02-22 10:34:47 +01:00

Compare commits

...

45 Commits

Author SHA1 Message Date
TiynGER
ca73df4fbb configs and projectPath
In the last few commits the problem occured
that absolute paths are needed for correct
and unambiguous locations for binary files
or the database.
For the compiler arguments the problem is now
solved with an automatic script, that creates
the absolute paths automatically for different
systems.
The database path is now specified by calling
a new function, that finds the root of the
project followed by a relative path.
2021-05-17 00:01:18 +02:00
TiynGER
fa05c87cb1 Todo: added todo file 2021-05-16 03:20:21 +02:00
TiynGER
e306de0573 refactoring: absolute path for db, general refactoring
Due to the path for the db being relative it came
to problems when importing the openingBook.nim file
in other modules (especially in ).
To change this i added a variable, that needs to point to the
root directory of the project.
Additionally i set some coding guidelines and enforced them
into the current codebase.
2021-05-16 03:15:40 +02:00
TiynGER
80772462da documentation: updated the readme documentation
The readme was partly outdated.
I decided not to put in depth documentation into
the readme.
Additionally i added missing docstrings into posMoveDB
2021-05-15 02:33:21 +02:00
TiynGER
362d293fb1 openingdatabase: added db access and pgn crawler
An opening database is an important feature of an engine.
This is because the opening has many possibilities and
there arent much pieces taken yet.
To quickly evaluate the position a database can be useful.
The first step for the database integration was now done by
a program to create and store pgn data in the db.
The next step is to add a way to use the data in the engine.
2021-05-15 02:12:59 +02:00
TiynGER
73f32e8598 project structure
The project previously wasnt structured very
well.
Now the engine has its own module so its more
visible what file belongs to what topic.
2021-05-15 02:10:29 +02:00
TiynGER
c6ecdd4c83 tests: created tests dir
The tests where cluttering the src dir
so i moved them to a separate dir.
Same goes for the binaries so i changed the
outputdirectory for them and
added these to the new gitignore file
2021-05-15 00:46:35 +02:00
TiynGER
2d021366cb art: created art dir and moved pictures into it 2021-05-14 23:57:17 +02:00
marten
acb6a1ad77 readme: removed not working notice for engine 2021-05-14 21:15:39 +02:00
TiynGER
f6787eedd3 chess bugfix: castle and convertToFen
Castling was bugged in the previous version.
The pieces moved on the wrong squares due to W and E being set wrongly.
convertToFen didnt record empty squares if located
at the end of the fen string.
2021-05-13 18:29:52 +02:00
TiynGER
23a97ae9fe chess bugfix: en passant square creation
The en passant square is only important if it is
target of an enemy pawn.
If that is not the case it should not be set
and shouldn't appear in the fen string either.

Now it is checked if the en passant square is
the target of an enemy pawn
2021-05-11 01:42:20 +02:00
TiynGER
5e796751d7 chess: repetition now uses less data
Repetitions can only occur if the pieces and castling
rights are exactly the same.
For memory sake the sequence of last boards
is now emptied if a capture occurs,
a pawn moves, or if the castle
rights change.
That directly makes the saving of the castling
rights obsolete.
2021-05-10 23:50:30 +02:00
TiynGER
a3c48fd50c chess: chess to fen notation added
Now there is a method to turn a chess
object into a fen-string.
Additionally the enPassant handling was
changed.
There can be only one enPassant field
at a time.
So a new attribute was added to the chess
object that stores the field for the
enPassant capture.
If there is no enPassant field it will be set to -1.
2021-05-10 01:30:11 +02:00
TiynGER
d5ac25a642 readme: added move notation documentation 2021-05-08 01:15:49 +02:00
TiynGER
53e1cc8df3 bugfix chess: genPawnPromotion always gives back promotions
The if statement was not tailored to the color and according
back ranks.
It didnt even check if the piece is a pawn.
Now it does.
2021-05-07 02:25:08 +02:00
TiynGER
a383e52bd7 ches: added fen
FEN is a notation to describe a state
of a chess game.
Added a initChess function, which is able
to create a chess object from FEN
2021-05-06 02:26:57 +02:00
TiynGER
c195f2da6a chess: refactoring genLegalMoves
genLegalMoves procs where copy and pasted, now they
are done by a common function.
Some magic numbers have been removed.
2021-05-04 18:36:13 +02:00
TiynGER
ee7d5a0a9d chess: refactoring genDests
genDests procs where copy and pasted, now they
are done by a common function.
Single line functions where also removed, due
to them being to trivial
2021-05-04 18:12:05 +02:00
TiynGER
087da7d3f1 bugfix chess: moveToNotation promotion part wrong
The function moveToNotation added the promotion character
even if a piece and not a pawn moves to the final rank.
2021-05-04 00:46:17 +02:00
TiynGER
4cac1ba6bf merge conflict 2021-05-03 17:14:15 +02:00
TiynGER
3c97395ce8 refactoring: cleanup engine code and add alpha beta to readme 2021-05-03 17:09:48 +02:00
TiynGER
b7480a0cd4 engine: added piece-square tables
Piece square tables are a way to encurage the engine to
put pieces on active squares.
Each piece got a corresponding table.
2021-05-03 02:01:36 +02:00
TiynGER
12ed9921f1 engine: added piece-square tables
Piece square tables are a way to encurage the engine to
put pieces on active squares.
Each piece got a corresponding table.
2021-05-03 01:57:12 +02:00
TiynGER
b3c76fd2c1 visibility: use include in tests and only set needed functions as public 2021-05-03 00:15:58 +02:00
TiynGER
10098da82f refactoring: renaming classes and functions
Renamed the 'game' type to 'chess' to make more sense
of the existing filestructure.
Doubled functions where removed.
2021-05-01 16:21:30 +02:00
TiynGER
3bc523c37a chess: fixed field indices
Field indices where counting up from h to a,
now they are counting up from a to h, so the board
is not inverted anymore
2021-05-01 02:40:08 +02:00
TiynGER
9095cf8eee engine: engine now correctly uses the negamax algorithm
NegaMax is a simplified version of the MiniMax algorithm that
doesn't need different subroutines for the different colors.
The testcases were selected and extended.
Evaluation now has values for draws and wins.
2021-05-01 01:58:26 +02:00
TiynGER
9ea20b6c1f lichess: turned down difficulty and removed unnecessary echoes 2021-05-01 01:56:07 +02:00
TiynGER
5bee0f84d2 bugfix: threefold repitition throws nullpointer
If the threefold repitition check is called before enough moves
are saved in the previous board state it throws a nullpointer.
A simple null check solves the problem
2021-05-01 01:54:16 +02:00
TiynGER
91281d97a7 lichess: added lichess integration
Lichess is a free and open source platform to play chess.
Connection is realized via the berserk python plugin that uses the lichess api.
2021-04-29 01:15:54 +02:00
TiynGER
7fb821254c game: added single player
Integrated the engine into the playable game.
Created a menu to choose between single player and hotseat.
Added a way to choose difficulty and color in single player.
2021-04-27 23:13:41 +02:00
TiynGER
4e69bd3977 engine: Readme update according to used algorithm 2021-04-27 00:55:30 +02:00
TiynGER
4ba3e06d93 engine: first push for engine
I created an engine, that uses a basic version of the minimax algorithm
to evaluate a position.
It then evaluates all the possible next moves in a given position and picks
the one that has the best evaluation.
Basic test cases were also added.
2021-04-27 00:49:39 +02:00
TiynGER
0e27d0a6b5 chess: syntax and structure changes
gameTest renamed to chessTest due to it testing chess.nim and not game.nim.
always used += in chess.nim to increase readability.
2021-04-26 19:54:19 +02:00
TiynGER
0a0accb3d0 chess: Moved replaced with CastleRights
Moved was a 10x12 1-d array, that stored bools.
It was only used for checking if a pawn moved (especially important for
the double move of the pawns) and if the other pieces moved (important
for checking if the player can castle or not).
The pawn moves are now checked by the starting position (pawns on the
second rank cant be already moved).
The castle rights of a game are expressed as CastleRights, a tuple with 4 bools.
This saves basically 116 boolean values.
2020-12-18 01:01:58 +01:00
TiynGER
c91bf64047 chess restructuring: changed order of functions 2020-12-18 00:25:22 +01:00
TiynGER
0ceb567048 structure: made a src folder and moved all source file into it 2020-12-16 04:24:19 +01:00
TiynGER
6ad11fde2a chess: updating documentation
Documentation is now set for every method with complete parameters.
Also all global variables and constants as well as types are documented.
2020-12-16 04:19:02 +01:00
TiynGER
9b68538b12 readme: specified plan for engine 2020-12-16 03:29:43 +01:00
TiynGER
c5cf2fd737 chess: refactoring setters and try
Setters are not needed as all the assigning gets handled inside the  file.
All Setters were removed.
The try-statements were used to excessively and were changed for manual checking
2020-12-16 03:18:24 +01:00
TiynGER
c4f7e3b98d chess: improved documentation and deleted unused stuff 2020-12-16 02:42:58 +01:00
TiynGER
81d5d57e75 chess: refactoring getters
Getters are not needed according to the official nim documentation.
https://nim-lang.org/docs/manual.html#procedures-properties
Getters were replaced accordingly
2020-12-16 01:03:06 +01:00
TiynGER
75e804ef6f chess/game: claimable draw at 50-Move-Rule added 2020-12-15 23:31:42 +01:00
TiynGER
9ed8b886db chess/game: claimable draw at 3-fold repitition added 2020-12-15 20:30:04 +01:00
TiynGER
7caa4db120 game bugfix: game isnt stopped after stalemate 2020-12-15 18:09:27 +01:00
16 changed files with 2795 additions and 2026 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
secret.nim
nim.cfg
bin
htmldocs
ressources

128
README.md
View File

@@ -1,24 +1,122 @@
# ychess
![ychess-logo](ychess.png)
![ychess-logo](./art/ychess.png)
ychess is a chess implementation written in nim.
A chess engine is planned.
ychess is a chess implementation and engine written in nim.
## Todo
## Usage and contribution
- draw by
- 3-fold repitition
- 50-move rule
For the following subsections to work properly you need to have `nim.cfg` files
so you don't need to pass all compiler arguments manually.
To automatically create the config files run `nim c -r createCfg.nim` from the
root directory of the project.
## Usage
### Todos
Simply download the code and run `nim c -r game.nim`.
You can now play a 1v1 hotseat game of chess in the commandline.
[TODO.md](./TODO.md) contains a list of features that are planned.
## Testing
### Command line
Testing is done by `einheit` by [jyapayne](https://github.com/jyapayne/einheit).
All legal chess moves are implemented in `chess.nim` and tested by the TestSuite
in `test.nim`.
You can simply run the tests with `nim c -r test.nim`.
To play chess in the commandline simply download the code (or clone the
repository) and run `nim c -r game.nim`.
You can either play the 1v1 hotseat mode or a single player mode vs the engine.
### Lichess
ychess uses the lichess api to make playing more convenient.
An instance of the engine occasionally plays on
[lichess](https://lichess.org/@/tiyn-ychess).
To get into the whitelist just write a ingame message to the account.
If you want to create an instance on lichess yourself you need to set a api
token.
This is done in `src/engine/secret.nim`.
It should have the following structure:
```nim
let api_token* = "<lichess api token for bot>"
```
Following that you will want to set your username into the whitelist in
`src/engine/lichessBridge.nim`.
After that you can start the lichess bot by running
`nim c -r src/engine/lichessBridge.nim`.
## Project Structure
- `art` - contains pictures and arts not used in the code.
- `bin` - is not pushed to the git repository but contains all binaries and will
be created if you compile a program.
- `htmldocs` - is not pushed to the git repository but contains all
automatically generated documentation.
- `ressources` - is not pushed to the git repository but contains all
the data used in source code but not being source code itself (e.g. databases).
- `src` - is the root folder for all programs except tests.
- `tests` - contains all tests.
### Documentation
Documentation is written into the code via DocGen.
For this reason it is not saved in this repository.
To extract it into html (assuming you want the documentation for `game.nim`)
run `nim doc --project --index:on --outdir:htmldocs game.nim`.
## Additional Documentation
### Moves
Moves are read from the commandline as
[pure coordinate notation](https://www.chessprogramming.org/Algebraic_Chess_Notation#Pure_coordinate_notation).
The inner program will convert this notation to a move-tuple.
### Board Representation
Due to easier off the board checking a
[10x12](https://www.chessprogramming.org/10x12_Board) board is used.
### Engine
The engine uses a simple implementation of the
[NegaMax](https://www.chessprogramming.org/NegaMax)-algorithm with
[Alpha-Beta-Pruning](https://www.chessprogramming.org/Alpha-Beta#Negamax_Framework).
For the evaluation function each piece has a corresponding value.
Additionally [piece-square tables](https://www.chessprogramming.org/Piece-Square_Tables)
are used.
### Code Style Guide
Make sure to take a look at the
[official nim style guide](https://nim-lang.org/docs/nep1.html).
All conventions should be applied to this code.
Additionally there are a bunch of steps to make the code more consistent.
#### Constants
Constants should start with an uppercase letter but shouldn't be written in all
caps (`const FooBar = 2`).
#### Whitespaces
Basic arithmetic operations should be surrounded by spaces for example: `1 + 3`.
This however is not true for negation of a single value (`-1`) or if the
arithmetic operation is done inside array brackets or in iterators (`a+1..3`,
`a[c+3]`).
#### Function calls
Determining the length of a string, array, etc should not be done via a function
(`len(array)`) but by appending it like `array.len`.
In the same style function calls should be done (`chess.convertToFen()` instead
of `convertToFen(chess)`).
This however is not true if the function takes a first argument that is not an
abstract datatype like the joinPath function (all the parameters are strings).
#### booleans and logic
If statements should not contain outer brackets.
In some cases (especially concatenations of `and` and `or`) inner brackets are
useful to increase readability in complexer logic formulas.
When assigning booleans with logical formulas outer brackets are expected
(`var boolVar = (1 == 1)`).

7
TODO.md Normal file
View File

@@ -0,0 +1,7 @@
# TODO
This is a list of features that are to be implemented.
- Opening Book
- Endgame Table
- Docker integration

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

763
chess.nim
View File

@@ -1,763 +0,0 @@
import tables
from strutils import parseInt
type
Color* = enum
Black = -1, White = 1
## Board that saves the board
Board* = array[0..119, int]
## Board that checks if pieces moved
Moved* = array[0..119, bool]
## Game as object of different values
Game* = object
board*: Board
moved: Moved
toMove*: Color
## Move as object
Move* = object
start: int
dest: int
color: Color
prom: int
## Amount of pieces
Pieces = tuple
p: int
k: int
b: int
r: int
q: int
const
# IDs for piece
BlockID* = 999
PawnID* = 1
KnightID* = 2
BishopID* = 3
RookID* = 4
QueenID* = 5
KingID* = 6
EnPassantID* = 7
# IDs that are saved in the array
Block* = BlockID
WPawn* = PawnID
WKnight* = KnightID
WBishop* = BishopID
WRook* = RookID
WQueen* = QueenID
WKing* = KingID
WEnPassant* = EnPassantID
BPawn* = -PawnID
BKnight* = -KnightID
BBishop* = -BishopID
BRook* = -RookID
BQueen* = -QueenID
BKing* = -KingID
BEnPassant* = EnPassantID
# Directions of movement
N = 10
S = -N
W = 1
E = -W
# Movement options for pieces (Bishop/Rook/Queen can repeat in the same direction)
Knight_Moves = [N+N+E, N+N+W, E+E+N, E+E+S, S+S+E, S+S+W, W+W+N, W+W+S]
Bishop_Moves = [N+E, N+W, S+E, S+W]
Rook_Moves = [N, E, S, W]
Queen_Moves = [N, E, S, W, N+E, N+W, S+E, S+W]
King_Moves = [N, E, S, W, N+E, N+W, S+E, S+W]
King_Moves_White_Castle = [E+E, W+W]
Pawn_Moves_White = [N]
Pawn_Moves_White_Double = [N+N]
Pawn_Moves_White_Attack = [N+E, N+W]
let PieceChar = {
0: " ",
1: "P",
2: "N",
3: "B",
4: "R",
5: "Q",
6: "K",
7: " ",
-1: "p",
-2: "n",
-3: "b",
-4: "r",
-5: "q",
-6: "k",
-7: " ",
999: "-"
}.newTable
let FileChar = {
"a": 7,
"b": 6,
"c": 5,
"d": 4,
"e": 3,
"f": 2,
"g": 1,
"h": 0
}.newTable
const InsufficientMaterial = @[
#p, n, b, r, q
# lone kings
(0, 0, 0, 0, 0),
# knight only
(0, 0, 1, 0, 0),
# bishop only
(0, 1, 0, 0, 0),
# 2 knights
(0, 2, 0, 0, 0)
]
proc getField*(board: Board, field: int): int =
return board[field]
proc setField(board: var Board, field: int, val: int): bool {.discardable.} =
if (val in PieceChar):
try:
board[field] = val
return true
except Exception:
return false
proc getField*(moved: Moved, field: int): bool =
return moved[field]
proc setField(moved: var Moved, field: int, val: bool): bool {.discardable.} =
try:
moved[field] = val
return true
except Exception:
return false
proc checkInsufficientMaterial(board: Board): bool =
## Checks for combinations of pieces on a `board`, where no checkmate can be forced
var wp = 0
var wn = 0
var wb = 0
var wr = 0
var wq = 0
var bp = 0
var bn = 0
var bb = 0
var br = 0
var bq = 0
for field in board.low..board.high:
case board.getField(field):
of WPawn:
wp = wp + 1
of BPawn:
bp = bp + 1
of WKnight:
wn = wn + 1
of BKnight:
bn = bn + 1
of WBishop:
wb = wb + 1
of BBishop:
bb = bb + 1
of WRook:
wr = wr + 1
of BRook:
br = br + 1
of WQueen:
wq = wq + 1
of BQueen:
bq = bq + 1
else:
continue
let wpieces = (wp, wn, wb, wr, wq)
let bpieces = (bp, bn, bb, br, bq)
return (wpieces in InsufficientMaterial) and (bpieces in InsufficientMaterial)
proc initBoard(): Board =
## Create and return a board with pieces in starting position.
let board = [
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, WRook, WKnight, WBishop, WKing, WQueen, WBishop, WKnight, WRook, Block,
Block, WPawn, WPawn, WPawn, WPawn, WPawn, WPawn, WPawn, WPawn, Block,
Block, 0, 0, 0, 0, 0, 0, 0, 0, Block,
Block, 0, 0, 0, 0, 0, 0, 0, 0, Block,
Block, 0, 0, 0, 0, 0, 0, 0, 0, Block,
Block, 0, 0, 0, 0, 0, 0, 0, 0, Block,
Block, BPawn, BPawn, BPawn, BPawn, BPawn, BPawn, BPawn, BPawn, Block,
Block, BRook, BKnight, BBishop, BKing, BQueen, BBishop, BKnight, BRook, Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block]
return board
proc initBoard(board: array[0..63, int]): Board =
## Create and return a board with pieces in position of choice
let board = [
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, board[0], board[1], board[2], board[3], board[4], board[5],
board[6], board[7], Block,
Block, board[8], board[9], board[10], board[11], board[12], board[13],
board[14], board[15], Block,
Block, board[16], board[17], board[18], board[19], board[20], board[
21], board[22], board[23], Block,
Block, board[24], board[25], board[26], board[27], board[28], board[
29], board[30], board[31], Block,
Block, board[32], board[33], board[34], board[35], board[36], board[
37], board[38], board[39], Block,
Block, board[40], board[41], board[42], board[43], board[44], board[
45], board[46], board[47], Block,
Block, board[48], board[49], board[50], board[51], board[52], board[
53], board[54], board[55], Block,
Block, board[56], board[57], board[58], board[59], board[60], board[
61], board[62], board[63], Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block]
return board
proc initMoved(): Moved =
## Create and return a board of pieces moved.
var moved: Moved
return moved
proc initGame*(): Game =
## Create and return a Game object.
let game = Game(board: initBoard(), moved: initMoved(),
to_move: Color.White)
return game
proc initGame*(board: array[0..63, int], color: Color): Game =
## Create ad return a Game object based on a position of choice.
let board = initBoard(board)
let compare = initBoard()
var moved = initMoved()
var same_piece: bool
for ind in board.low..board.high:
same_piece = (board[ind] != compare[ind])
moved.setField(ind, same_piece)
let game = Game(board: board, moved: moved,
to_move: color)
return game
proc getMove*(start: int, dest: int, prom: int, color: Color): Move =
## Get a move object from `start` to `dest` with an eventual promition to `prom`
var move = Move(start: start, dest: dest, prom: prom * ord(color), color: color)
if (KnightID > prom or QueenID < prom):
move.prom = QueenID
return move
proc getMove*(start: int, dest: int, color: Color): Move =
## Get a move object from `start` to `dest` with automatic promition to `queen`
var move = Move(start: start, dest: dest, prom: QueenID * ord(color), color: color)
return move
proc echoBoard*(game: Game, color: Color) =
## Prints out the given `board` with its pieces as characters and line indices from perspecive of `color`.
var line_str = ""
if (color == Color.Black):
for i in countup(0, len(game.board)-1):
if (game.board.getField(i) == 999):
continue
line_str &= PieceChar[game.board[i]] & " "
if ((i+2) %% 10 == 0):
line_str &= $((int)((i)/10)-1) & "\n"
echo line_str
echo "h g f e d c b a"
else:
for i in countdown(len(game.board)-1, 0):
if (game.board.getField(i) == 999):
continue
line_str &= PieceChar[game.board[i]] & " "
if ((i-1) %% 10 == 0):
line_str &= $((int)((i)/10)-1) & "\n"
echo line_str
echo "a b c d e f g h"
proc fieldToInd*(file: string, line: int): int =
## Calculate board index from `file` and `line` of a chess board.
try:
return 1+(line+1)*10+FileChar[file]
except IndexDefect, ValueError:
return -1
proc fieldToInd*(field: string): int =
## Calculate board index from `field` of a chess board.
try:
return fieldToInd($field[0], parseInt($field[1]))
except IndexDefect, ValueError:
return -1
proc indToField*(ind: int): string =
## Calculate field name from board index `ind`.
let line = (int)ind/10-1
let file_ind = (ind)%%10-1
for file, i in FileChar:
if FileChar[file] == file_ind:
return $file & $line
proc notationToMove*(notation: string, color: Color): Move =
## Convert simplified algebraic chess `notation` to a move object, color of player is `color`.
try:
var move: Move
var start = fieldToInd(notation[0..1])
var dest = fieldToInd(notation[2..3])
move = getMove(start, dest, color)
if (len(notation) > 4):
var promStr = $notation[4]
var prom: int
case promStr:
of "Q":
prom = QueenID * ord(color)
of "R":
prom = RookID * ord(color)
of "B":
prom = BishopID * ord(color)
of "N":
prom = KnightID * ord(color)
move = getMove(start, dest, prom, color)
return move
except IndexError:
var move: Move
return move
proc genBishopDests(game: Game, field: int, color: Color): seq[int] =
## Generate possible destinations for a bishop with specific `color` located at index `field` of `game`.
## Returns a sequence of possible indices to move to.
try:
var res = newSeq[int]()
var dest: int
var target: int
for move in Bishop_Moves:
dest = field+move
target = game.board.getField(dest)
while (target != 999 and (ord(color) * target <= 0) or target ==
EnPassantID or target == -EnPassantID):
res.add(dest)
if (ord(color) * target < 0 and ord(color) * target > -EnPassantID):
break
dest = dest+move
target = game.board.getField(dest)
return res
except IndexDefect:
return @[]
proc genRookDests(game: Game, field: int, color: Color): seq[int] =
## Generate possible destinations for a rook with specific `color` located at index `field` of `game`.
## Returns a sequence of possible indices to move to.
try:
var res = newSeq[int]()
var dest: int
var target: int
for move in Rook_Moves:
dest = field+move
target = game.board.getField(dest)
while (target != 999 and (ord(color) * target <= 0) or target ==
EnPassantID or target == -EnPassantID):
res.add(dest)
if (ord(color) * target < 0 and ord(color) * target > -EnPassantID):
break
dest = dest+move
target = game.board.getField(dest)
return res
except IndexDefect:
return @[]
proc genQueenDests(game: Game, field: int, color: Color): seq[int] =
## Generate possible destinations for a queen with specific `color` located at index `field` of `game`.
## Returns a sequence of possible indices to move to.
try:
var res = newSeq[int]()
var dest: int
var target: int
for move in Queen_Moves:
dest = field+move
target = game.board.getField(dest)
while (target != 999 and (ord(color) * target <= 0) or target ==
EnPassantID or target == -EnPassantID):
res.add(dest)
if (ord(color) * target < 0 and ord(color) * target > -EnPassantID):
break
dest = dest+move
target = game.board.getField(dest)
return res
except IndexDefect:
return @[]
proc genKingCastleDest(game: Game, field: int, color: Color): seq[int] =
## Generate possible castle destinations for a king with specific `color` located at index `field` of `game`
## Returns a sequence of possible indices to move to.
try:
var res = newSeq[int]()
var dest: int
var target: int
var half_dest: int
var half_target: int
for castle in King_Moves_White_Castle:
dest = field + castle
target = game.board.getField(dest)
half_dest = field + (int)castle/2
half_target = game.board.getField(half_dest)
if (target == 999 or (target != 0)):
continue
if (half_target == 999 or (half_target != 0)):
continue
res.add(dest)
return res
except IndexDefect:
return @[]
proc genKingDests(game: Game, field: int, color: Color): seq[int] =
## Generate possible destinations for a king with specific `color` located at index `field` of `game`.
## Returns a sequence of possible indices to move to.
try:
var res = newSeq[int]()
var dest: int
var target: int
for move in King_Moves:
dest = field + move
target = game.board.getField(dest)
if (target == 999 or (ord(color) * target > 0 and ord(color) * target != EnPassantID)):
continue
res.add(dest)
res.add(game.genKingCastleDest(field, color))
return res
except IndexDefect:
return @[]
proc genKnightDests(game: Game, field: int, color: Color): seq[int] =
## Generate possible destinations for a knight with specific `color` located at index `field` of `game`.
## Returns a sequence of possible indices to move to.
try:
var res = newSeq[int]()
var dest: int
var target: int
for move in Knight_Moves:
dest = field + move
target = game.board.getField(dest)
if (target == 999 or (ord(color) * target > 0 and ord(color) * target != EnPassantID)):
continue
res.add(dest)
return res
except IndexDefect:
return @[]
proc genPawnAttackDests(game: Game, field: int, color: Color): seq[int] =
## Generate possible attack destinations for a pawn with specific `color` located at index `field` of `game`.
## Returns a sequence of possible indices to move to.
try:
var res = newSeq[int]()
var dest: int
var target: int
for attacks in Pawn_Moves_White_Attack:
dest = field + (attacks * ord(color))
target = game.board.getField(dest)
if (target == 999 or ord(color) * target >= 0):
continue
res.add(dest)
return res
except IndexDefect:
return @[]
proc genPawnDoubleDests(game: Game, field: int, color: Color): seq[int] =
## Generate possible double destinations for a pawn with specific `color` located at index `field` of `game`.
## Returns a sequence of possible indices to move to.
try:
var res = newSeq[int]()
var dest: int
var target: int
for doubles in Pawn_Moves_White_Double:
dest = field + doubles * ord(color)
target = game.board.getField(dest)
if (game.moved.getField(field) or (target != 0) or (
game.board.getField(dest+(S*ord(color))) != 0)):
continue
res.add(dest)
return res
except IndexDefect:
return @[]
proc genPawnDests(game: Game, field: int, color: Color): seq[int] =
## Generate possible destinations for a pawn with specific `color` located at index `field` of `game`.
## Returns a sequence of possible indices to move to.
try:
var res = newSeq[int]()
var dest: int
var target: int
for move in Pawn_Moves_White:
dest = field + move * ord(color)
target = game.board.getField(dest)
if (target != 0 and target != ord(color) * EnPassantID):
continue
res.add(dest)
res.add(game.genPawnAttackDests(field, color))
res.add(game.genPawnDoubleDests(field, color))
return res
except IndexDefect:
return @[]
proc pieceOn(game: Game, color: Color, sequence: seq[int],
pieceID: int): bool =
## Check if a piece with `pieceID` of a given `color` is in a field described in a `sequence` in a `game`.
for check in sequence:
if game.board.getField(check) == ord(color) * -1 * pieceID:
return true
return false
proc isAttacked(game: Game, position: int, color: Color): bool =
## Check if a field is attacked by the opposite of `color` in a `game`.
var attacked = false
attacked = attacked or game.pieceOn(color, game.genPawnAttackDests(
position, color), PawnID)
attacked = attacked or game.pieceOn(color, game.genQueenDests(position,
color), QueenID)
attacked = attacked or game.pieceOn(color, game.genKingDests(position,
color), KingID)
attacked = attacked or game.pieceOn(color, game.genRookDests(position,
color), RookID)
attacked = attacked or game.pieceOn(color, game.genBishopDests(position,
color), BishopID)
attacked = attacked or game.pieceOn(color, game.genKnightDests(position,
color), KnightID)
return attacked
proc isInCheck*(game: Game, color: Color): bool =
## Check if the King of a given `color` is in check in a `game`.
var king_pos: int
for i in countup(0, game.board.high):
if game.board.getField(i) == ord(color) * KingID:
king_pos = i
return game.isAttacked(king_pos, color)
proc uncheckedMove(game: var Game, start: int, dest: int): bool {.discardable.} =
## Moves a piece if possible from `start` position to `dest` position.
## Doesnt check boundaries, checks, movement.
## returns true if the piece moved, else false
try:
let piece = game.board.getField(start)
if game.board.setField(start, 0):
if game.board.setField(dest, piece):
game.moved.setField(start, true)
game.moved.setField(dest, true)
return true
else:
game.board.setField(start, piece)
except IndexDefect, ValueError:
return false
proc moveLeadsToCheck(game: Game, start: int, dest: int,
color: Color): bool =
## Checks in a `game` if a move from `start` to `dest` puts the `color` king in check.
var check = game
check.uncheckedMove(start, dest)
return check.isInCheck(color)
proc removeEnPassant(board: var Board, color: Color): void =
## Removes every en passant of given `color` from the `game`.
for field in board.low..board.high:
if board.getField(field) == ord(color) * EnPassantID:
board.setField(field, 0)
proc genLegalKnightMoves(game: Game, field: int, color: Color): seq[Move] =
## Generates all legal knight moves starting from `field` in a `game` for a `color`.
if game.board.getField(field) != KnightID * ord(color):
return @[]
var res = newSeq[Move]()
var moves = game.genKnightDests(field, color)
for dest in moves:
if not game.moveLeadsToCheck(field, dest, color):
res.add(getMove(field, dest, color))
return res
proc genLegalBishopMoves(game: Game, field: int, color: Color): seq[Move] =
## Generates all legal bishop moves starting from `field` in a `game` for a `color`.
if game.board.getField(field) != BishopID * ord(color):
return @[]
var res = newSeq[Move]()
var moves = game.genBishopDests(field, color)
for dest in moves:
if not game.moveLeadsToCheck(field, dest, color):
res.add(getMove(field, dest, color))
return res
proc genLegalRookMoves(game: Game, field: int, color: Color): seq[Move] =
## Generates all legal rook moves starting from `field` in a `game` for a `color`.
if game.board.getField(field) != RookID * ord(color):
return @[]
var res = newSeq[Move]()
var moves = game.genRookDests(field, color)
for dest in moves:
if not game.moveLeadsToCheck(field, dest, color):
res.add(getMove(field, dest, color))
return res
proc genLegalQueenMoves(game: Game, field: int, color: Color): seq[Move] =
## Generates all legal queen moves starting from `field` in a `game` for a `color`.
if game.board.getField(field) != QueenID * ord(color):
return @[]
var res = newSeq[Move]()
var moves = game.genQueenDests(field, color)
for dest in moves:
if not game.moveLeadsToCheck(field, dest, color):
res.add(getMove(field, dest, color))
return res
proc genLegalKingMoves(game: Game, field: int, color: Color): seq[Move] =
## Generates all legal king moves starting from `field` in a `game` for a `color`.
if game.board.getField(field) != KingID * ord(color):
return @[]
var res = newSeq[Move]()
var moves = game.genKingDests(field, color)
for dest in moves:
if field - dest == W+W and game.isAttacked(dest+W, color):
continue
if field - dest == E+E and game.isAttacked(dest+E, color):
continue
if not game.moveLeadsToCheck(field, dest, color):
res.add(getMove(field, dest, color))
return res
proc genPawnPromotion(move: Move, color: Color): seq[Move] =
## Generate all possible promotions of a `move` by `color`.
var promotions = newSeq[Move]()
let start = move.start
let dest = move.dest
if (90 < dest and dest < 99) or (20 < dest and dest < 29):
for piece in KnightID..QueenID:
promotions.add(getMove(start, dest, piece, color))
return promotions
proc genLegalPawnMoves(game: Game, field: int, color: Color): seq[Move] =
## Generates all legal pawn moves starting from `field` in a `game` for a `color`.
if game.board.getField(field) != PawnID * ord(color):
return @[]
var res = newSeq[Move]()
var moves = game.genPawnDests(field, color)
for dest in moves:
if not game.moveLeadsToCheck(field, dest, color):
var promotions = genPawnPromotion(getMove(field, dest, color), color)
if promotions != @[]:
res.add(promotions)
else:
res.add(getMove(field, dest, color))
return res
proc genLegalMoves*(game: Game, field: int, color: Color): seq[Move] =
## Generates all legal moves starting from `field` in a `game` for a `color`.
var legal_moves = newSeq[Move]()
var target = ord(color) * game.board.getField(field)
if 0 < target and target < EnPassantID:
legal_moves = case target:
of PawnID:
game.genLegalPawnMoves(field, color)
of KnightID:
game.genLegalKnightMoves(field, color)
of BishopID:
game.genLegalBishopMoves(field, color)
of RookID:
game.genLegalRookMoves(field, color)
of QueenID:
game.genLegalQueenMoves(field, color)
of KingID:
game.genLegalKingMoves(field, color)
else:
@[]
return legal_moves
proc genLegalMoves*(game: Game, color: Color): seq[Move] =
## Generates all legal moves in a `game` for a `color`.
var legal_moves = newSeq[Move]()
for field in game.board.low..game.board.high:
legal_moves.add(game.genLegalMoves(field, color))
return legal_moves
proc castling(game: var Game, kstart: int, dest_kingside: bool,
color: Color): bool {.discardable.} =
## Tries to castle in a given `game` with the king of a given `color` from `start`.
## `dest_kingside` for kingside castling, else castling is queenside.
## This process checks for the legality of the move and performs the switch of `game.to_move`
try:
if game.toMove != color:
return false
var kdest = kstart
var rstart: int
var rdest: int
if (dest_kingside):
kdest = kstart + (E+E)
rstart = kstart + (E+E+E)
rdest = rstart + (W+W)
else:
rstart = kstart + (W+W+W+W)
rdest = rstart + (E+E+E)
kdest = kstart + (W+W)
if not game.moved.getField(kstart) and not game.moved.getField(rstart):
var check = false
if (dest_kingside):
check = check or game.isAttacked(kstart, color)
check = check or game.isAttacked(kstart+(E), color)
check = check or game.isAttacked(kstart+(E+E), color)
else:
check = check or game.isAttacked(kstart, color)
check = check or game.isAttacked(kstart+(W), color)
check = check or game.isAttacked(kstart+(W+W), color)
if check:
return false
game.uncheckedMove(kstart, kdest)
game.uncheckedMove(rstart, rdest)
game.toMove = Color(ord(game.toMove)*(-1))
return true
return false
except IndexDefect, ValueError:
return false
proc checkedMove*(game: var Game, move: Move): bool {.discardable.} =
## Tries to make a move in a given `game` with the piece of a given `color` from `start` to `dest`.
## This process checks for the legality of the move and performs the switch of `game.to_move`
try:
let start = move.start
let dest = move.dest
let color = move.color
let prom = move.prom
if game.toMove != color:
return false
var sequence = newSeq[Move]()
let piece = game.board.getField(start)
var create_en_passant = false
var captured_en_passant = false
var move: Move
move = getMove(start, dest, color)
if (piece == PawnID * ord(color)):
create_en_passant = dest in game.genPawnDoubleDests(start, color)
captured_en_passant = (game.board.getField(dest) == -1 * ord(color) * EnPassantID)
sequence.add(game.genLegalMoves(start, color))
if (move in sequence):
game.board.removeEnPassant(color)
if (piece == KingID * ord(color) and (start - dest == (W+W))):
return game.castling(start, true, color)
elif (piece == KingID * ord(color) and (start - dest == (E+E))):
return game.castling(start, false, color)
else:
game.uncheckedMove(start, dest)
game.toMove = Color(ord(game.toMove)*(-1))
if create_en_passant:
game.board.setField(dest-(N*ord(color)), EnPassantID * ord(color))
if captured_en_passant:
game.board.setField(dest-(N*ord(color)), 0)
if ((90 < dest and dest < 99) or (20 < dest and dest < 29)) and
game.board.getField(dest) == PawnID * ord(color):
game.board.setField(dest, prom)
return true
except IndexDefect, ValueError:
return false
proc hasNoMoves(game: Game, color: Color): bool =
## Checks if a player of a given `color` has no legal moves in a `game`.
return (game.genLegalMoves(color) == @[])
proc isCheckmate*(game: Game, color: Color): bool =
## Checks if a player of a given `color` in a `game` is checkmate.
return game.hasNoMoves(color) and game.isInCheck(color)
proc isStalemate*(game: Game, color: Color): bool =
## Checks if a player of a given `color` in a `game` is stalemate.
return (game.hasNoMoves(color) and not game.isInCheck(color)) or
game.board.checkInsufficientMaterial()

13
createCfg.nim Normal file
View File

@@ -0,0 +1,13 @@
import os
import src/project
let projDir = getProjRootDir()
var tmpPath: string
for dir in ["src", "tests"]:
tmpPath = joinPath(projDir, dir)
var f = open(joinPath(tmpPath, "nim.cfg"), fmWrite)
f.writeLine("--outdir:\"" & joinPath(projDir, "bin", dir) & "\"")
f.writeLine("--path:\"" & joinPath(projDir, "src") & "\"")
f.close()

View File

@@ -1,21 +0,0 @@
from strutils import parseInt
import rdstdin
import ./chess
proc runGame(): void =
var game = initGame()
game.echoBoard(game.toMove)
while not game.isCheckmate(game.toMove):
echo "Make a move"
echo game.toMove
var move = readLine(stdin)
while not game.checkedMove(notationToMove(move, game.toMove)):
move = readLine(stdin)
game.echoBoard(game.toMove)
if game.isCheckmate(game.toMove):
echo $game.toMove & " was checkmated"
if game.isStalemate(game.toMove):
echo "Stalemate"
runGame()

847
src/chess.nim Normal file
View File

@@ -0,0 +1,847 @@
import algorithm
import sequtils
import strutils
import sugar
import tables
type
Color* = enum
## `Color` describes the possible color of players.
Black = -1,
White = 1
Board = array[120, int] ## \
## `Board` saves the position of the chess pieces.
CastleRights = tuple
## `CastleRights` contains the rights to castling for each player.
wk: bool # `wk` describes White kingside castle
wq: bool # `wq` describes White queenside castle
bk: bool # `bk` describes Black kingside castle
bq: bool # `bq` describes Black queenside castle
Chess* = object
## `Chess` stores all important information of a chess chess.
board*: Board
toMove*: Color
previousBoard: seq[Board]
halfMoveClock: int
fullMoveCounter*: int
castleRights: CastleRights
enPassantSquare: int
Move* = object
## `Move` stores all important information for a move.
start: int
dest: int
color: Color
prom: int
PieceAmount = tuple
## `PieceAmount` describes the number of pieces of a certain type a/both
## player/s has/have.
p: int # `p` describes the amount of pawns.
n: int # `n` describes the amount of knights.
b: int # `b` describes the amount of bishops.
r: int # `r` describes the amount of rooks.
q: int # `q` describes the amount of queens.
Pieces = array[10, int]
const
Block = 999 ## \
## `Block` is the value assigned to empty blocked fields in a board.
Empty = 0 ## \
## `Empty` is the value assigned to empty fields on a board.
WPawn* = 1
## `WPawn` is the value assigned to a square in a board with a white pawn.
WKnight* = 2 ## \
## `WKnight` is the value assigned to a square in a board with a white
## knight.
WBishop* = 3 ## \
## `WBishop` is the value assigned to a square in a board with a white
## bishop.
WRook* = 4 ## \
## `WRook` is the value assigned to a square in a board with a white rook.
WQueen* = 5 ## \
## `WQueen` is the value assigned to a square in a board with a white
## queen.
WKing* = 6 ## \
## `WKing` is the value assigned to a square in a board with a white king.
BPawn* = -WPawn ## \
## `BPawn` is the value assigned to a square in a board with a black pawn.
BKnight* = -WKnight ## \
## `BKnight` is the value assigned to a square in a board with a black\
## knight.
BBishop* = -WBishop ## \
## `BBishop` is the value assigned to a square in a board with a black\
## bishop.
BRook* = -WRook ## \
## `BRook` is the value assigned to a square in a board with a black rook.
BQueen* = -WQueen ## \
## `BQueen` is the value assigned to a square in a board with a black queen.
BKing* = -WKing ## \
## `BKing` is the value assigned to a square in a board with a black king.
N = 10 ## `N` describes a move a field up the board from whites perspective.
S = -N ## `S` describes a move a field down the board from whites perspective.
W = 1 ## `W` describes a move a field to the left from whites perspective.
E = -W ## `E` describes a move a field to the right from whites perspective.
# Directions for the pieces. Special moves are in separate arrays.
Knight_Moves = @[N+N+E, N+N+W, E+E+N, E+E+S, S+S+E, S+S+W, W+W+N, W+W+S] ## \
## `Knight_Moves` describes the possible knight moves.
Bishop_Moves = @[N+E, N+W, S+E, S+W] ## \
## `Bishop_Moves` describes the possible 1 field distance bishop moves.
Rook_Moves = @[N, E, S, W] ## \
## `Rook_Moves` describes the possible 1 field distance rook moves.
Queen_Moves = @[N, E, S, W, N+E, N+W, S+E, S+W] ## \
## `Queen_Moves` describes the possible 1 field distance queen moves.
King_Moves = @[N, E, S, W, N+E, N+W, S+E, S+W] ## \
## `King_Moves` describes the possible 1 field distance king moves.
King_Moves_White_Castle = @[E+E, W+W] ## \
## `King_Moves` describes the possible king moves for castling.
Pawn_Moves_White = @[N] ## \
## `Pawn_Moves_White` describes the possible 1 field distance pawn moves
## from whites perspective that are not attacks.
Pawn_Moves_White_Double = @[N+N] ## \
## `Pawn_Moves_White_Double` describes the possible pawn 2 field distance
## moves from whites perspective.
Pawn_Moves_White_Attack = @[N+E, N+W] ## \
## `Pawn_Moves_White` describes the possible 1 field distance pawn moves
## from whites perspective that are ttacks.
InsufficientMaterial: array[4, PieceAmount] = [
(0, 0, 0, 0, 0), # only kings
(0, 0, 1, 0, 0), # bishop only
(0, 1, 0, 0, 0), # knight only
(0, 2, 0, 0, 0) # 2 knights
] ## `InsufficientMaterial` describes the pieces where no checkmate can be
## forced
let
PieceChar = {
0: " ",
WPawn: "P",
WKnight: "N",
WBishop: "B",
WRook: "R",
WQueen: "Q",
WKing: "K",
BPawn: "p",
BKnight: "n",
BBishop: "b",
BRook: "r",
BQueen: "q",
BKing: "k",
}.newTable ## \
## `PieceChar` describes the representation for the pieceIDs for the cli.
FileChar = {
"a": 0,
"b": 1,
"c": 2,
"d": 3,
"e": 4,
"f": 5,
"g": 6,
"h": 7,
}.newTable ## \
# `FileChar` maps the files of the chessboard to numbers for better
# conversion.
proc fieldToInd(field: string): int =
## Calculate and return board index from `field` of a chess board.
## Returns -1 if the `field` was not input correct.
try:
var file = $field[0]
var line = parseInt($field[1])
return 1 + (line + 1) * 10 + (7 - FileChar[file])
except IndexDefect, ValueError:
return -1
proc indToField(ind: int): string =
## Calculate and returns field name from board index `ind`.
let line = int(ind / 10 - 1)
let file_ind = 7 - (ind %% 10 - 1)
for file, i in FileChar:
if FileChar[file] == file_ind:
return $file & $line
proc getMove(start: int, dest: int, prom: int, color: Color): Move =
## Get a move object of the `color` player from `start` to `dest` with an
## eventual promition to `prom`.
var move = Move(start: start, dest: dest, prom: prom * ord(color), color: color)
if prom < WKnight or prom > WQueen:
move.prom = WQueen
return move
proc getMove(start: int, dest: int, color: Color): Move =
## Get a move object of the `color` player from `start` to `dest` with
## automatic promition to queen.
var move = Move(start: start, dest: dest, prom: WQueen * ord(color), color: color)
return move
proc notationToMove*(notation: string, color: Color): Move =
## Convert and return simplified algebraic chess `notation` to a move object,
## color of player is `color`.
var move: Move
if notation.len < 4:
return getMove(-1, -1, -1, color)
var start = fieldToInd(notation[0..1])
var dest = fieldToInd(notation[2..3])
move = getMove(start, dest, color)
if notation.len > 4:
var promStr = $notation[4]
let prom = case promStr:
of "R":
WRook
of "B":
WBishop
of "N":
WKnight
else:
WQueen
move = getMove(start, dest, prom, color)
return move
proc moveToNotation*(move: Move, board: Board): string =
## Convert and return a `move` object to simplified algebraic chess notation.
var res: string
var start = indToField(move.start)
res.add(start)
var dest = indToField(move.dest)
res.add(dest)
var color = move.color
var prom = PieceChar[move.prom]
if abs(board[move.start]) == WPawn and ((color == Color.White and dest[1] ==
'8') or (color == Color.Black and dest[1] == '1')):
res.add(prom)
return res
proc initBoard(): Board =
## Create and return a board with pieces in starting position.
let board = [
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, WRook, WKnight, WBishop, WKing, WQueen, WBishop, WKnight, WRook, Block,
Block, WPawn, WPawn, WPawn, WPawn, WPawn, WPawn, WPawn, WPawn, Block,
Block, 0, 0, 0, 0, 0, 0, 0, 0, Block,
Block, 0, 0, 0, 0, 0, 0, 0, 0, Block,
Block, 0, 0, 0, 0, 0, 0, 0, 0, Block,
Block, 0, 0, 0, 0, 0, 0, 0, 0, Block,
Block, BPawn, BPawn, BPawn, BPawn, BPawn, BPawn, BPawn, BPawn, Block,
Block, BRook, BKnight, BBishop, BKing, BQueen, BBishop, BKnight, BRook, Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block]
return board
proc initBoard(board: array[64, int]): Board =
## Create and return a board with pieces in position of choice, described in
## `board`.
let board = [
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, board[0], board[1], board[2], board[3], board[4], board[5],
board[6], board[7], Block,
Block, board[8], board[9], board[10], board[11], board[12], board[13],
board[14], board[15], Block,
Block, board[16], board[17], board[18], board[19], board[20], board[
21], board[22], board[23], Block,
Block, board[24], board[25], board[26], board[27], board[28], board[
29], board[30], board[31], Block,
Block, board[32], board[33], board[34], board[35], board[36], board[
37], board[38], board[39], Block,
Block, board[40], board[41], board[42], board[43], board[44], board[
45], board[46], board[47], Block,
Block, board[48], board[49], board[50], board[51], board[52], board[
53], board[54], board[55], Block,
Block, board[56], board[57], board[58], board[59], board[60], board[
61], board[62], board[63], Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block,
Block, Block, Block, Block, Block, Block, Block, Block, Block, Block]
return board
proc initChess*(): Chess =
## Create and return a Chess object.
let chess = Chess(board: initBoard(),
to_move: Color.White, castleRights: (true, true, true, true), fullMoveCounter: 1, enPassantSquare: -1)
return chess
proc initChess*(fen: string): Chess =
## Create and return a Chess object from `fen`.
var revPieceChar = toSeq(PieceChar.pairs).map(y => (y[1], y[0])).toTable
var fenArr = fen.split(" ")
var squares: array[64, int]
var squaresInd: int
for i, subc in fenArr[0].reversed():
if subc == '/':
continue
if subc.isDigit():
for j in 1..parseInt($subc):
squares[squaresInd] = revPieceChar[" "]
squaresInd += 1
continue
else:
squares[squaresInd] = revPieceChar[$subc]
squaresInd += 1
var board = initBoard(squares)
var toMove: Color
if fenArr[1] == "w":
toMove = Color.White
else:
toMove = Color.Black
var castleRights: CastleRights
if fenArr[2].contains("K"):
castleRights.wk = true
if fenArr[2].contains("Q"):
castleRights.wq = true
if fenArr[2].contains("k"):
castleRights.bk = true
if fenArr[2].contains("q"):
castleRights.bq = true
var enPassantSquare = -1
if fenArr[3] != "-":
enPassantSquare = fieldToInd(fenArr[3])
let halfMoveClock = parseInt(fenArr[4])
let fullMoveCounter = parseInt(fenArr[5])
return Chess(board: board, toMove: toMove, castleRights: castleRights,
halfMoveClock: halfMoveClock, fullMoveCounter: fullMoveCounter, enPassantSquare: enPassantSquare)
proc convertToFen*(chess: Chess): string =
## Build and return a fen string from a given `chess` object.
var pieces: string
var fen: string
var spaceOcc: int
var fileCounter = 0
for i, piece in chess.board.reversed:
if not (piece == Block):
if fileCounter == 8:
if spaceOcc != 0:
pieces &= $spaceOcc
spaceOcc = 0
pieces &= "/"
fileCounter = 0
if PieceChar[piece] == " ":
spaceOcc += 1
else:
if spaceOcc != 0:
pieces &= $spaceOcc
spaceOcc = 0
pieces &= PieceChar[piece]
fileCounter += 1
if i == chess.board.reversed.high and spaceOcc > 0:
pieces &= $spaceOcc
fen &= pieces & " "
if chess.toMove == Color.White:
fen &= "w "
else:
fen &= "b "
var castleR: string
if chess.castleRights.wk:
castleR &= "K"
if chess.castleRights.wq:
castleR &= "Q"
if chess.castleRights.bk:
castleR &= "k"
if chess.castleRights.bq:
castleR &= "q"
if castleR.isEmptyOrWhitespace:
castleR = "-"
fen &= castleR & " "
if chess.enPassantSquare != -1:
fen &= indToField(chess.enPassantSquare) & " "
else:
fen &= "- "
fen &= $chess.halfMoveClock & " " & $chess.fullMoveCounter
return fen
proc echoBoard*(chess: Chess, color: Color) =
## Prints out the given `board` with its pieces as characters and line
## indices from perspecive of `color`.
var line_str: string
if color == Color.Black:
for i in 0..chess.board.len-1:
if chess.board[i] == Block:
continue
line_str &= PieceChar[chess.board[i]] & " "
if (i + 2) %% 10 == 0:
line_str &= $int((i / 10) - 1) & "\n"
echo line_str
echo "h g f e d c b a"
else:
for i in countdown(len(chess.board) - 1, 0):
if chess.board[i] == Block:
continue
line_str &= PieceChar[chess.board[i]] & " "
if (i - 1) %% 10 == 0:
line_str &= $int((i / 10) - 1) & "\n"
echo line_str
echo "a b c d e f g h"
proc genPawnAttackDests(chess: Chess, field: int, color: Color): seq[int] =
## Generate possible attack destinations for a pawn with specific `color`
## located at index `field` of `chess`.
## Returns a sequence of possible indices to move to.
if field < chess.board.low or field > chess.board.high:
return @[]
var res = newSeq[int]()
var dest: int
var target: int
for attacks in Pawn_Moves_White_Attack:
dest = field + (attacks * ord(color))
if dest == chess.enPassantSquare:
res.add(dest)
if not dest in chess.board.low..chess.board.high:
continue
target = chess.board[dest]
if target == Block or ord(color) * target >= 0:
continue
res.add(dest)
return res
proc genPawnDoubleDests(chess: Chess, field: int, color: Color): seq[int] =
## Generate possible double destinations for a pawn with specific `color`
## located at index `field` of `chess`.
## Returns a sequence of possible indices to move to.
if field < chess.board.low or field > chess.board.high:
return @[]
var res = newSeq[int]()
var dest: int
var target: int
for doubles in Pawn_Moves_White_Double:
dest = field + doubles * ord(color)
target = chess.board[dest]
if target != 0 or
chess.board[dest + (S * ord(color))] != 0:
continue
if color == Color.White and not (field in fieldToInd("h2")..fieldToInd("a2")):
continue
if color == Color.Black and not (field in fieldToInd("h7")..fieldToInd("a7")):
continue
res.add(dest)
return res
proc genPawnDests(chess: Chess, field: int, color: Color): seq[int] =
## Generate possible destinations for a pawn with specific `color` located at
## index `field` of `chess`.
## Returns a sequence of possible indices to move to.
if field < chess.board.low or field > chess.board.high:
return @[]
var res = newSeq[int]()
var dest: int
var target: int
for move in Pawn_Moves_White:
dest = field + move * ord(color)
if not dest in chess.board.low..chess.board.high:
continue
target = chess.board[dest]
if target != 0 and dest != chess.enPassantSquare:
continue
res.add(dest)
res.add(chess.genPawnAttackDests(field, color))
res.add(chess.genPawnDoubleDests(field, color))
return res
proc genKnightDests(chess: Chess, field: int, color: Color): seq[int] =
## Generate possible destinations for a knight with specific `color` located
## at index `field` of `chess`.
## Returns a sequence of possible indices to move to.
if field < chess.board.low or field > chess.board.high:
return @[]
var res = newSeq[int]()
var dest: int
var target: int
for move in Knight_Moves:
dest = field + move
if not dest in chess.board.low..chess.board.high:
continue
target = chess.board[dest]
if target == Block or ord(color) * target > 0:
continue
res.add(dest)
return res
proc genSlidePieceDests(chess: Chess, field: int, color: Color, moves: seq[
int]): seq[int] =
## Generate possible destinations for a piece with `moves` and specific `color`
## located at index `field` of `chess`.
## Returns a sequence of possible indices to move to.
if field < chess.board.low or field > chess.board.high:
return @[]
var res = newSeq[int]()
var dest: int
var target: int
for move in moves:
dest = field + move
if not dest in chess.board.low..chess.board.high:
continue
target = chess.board[dest]
while target != Block and ord(color) * target <= 0:
res.add(dest)
if ord(color) * target < 0:
break
dest = dest + move
target = chess.board[dest]
return res
proc genBishopDests(chess: Chess, field: int, color: Color): seq[int] =
## Generate possible destinations for a bishop with specific `color` located
## at index `field` of `chess`.
## Returns a sequence of possible indices to move to.
return genSlidePieceDests(chess, field, color, Bishop_Moves)
proc genRookDests(chess: Chess, field: int, color: Color): seq[int] =
## Generate possible destinations for a rook with specific `color` located at
## index `field` of `chess`.
## Returns a sequence of possible indices to move to.
return genSlidePieceDests(chess, field, color, Rook_Moves)
proc genQueenDests(chess: Chess, field: int, color: Color): seq[int] =
## Generate possible destinations for a queen with specific `color` located
## at index `field` of `chess`.
## Returns a sequence of possible indices to move to.
return genSlidePieceDests(chess, field, color, Queen_Moves)
proc genKingCastleDest(chess: Chess, field: int, color: Color): seq[int] =
## Generate possible castle destinations for a king with specific `color`
## located at index `field` of `chess`
## Returns a sequence of possible indices to move to.
if field < chess.board.low or field > chess.board.high:
return @[]
var res = newSeq[int]()
var dest: int
var target: int
var half_dest: int
var half_target: int
for castle in King_Moves_White_Castle:
dest = field + castle
if not dest in chess.board.low..chess.board.high:
continue
target = chess.board[dest]
half_dest = field + int(castle / 2)
half_target = chess.board[half_dest]
if target == Block or target != 0:
continue
if half_target == Block or half_target != 0:
continue
res.add(dest)
return res
proc genKingDests(chess: Chess, field: int, color: Color): seq[int] =
## Generate possible destinations for a king with specific `color`
## located at index `field` of `chess`.
## Returns a sequence of possible indices to move to.
if field < chess.board.low or field > chess.board.high:
return @[]
var res = newSeq[int]()
var dest: int
var target: int
for move in King_Moves:
dest = field + move
if not dest in chess.board.low..chess.board.high:
continue
target = chess.board[dest]
if target == Block or ord(color) * target > 0:
continue
res.add(dest)
res.add(chess.genKingCastleDest(field, color))
return res
proc pieceOn(chess: Chess, color: Color, sequence: seq[int],
pieceID: int): bool =
## Returns true if the `PieceID` of a given `color` is in `sequence` else
## wrong.
for check in sequence:
if chess.board[check] == ord(color) * -pieceID:
return true
return false
proc isAttacked(chess: Chess, position: int, color: Color): bool =
## Returns true if a `position` in a `chess` is attacked by the opposite
## color of `color`.
var attacked = false
attacked = attacked or chess.pieceOn(color, chess.genPawnAttackDests(
position, color), WPawn)
attacked = attacked or chess.pieceOn(color, chess.genQueenDests(position,
color), WQueen)
attacked = attacked or chess.pieceOn(color, chess.genKingDests(position,
color), WKing)
attacked = attacked or chess.pieceOn(color, chess.genRookDests(position,
color), WRook)
attacked = attacked or chess.pieceOn(color, chess.genBishopDests(position,
color), WBishop)
attacked = attacked or chess.pieceOn(color, chess.genKnightDests(position,
color), WKnight)
return attacked
proc isInCheck(chess: Chess, color: Color): bool =
## Returns true if the king of a given `color` is in check in a `chess`.
var king_pos: int
for i in 0..chess.board.high:
if chess.board[i] == ord(color) * WKing:
king_pos = i
return chess.isAttacked(king_pos, color)
proc uncheckedMove(chess: var Chess, start: int, dest: int): bool {.discardable.} =
## Moves a piece if possible from `start` position to `dest` position in a
## `chess`.
let piece = chess.board[start]
chess.board[start] = 0
chess.board[dest] = piece
if start == fieldToInd("e1"):
chess.castleRights.wk = false
chess.castleRights.wq = false
elif start == fieldToInd("h1"):
chess.castleRights.wk = false
elif start == fieldToInd("a1"):
chess.castleRights.wq = false
elif start == fieldToInd("e8"):
chess.castleRights.bk = false
chess.castleRights.bq = false
elif start == fieldToInd("h8"):
chess.castleRights.bk = false
elif start == fieldToInd("a8"):
chess.castleRights.bq = false
return true
proc moveLeadsToCheck(chess: Chess, start: int, dest: int,
color: Color): bool =
## Returns true if a move from `start` to `dest` in a `chess` puts the `color`
## king in check.
var check = chess
check.uncheckedMove(start, dest)
return check.isInCheck(color)
proc genPawnPromotion(board: Board, move: Move, color: Color): seq[Move] =
## Generate all possible promotions on a `board` of a `move` by `color`.
var promotions = newSeq[Move]()
let start = move.start
let dest = move.dest
if board[start] != WPawn * ord(color):
return @[]
if (fieldToInd("h8") <= dest and fieldToInd("a8") >= dest and color == Color.White) or
(fieldToInd("h1") <= dest and fieldToInd("a1") >= dest and color == Color.Black):
for piece in WKnight..WQueen:
promotions.add(getMove(start, dest, piece, color))
return promotions
proc genLegalMovesStd(chess: Chess, field: int, color: Color, piece: int,
moves: seq[int]): seq[Move] =
## Generates all legal knight moves in a `chess` starting from `field` for a
## `color`.
if chess.board[field] != piece * ord(color):
return @[]
var res = newSeq[Move]()
for dest in moves:
if not chess.moveLeadsToCheck(field, dest, color):
res.add(getMove(field, dest, color))
return res
proc genLegalPawnMoves(chess: Chess, field: int, color: Color): seq[Move] =
## Generates all legal pawn moves in a `chess` starting from `field` for a
## `color`.
if chess.board[field] != WPawn * ord(color):
return @[]
var res = newSeq[Move]()
var moves = chess.genPawnDests(field, color)
for dest in moves:
if not chess.moveLeadsToCheck(field, dest, color):
var promotions = chess.board.genPawnPromotion(getMove(field, dest, color), color)
if promotions != @[]:
res.add(promotions)
else:
res.add(getMove(field, dest, color))
return res
proc genLegalKnightMoves(chess: Chess, field: int, color: Color): seq[Move] =
## Generates all legal knight moves in a `chess` starting from `field` for a
## `color`.
return genLegalMovesStd(chess, field, color, WKnight, chess.genKnightDests(
field, color))
proc genLegalBishopMoves(chess: Chess, field: int, color: Color): seq[Move] =
## Generates all legal bishop moves in a `chess` starting from `field` for a
## `color`.
return genLegalMovesStd(chess, field, color, WBishop, chess.genBishopDests(
field, color))
proc genLegalRookMoves(chess: Chess, field: int, color: Color): seq[Move] =
## Generates all legal rook moves in a `chess` starting from `field` for a
## `color`.
return genLegalMovesStd(chess, field, color, WRook, chess.genRookDests(
field, color))
proc genLegalQueenMoves(chess: Chess, field: int, color: Color): seq[Move] =
## Generates all legal queen moves in a `chess` starting from `field` for a
## `color`.
return genLegalMovesStd(chess, field, color, WQueen, chess.genQueenDests(
field, color))
proc genLegalKingMoves(chess: Chess, field: int, color: Color): seq[Move] =
## Generates all legal king moves in a `chess` starting from `field` for a
## `color`.
return genLegalMovesStd(chess, field, color, WKing, chess.genKingDests(
field, color))
proc genLegalMoves(chess: Chess, field: int, color: Color): seq[Move] =
## Generates all legal moves in a `chess` starting from `field` for a `color`.
var legal_moves = newSeq[Move]()
var target = abs(chess.board[field])
if WPawn <= target and target <= WKing:
legal_moves = case target:
of WPawn:
chess.genLegalPawnMoves(field, color)
of WKnight:
chess.genLegalKnightMoves(field, color)
of WBishop:
chess.genLegalBishopMoves(field, color)
of WRook:
chess.genLegalRookMoves(field, color)
of WQueen:
chess.genLegalQueenMoves(field, color)
of WKing:
chess.genLegalKingMoves(field, color)
else:
@[]
return legal_moves
proc genLegalMoves*(chess: Chess, color: Color): seq[Move] =
## Generates all legal moves in a `chess` for a `color`.
var legal_moves = newSeq[Move]()
for field in chess.board.low..chess.board.high:
legal_moves.add(chess.genLegalMoves(field, color))
return legal_moves
proc castling(chess: var Chess, kstart: int, dest_kingside: bool,
color: Color): bool {.discardable.} =
## Tries to castle in a given `chess` with the king of a given `color` from
## `kstart`.
## `dest_kingside` for kingside castling, else castling is queenside.
## This process checks for the legality of the move and performs the switch
## of `chess.to_move`
if chess.toMove != color:
return false
var kdest: int
var rstart: int
var rdest: int
var rights: bool
if dest_kingside:
kdest = kstart + E + E
rstart = kstart + E + E + E
rdest = rstart + W + W
if color == Color.White:
rights = chess.castleRights.wk
else:
rights = chess.castleRights.bk
else:
kdest = kstart + W + W
rstart = kstart + W + W + W + W
rdest = rstart + E + E + E
if color == Color.White:
rights = chess.castleRights.wq
else:
rights = chess.castleRights.bq
if rights:
if dest_kingside:
if chess.isAttacked(kstart, color) or chess.isAttacked(kstart+E, color) or
chess.isAttacked(kstart+E+E, color) or chess.board[kstart+E] != 0 or
chess.board[kstart+E+E] != 0:
return false
else:
if chess.isAttacked(kstart, color) or chess.isAttacked(kstart+W, color) or
chess.isAttacked(kstart+W+W, color) or chess.board[kstart+W] != 0 or
chess.board[kstart+W+W] != 0 or chess.board[kstart+W+W+W] != 0:
return false
chess.uncheckedMove(kstart, kdest)
chess.uncheckedMove(rstart, rdest)
chess.previousBoard = @[chess.board]
chess.toMove = Color(ord(chess.toMove) * (-1))
chess.enPassantSquare = -1
return true
return false
proc checkedMove*(chess: var Chess, move: Move): bool {.discardable.} =
## Tries to make a `move` in a given `chess``.
## This process checks for the legality of the move and performs the switch
## of `chess.to_move` with exception of castling (castling() switches).
let start = move.start
let dest = move.dest
let color = move.color
let prom = move.prom
if chess.toMove != color or start == -1 or dest == -1:
return false
let piece = chess.board[start]
var createEnPassant: bool
var capturedEnPassant: bool
var fiftyMoveRuleReset: bool
var move: Move
move = getMove(start, dest, color)
if piece == WKing * ord(color) and start - dest == (W+W):
return chess.castling(start, true, color)
elif piece == WKing * ord(color) and start - dest == (E+E):
return chess.castling(start, false, color)
if piece == WPawn * ord(color):
createEnPassant = dest in chess.genPawnDoubleDests(start, color)
capturedEnPassant = (dest == chess.enPassantSquare)
fiftyMoveRuleReset = true
if chess.board[move.dest] != 0:
fiftyMoveRuleReset = true
if move in chess.genLegalMoves(start, color):
chess.enPassantSquare = -1
chess.uncheckedMove(start, dest)
chess.toMove = Color(ord(chess.toMove)*(-1))
if capturedEnPassant:
chess.board[dest - (N * ord(color))] = 0
if ((fieldToInd("h8") <= dest and dest <= fieldToInd("a8")) or
(fieldToInd("h1") <= dest and dest <= fieldToInd("a1"))) and
piece == WPawn * ord(color):
chess.board[dest] = prom
chess.previousBoard.add(chess.board)
chess.halfMoveClock = chess.halfMoveClock + 1
if color == Color.Black:
chess.fullMoveCounter += 1
if fiftyMoveRuleReset:
chess.halfMoveClock = 0
chess.previousBoard = @[chess.board]
if createEnPassant and (chess.board[dest + E] == BPawn * ord(color) or
chess.board[dest + W] == BPawn * ord(color)):
chess.enPassantSquare = dest - (N * ord(color))
chess.previousBoard = @[]
return true
proc isCheckmate*(chess: Chess, color: Color): bool =
## Returns true if the `color` player is checkmate in a `chess`.
return chess.genLegalMoves(color) == @[] and chess.isInCheck(color)
proc threeMoveRep(chess: Chess): bool =
## Returns true if a 3-fold repitition happened on the last move of the
## `chess`.
if chess.previousBoard == @[]:
return false
var lastState = chess.previousBoard[chess.previousBoard.high]
var reps: int
for stateInd in chess.previousBoard.low..chess.previousBoard.high:
if chess.previousBoard[stateInd] == lastState:
reps = reps + 1
return reps >= 3
proc isDrawClaimable*(chess: Chess): bool =
## Returns true if a draw is claimable by the current player.
return chess.threeMoveRep() or chess.halfMoveClock >= 100
proc checkInsufficientMaterial(board: Board): bool =
## Checks for combinations of pieces on a `board`, where no checkmate can be
## forced.
## Returns true if no player can force a checkmate to the other.
var pieces: Pieces
for field in board.low..board.high:
var piece = board[field]
var index: int
if piece >= WPawn and piece <= WRook:
index = piece - WPawn # map lowest value to 0
pieces[index] += 1
elif piece <= BPawn and piece >= BRook:
index = WRook - piece # map black pieces after whites
pieces[index] += 1
let wpieces: PieceAmount = (pieces[0], pieces[1], pieces[2], pieces[3],
pieces[4])
let bpieces: PieceAmount = (pieces[5], pieces[6], pieces[7], pieces[8],
pieces[9])
return wpieces in InsufficientMaterial and bpieces in InsufficientMaterial
proc isStalemate*(chess: Chess, color: Color): bool =
## Returns true if the `color` player is stalemate in a `chess`.
return (chess.genLegalMoves(color) == @[] and not chess.isInCheck(color)) or
chess.board.checkInsufficientMaterial()

191
src/engine/engine.nim Normal file
View File

@@ -0,0 +1,191 @@
import algorithm
import chess
type
MoveTree = object
## `Movetree` is a visualization for possible moves.
chess: Chess
evaluation: float
children: seq[Movetree]
const
PawnVal = 100 ## `PawnVal` is the engines value for a pawn.
KnightVal = 310 ## `KnightVal` is the engines value for a knight.
BishopVal = 330 ## `BishopVal` is the engines value for a bishop.
RookVal = 500 ## `RookVal` is the engines value for a rook.
QueenVal = 900 ## `QueenVal` is the engines value for a queen.
CheckmateVal = -100000 ## `CheckmateVal` is the engines value for a checkmate.
DrawVal = 0 ## `DrawVal` is the engines value for a draw.
HiVal = 10000 ## `LoVal` is a value always lower than any evaluation.
LoVal = -HiVal ## `LoVal` is a value always lower than any evaluation.
pawnTable = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 5, 10, 10, -20, -20, 10, 10, 5, 0,
0, 5, -5, -10, 0, 0, -10, -5, 5, 0,
0, 0, 0, 0, 20, 20, 0, 0, 0, 0,
0, 5, 5, 10, 25, 25, 10, 5, 5, 0,
0, 10, 10, 20, 30, 30, 20, 10, 10, 0,
0, 50, 50, 50, 50, 50, 50, 50, 50, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
] ## `pawnTable` is the piece-square table for pawns.
knightTable = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, -50, -40, -30, -30, -30, -30, -40, -50, 0,
0, -40, -20, 0, 5, 5, 0, -20, -40, 0,
0, -30, 0, 10, 15, 15, 10, 0, -30, 0,
0, -30, 5, 15, 20, 20, 15, 5, -30, 0,
0, -30, 0, 15, 20, 20, 15, 0, -30, 0,
0, -30, 5, 10, 15, 15, 10, 5, -30, 0,
0, -40, -20, 0, 0, 0, 0, -20, -40, 0,
0, -50, -40, -30, -30, -30, -30, -40, -50, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
] ## `knightTable` is the piece-square table for pawns.
bishopTable = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, -20, -10, -10, -10, -10, -10, -10, -20, 0,
0, -10, 5, 0, 0, 0, 0, 5, -10, 0,
0, -10, 10, 10, 10, 10, 10, 10, -10, 0,
0, -10, 0, 10, 10, 10, 10, 0, -10, 0,
0, -10, 5, 5, 10, 10, 5, 5, -10, 0,
0, -10, 0, 5, 10, 10, 5, 0, -10, 0,
0, -10, 0, 0, 0, 0, 0, 0, -10, 0,
0, -20, -10, -10, -10, -10, -10, -10, -20, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
] ## `bishopTable` is the piece-square table for pawns.
rookTable = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 5, 5, 0, 0, 0, 0,
0, -5, 0, 0, 0, 0, 0, 0, -5, 0,
0, -5, 0, 0, 0, 0, 0, 0, -5, 0,
0, -5, 0, 0, 0, 0, 0, 0, -5, 0,
0, -5, 0, 0, 0, 0, 0, 0, -5, 0,
0, -5, 0, 0, 0, 0, 0, 0, -5, 0,
0, 5, 10, 10, 10, 10, 10, 10, 5, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
] ## `rookTable` is the piece-square table for pawns.
queenTable = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, -20, -10, -10, -5, -5, -10, -10, -20, 0,
0, -10, 0, 0, 0, 0, 5, 0, -10, 0,
0, -10, 0, 5, 5, 5, 5, 0, -10, 0,
0, -10, 0, 5, 5, 5, 5, 0, -10, 0,
0, -10, 0, 5, 5, 5, 5, 0, -10, 0,
0, -10, 0, 5, 5, 5, 5, 0, -10, 0,
0, -10, 0, 0, 0, 0, 0, 0, -10, 0,
0, -20, -10, -10, -5, -5, -10, -10, -20, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
] ## `queenTable` is the piece-square table for pawns.
kingTable = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 20, 30, 10, 0, 0, 10, 30, 20, 0,
0, 20, 20, 0, 0, 0, 0, 20, 20, 0,
0, -10, -20, -20, -20, -20, -20, -20, -10, 0,
0, -20, -30, -30, -40, -40, -30, -30, -20, 0,
0, -30, -40, -40, -50, -50, -40, -40, -30, 0,
0, -30, -40, -40, -50, -50, -40, -40, -30, 0,
0, -30, -40, -40, -50, -50, -40, -40, -30, 0,
0, -30, -40, -40, -50, -50, -40, -40, -30, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
] ## `kingTable` is the piece-square table for pawns.
proc pieceEval(chess: Chess): int =
## Returns the evaluation of existing pieces on the `board`
var evaluation: int
for ind, piece in chess.board:
let tmpEval = case piece:
of WPawn:
ord(Color.White) * PawnVal + ord(Color.White) * pawnTable[ind]
of WKnight:
ord(Color.White) * KnightVal + ord(Color.White) * knightTable[ind]
of WBishop:
ord(Color.White) * BishopVal + ord(Color.White) * bishopTable[ind]
of WRook:
ord(Color.White) * RookVal + ord(Color.White) * rookTable[ind]
of WQueen:
ord(Color.White) * QueenVal + ord(Color.White) * queenTable[ind]
of WKing:
ord(Color.White) * kingTable[ind]
of BPawn:
ord(Color.Black) * PawnVal + ord(Color.Black) * pawnTable.reversed[ind]
of BKnight:
ord(Color.Black) * KnightVal + ord(Color.Black) * knightTable.reversed[ind]
of BBishop:
ord(Color.Black) * BishopVal + ord(Color.Black) * bishopTable.reversed[ind]
of BRook:
ord(Color.Black) * RookVal + ord(Color.Black) * rookTable.reversed[ind]
of BQueen:
ord(Color.Black) * QueenVal + ord(Color.Black) * queenTable.reversed[ind]
of BKing:
ord(Color.White) * kingTable.reversed[ind]
else:
DrawVal
evaluation += tmpEval
return evaluation
proc evaluate(chess: Chess): int =
## Returns a complete evaluation of a `chess` with player `toMove` about to make
## a move.
var evaluation: int
if chess.isCheckmate(chess.toMove):
evaluation = ord(chess.toMove) * CheckmateVal
if chess.isStalemate(chess.toMove):
evaluation = DrawVal
else:
evaluation = chess.pieceEval()
if chess.isDrawClaimable():
if chess.toMove == Color.White:
evaluation = max(DrawVal, evaluation)
else:
evaluation = min(DrawVal, evaluation)
return evaluation
proc negaMax(chess: Chess, depth: int, a: int, b: int): int =
## Return the value of a given `chess` with `depth` and `a` and `b` for alpha-beta
## pruning.
var alpha = a
var beta = b
if depth <= 0 or chess.isCheckmate(chess.toMove) or chess.isStalemate(chess.toMove):
return chess.evaluate()
let possibleMoves = chess.genLegalMoves(chess.toMove)
for move in possibleMoves:
var tmpChess = chess
tmpChess.checkedMove(move)
var tmpVal = -negaMax(tmpChess, depth - 1, -beta, -alpha)
if tmpVal >= beta:
return beta
if tmpVal > alpha:
alpha = tmpVal
return alpha
proc bestMove*(chess: Chess, depth: int): Move =
## Generate a MoveTree of a `chess` with a given `depth`, run negaMax and return
## the best evaluated move.
var moves = chess.genLegalMoves(chess.toMove)
var bestMove: Move
var bestEval: int
bestEval = LoVal
for move in moves:
var tmpChess = chess
tmpChess.checkedMove(move)
var tmpEval = -tmpChess.negaMax(depth, LoVal, HiVal)
echo("move:", moveToNotation(move, tmpChess.board), "; eval:", tmpEval)
if tmpEval > bestEval:
bestEval = tmpEval
bestMove = move
return bestMove

View File

@@ -0,0 +1,66 @@
import nimpy
import asyncnet, asyncdispatch
import chess
import engine/secret
import engine/engine
let berserk = pyImport("berserk")
var session = berserk.TokenSession(secret.api_token)
var client = berserk.Client(session = session)
let engineID = "tiyn-ychess"
let engineDifficulty = 2
let toAccept = ["tiynger"]
proc playLichessGame(id: string) {.async.} =
## Plays a lichess game with `id` asynchronously.
var color: Color
var chess = initChess()
for event in client.bots.stream_game_state(id):
echo(event)
if $event["type"] in ["gameState", "gameFull"]:
var movesString: PyObject
if $event["type"] == "gameFull":
echo("gameFull received")
movesString = event["state"]["moves"]
if $event["white"]["id"] == engineID:
color = Color.White
if $event["black"]["id"] == engineID:
color = Color.Black
else:
echo("gameState received")
movesString = event["moves"]
if $movesString != "":
var moves = movesString.split(" ")
chess.checkedMove(notationToMove($moves[-1], chess.toMove))
chess.echoBoard(chess.toMove)
if chess.toMove == color:
echo("engine has to make a move")
var bestMove = moveToNotation(chess.bestMove(engineDifficulty), chess.board)
echo(bestMove)
discard client.bots.make_move(id, bestMove)
proc acceptChallenge(whiteList: openArray[string]): void =
## Accepts a challenge of users in a `whiteList` and starts the
## game process for each.
var events = client.bots.stream_incoming_events()
for event in events:
echo(event)
if $event["type"] == "challenge":
var challenger = $event["challenge"]["challenger"]["id"]
var id = $event["challenge"]["id"]
var speed = $event["challenge"]["speed"]
if challenger in whiteList and speed == "correspondence":
echo("challenge of ", challenger, " whiteList: ", id)
discard client.bots.accept_challenge(id)
discard playLichessGame($event["challenge"]["id"])
else:
discard client.bots.decline_challenge(id)
echo("challenge of ", challenger, " whiteList: ", id)
when isMainModule:
acceptChallenge(toAccept)

256
src/engine/openingBook.nim Normal file
View File

@@ -0,0 +1,256 @@
import db_sqlite
import sequtils
import strutils
import sugar
import tables
import os
include chess
import secret
import project
type
BookMove* = object
## `PossibleMove` capsulates a possible moves in a position with additional
## statistics.
fen*: string # `fen` is the fen string of a position.
move*: string # `move` describes a move in pure coordinate notation.
white*: int # `white` is the number of game white won from this position.
black*: int # `black` is the number of game black won from this position.
draw*: int # `draw` is the number of game drawn from this position.
rating*: int # `rating` is the average rating of the player to move.
let dbConn = joinPath(getProjRootDir(), "ressources/openings.db")
let dbUser = ""
let dbPasswd = ""
let dbName = ""
let tableName = "posmoves"
proc initDB(): void =
## Initialize the database with a table if it doesnt currently exist.
let db = open(dbConn, dbUser, dbPasswd, dbName)
db.exec(sql"""CREATE TABLE IF NOT EXISTS ? (
fen VARCHAR(100) NOT NULL,
move VARCHAR(6) NOT NULL,
white INTEGER NOT NULL,
black INTEGER NOT NULL,
draw INTEGER NOT NULL,
rating INTEGER NOT NULL,
PRIMARY KEY (fen, move)
)""", tableName)
db.close()
echo("Database initialization done.")
proc storeMove(fen: string, move: string, white: bool, black: bool, draw: bool,
rating: int): void =
## Store a possible `move` done by a player with `rating` (0 for unknown)
## in a position described by `fen`.
## The result of the game is described by `white`, `black` and `draw`.
let db = open(dbConn, dbUser, dbPasswd, dbName)
var query = """
INSERT INTO ? (fen, move, white, black, draw, rating)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(fen, move) DO UPDATE SET
"""
if not rating == 0:
query &= "rating = ((rating * (black + white + draw)) + ?) / (black + white + draw + 1),"
if white:
query &= "white = white + 1"
elif black:
query &= "black = black + 1"
else:
query &= "draw = draw + 1"
db.exec(sql(query), tableName, fen, move, int(white), int(black), int(draw),
rating, rating)
db.close()
echo("inserted (", join([fen, move, $white, $black, $draw, $rating], ", "),
") into ", tableName)
proc loadMove*(fen: string): seq[BookMove] =
## Load all possible moves possible in a given position described by `fen`
## from the database. Format moves as a BookMove object.
let db = open(dbConn, dbUser, dbPasswd, dbName)
let res = db.getAllRows(sql """SELECT move, white, black, draw, rating
FROM ?
WHERE fen == ?
ORDER BY rating DESC
""", tableName, fen)
db.close()
var fRes: seq[BookMove]
for entry in res:
var bookMv: BookMove
bookMv.fen = fen
bookMv.move = entry[0]
bookMv.white = parseInt(entry[1])
bookMv.black = parseInt(entry[2])
bookMv.draw = parseInt(entry[3])
bookMv.rating = parseInt(entry[4])
fRes.add(bookMv)
return fRes
proc sanToPcn(sanMoves: string): string =
## Convert a list of `sanMoves` to pure coordinate notation (assuming the game
## starts in the standard initial position)
var sanMoveArr = sanMoves.replace("+").replace("x").replace("#").replace(
"!").replace("?").split(" ")
sanMoveArr.del(sanMoveArr.high)
var fSanMoves: string
var inComment: bool
for word in sanMoveArr:
if inComment:
if word.endsWith("}"):
inComment = false
else:
if word.startsWith("{"):
inComment = true
continue
if not word.endsWith("."):
fSanMoves &= word & " "
var revPieceChar = toSeq(PieceChar.pairs).map(y => (y[1], y[0])).toTable
var chess = initChess()
var lastChess = chess
let sanArr = fsanMoves.split(" ")
var pcnMoves: string
for sanMove in sanArr:
var start: string
var dest: string
if sanMove.isEmptyOrWhitespace:
continue
dest = sanMove[sanMove.high-1..sanMove.high]
if sanMove.contains("="):
let promotion = sanMove[sanMove.high]
if sanMove.len == 5:
let file = sanMove[0]
dest = sanMove[1..2] & promotion
if chess.toMove == Color.White:
start = file & "7"
else:
start = file & "2"
elif sanMove.len == 4:
dest = sanMove[0..1] & promotion
start = indToField(fieldToInd(dest) + ord(chess.toMove) * S)
chess.checkedMove(notationToMove(start & dest, chess.toMove))
elif "abcdefgh".contains(sanMove[0]):
let file = sanMove[0]
for rank in 1..8:
start = file & $rank
if fieldToInd(dest) in chess.genPawnDests(fieldToInd(start), chess.toMove):
if chess.checkedMove(notationToMove(start & dest, chess.toMove)):
break
elif sanMove.startsWith("O-O"):
if chess.toMove == Color.White:
start = "e1"
if sanMove.len == 3:
dest = "g1"
else:
dest = "c1"
else:
start = "e8"
if sanMove.len == 3:
dest = "g8"
else:
dest = "c8"
chess.checkedMove(notationToMove(start & dest, chess.toMove))
else:
var piece = revPieceChar[$sanMove[0]] * ord(chess.toMove)
if sanMove.len == 3:
var possibleStarts: seq[string]
for i, field in chess.board:
if field == piece:
possibleStarts.add(indToField(i))
for possibleStart in possibleStarts:
if chess.checkedMove(notationToMove(possibleStart & dest,
chess.toMove)):
start = possibleStart
break
elif sanMove.len == 4:
if sanMove[1].isDigit():
let rank = $sanMove[1]
for file in "abcdefg":
if chess.board[fieldToInd($file & rank)] == piece:
start = $file & rank
chess.checkedMove(notationToMove(start & dest, chess.toMove))
break
continue
else:
let file = sanMove[1]
for rank in 1..8:
if chess.board[fieldToInd(file & $rank)] == piece:
start = file & $rank
chess.checkedMove(notationToMove(start & dest, chess.toMove))
break
elif sanMove.len == 5:
start = sanMove[1..2]
dest = sanMove[3..4]
chess.checkedMove(notationToMove(start & dest, chess.toMove))
if lastChess == chess:
chess.echoBoard(Color.White)
echo("ERROR OCCURED")
return ""
pcnMoves.add(start & dest & " ")
lastChess = chess
return pcnMoves
proc iterMultiPGN(fileP: string): void =
## Iterate through a (multi) PGN file at `fileP` and store the games in the
## opening database. This function is designed to work with multi PGN files
## from lichess, but should also work for other formats (elo may be not working)
var sanMoves: string
var secondSpace: bool
var white: bool
var black: bool
var wElo: int
var bElo: int
var i: int
var chess: Chess
for line in lines(fileP):
if line.isEmptyOrWhitespace:
if not secondSpace:
secondSpace = true
else:
secondSpace = false
chess = initChess()
for move in sanToPcn(sanMoves).split(" "):
var rating = 0
if chess.toMove == Color.White:
rating = wElo
else:
rating = bElo
storeMove(chess.convertToFen(), move, white, black, not white and
not black, rating)
chess.checkedMove(notationToMove(move, chess.toMove))
i += 1
sanMoves = ""
white = false
black = false
wElo = 0
bElo = 0
if i == 1000:
return
if line.startsWith("["):
if line.contains("Result "):
if line.contains("1-0"):
white = true
elif line.contains("0-1"):
black = true
elif line.contains("WhiteElo"):
var eloStr = line.replace("[WhiteElo \"").replace("\"]")
if eloStr != "?":
wElo = parseInt(eloStr)
else:
wElo = 0
elif line.contains("BlackElo"):
var eloStr = line.replace("[BlackElo \"").replace("\"]")
if eloStr != "?":
bElo = parseInt(eloStr)
else:
bElo = 0
else:
sanMoves &= line
when isMainModule:
initDB()
#iterMultiPGN("file.pgn")

93
src/game.nim Normal file
View File

@@ -0,0 +1,93 @@
import parseutils
import chess
import engine/engine
proc runGameHotseat*(): void =
## Initializes and runs a game of chess as hotseat.
var chess = initChess()
var draw: string
chess.echoBoard(chess.toMove)
while not chess.isCheckmate(chess.toMove) and not chess.isStalemate(chess.toMove):
echo "Make a move"
echo chess.toMove
var move = readLine(stdin)
while not chess.checkedMove(notationToMove(move, chess.toMove)):
move = readLine(stdin)
chess.echoBoard(chess.toMove)
if chess.isDrawClaimable():
echo "Do you want to claim a draw? (y/N)"
draw = readLine(stdin)
if draw == "y":
echo "Draw claimed"
break
if chess.isCheckmate(chess.toMove):
echo $chess.toMove & " was checkmated"
if chess.isStalemate(chess.toMove):
echo "Stalemate"
proc runGameSolo*(color: Color, difficulty: int): void =
## Initializes and runs a solo game of chess.
## The player plays as `color`.
echo("run game")
var chess = initChess()
var draw: string
while not chess.isCheckmate(chess.toMove) and not chess.isStalemate(chess.toMove):
if chess.toMove == color:
chess.echoBoard(color)
echo "Make a move"
var hMove = readLine(stdin)
while not chess.checkedMove(notationToMove(hMove, chess.toMove)):
hMove = readLine(stdin)
chess.echoBoard(color)
if chess.isDrawClaimable():
echo "Do you want to claim a draw? (y/N)"
draw = readLine(stdin)
if draw == "y":
echo "Draw claimed"
break
else:
var cMove = bestMove(chess, difficulty)
chess.checkedMove(cMove)
if chess.isCheckmate(chess.toMove):
echo $chess.toMove & " was checkmated"
if chess.isStalemate(chess.toMove):
echo "Stalemate"
proc menu(): void =
## Presents choices on what to play.
echo("\nWelcome to YchESs!\n\n\n")
var input: string
var playerCount: int
while true:
echo("How many players? (1/2)")
input = readLine(stdin)
discard parseInt(input, playerCount, 0)
if playerCount == 1 or playerCount == 2:
break
if playerCount == 1:
var color: string
var difficulty: int
while true:
echo("Choose the difficulty for the engine (1-10)")
input = readLine(stdin)
discard parseInt(input, difficulty, 0)
if difficulty >= 1 and difficulty <= 10:
break
while true:
echo("Do you want to play Black or White? (B/W)")
color = readLine(stdin)
if color == "B":
echo("\n\n\n")
runGameSolo(Color.Black, difficulty)
break
elif color == "W":
echo("\n\n\n")
runGameSolo(Color.White, difficulty)
break
else:
echo("\n\n\n")
runGameHotseat()
when isMainModule:
menu()

15
src/project.nim Normal file
View File

@@ -0,0 +1,15 @@
import os
proc getProjRootDir*(): string =
## Returns the root directory for the ychess project.
var maxDepth = 4
var path = os.getCurrentDir()
var projectDir: string
while maxDepth > 0:
if dirExists(joinPath(path, "src")):
projectDir = path
break
else:
path = parentDir(path)
maxDepth -= 1
return projectDir

1227
test.nim

File diff suppressed because it is too large Load Diff

1140
tests/chessTest.nim Normal file

File diff suppressed because it is too large Load Diff

48
tests/engineTest.nim Normal file
View File

@@ -0,0 +1,48 @@
import einheit
include chess
include engine/engine
testSuite ChessTest of TestSuite:
var
chess: Chess
method setup() =
self.chess = initChess()
method testPieceEvalStalemate() =
self.chess = initChess("8/8/2k5/8/8/5K2/8/8 b - - 0 1")
var pieceEvaluation = self.chess.evaluate()
self.check(pieceEvaluation == 0)
method testBestMoveProm() =
self.chess = initChess("8/2k1P3/8/8/8/5K2/8/8 w - - 0 1")
var testBestMove = self.chess.bestMove(2)
self.check(testBestMove.start != 0)
self.check(indToField(testBestMove.start) == "e7")
self.check(indToField(testBestMove.dest) == "e8")
method testBestMoveStopProm() =
self.chess = initChess("8/2k1P3/8/8/8/5K2/8/8 b - - 0 1")
var testBestMove = self.chess.bestMove(2)
self.check(testBestMove.start != 0)
self.check(indToField(testBestMove.start) == "c7")
self.check(indToField(testBestMove.dest) == "d7")
method testBestMoveTacticBlack() =
self.chess = initChess("8/2k3r1/8/6p1/5P2/8/4K1R1/8 b - - 0 1")
var testBestMove = self.chess.bestMove(2)
self.check(testBestMove.start != 0)
self.check(indToField(testBestMove.start) != "g5" or indToField(
testBestMove.dest) != "f4")
method testBestMoveTacticWhite() =
self.chess = initChess("8/2k3r1/8/5p2/6P1/8/4K1R1/8 w - - 0 1")
var testBestMove = self.chess.bestMove(2)
self.check(testBestMove.start != 0)
self.check(indToField(testBestMove.start) != "g4" or indToField(
testBestMove.dest) != "f5")
when isMainModule:
einheit.runTests()