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

Compare commits

..

36 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
14 changed files with 2488 additions and 2191 deletions

6
.gitignore vendored Normal file
View File

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

118
README.md
View File

@@ -1,30 +1,122 @@
# ychess
Attention: **The Chess engine is not finished yet.**
![ychess-logo](ychess.png)
![ychess-logo](./art/ychess.png)
ychess is a chess implementation and engine written in nim.
## Usage
## Usage and contribution
Simply download the code and run `nim c -r game.nim`.
You can now play a 1v1 hotseat game of chess in the commandline.
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.
## Testing
### Todos
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`.
[TODO.md](./TODO.md) contains a list of features that are planned.
## Documentation
### Command line
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 run `nim doc --project --index:on --outdir:htmldocs game.nim`
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

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()

File diff suppressed because it is too large Load Diff

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")

View File

@@ -1,29 +1,93 @@
from strutils import parseInt
import rdstdin
import parseutils
import ./chess
import chess
import engine/engine
proc runGame*(): void =
## Initializes and runs a game of chess.
var game = initGame()
proc runGameHotseat*(): void =
## Initializes and runs a game of chess as hotseat.
var chess = initChess()
var draw: string
game.echoBoard(game.toMove)
while not game.isCheckmate(game.toMove) and not game.isStalemate(game.toMove):
chess.echoBoard(chess.toMove)
while not chess.isCheckmate(chess.toMove) and not chess.isStalemate(chess.toMove):
echo "Make a move"
echo game.toMove
echo chess.toMove
var move = readLine(stdin)
while not game.checkedMove(notationToMove(move, game.toMove)):
while not chess.checkedMove(notationToMove(move, chess.toMove)):
move = readLine(stdin)
game.echoBoard(game.toMove)
if (game.isDrawClaimable):
chess.echoBoard(chess.toMove)
if chess.isDrawClaimable():
echo "Do you want to claim a draw? (y/N)"
draw = readLine(stdin)
if (draw == "y"):
if draw == "y":
echo "Draw claimed"
break
if game.isCheckmate(game.toMove):
echo $game.toMove & " was checkmated"
if game.isStalemate(game.toMove):
if chess.isCheckmate(chess.toMove):
echo $chess.toMove & " was checkmated"
if chess.isStalemate(chess.toMove):
echo "Stalemate"
runGame()
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

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()