egd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +32 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +70 -0
- data/LICENSE.txt +25 -0
- data/README.md +200 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/egd.gemspec +33 -0
- data/lib/egd/fen_builder.rb +23 -0
- data/lib/egd/fen_difference_discerner.rb +204 -0
- data/lib/egd/fen_to_board.rb +45 -0
- data/lib/egd/pgn_parser.rb +177 -0
- data/lib/egd/position_feature_discerner.rb +25 -0
- data/lib/egd/procedures.rb +53 -0
- data/lib/egd/version.rb +3 -0
- data/lib/egd.rb +101 -0
- metadata +163 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: '08654fa684c9a1a58f3683757a327c67abad2656'
|
|
4
|
+
data.tar.gz: 903d018099474b94ac8e93e92dbb7e402d7a4282
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '093c40611777d951fb712dc79b3f5d1a98a1822c832831dc045c69cd2ebd8103c332b36a89b716da31536e5c02becfde47a4cf54144d9fbb94e0065ef217a26e'
|
|
7
|
+
data.tar.gz: 4e6f7baab0cf159afadfe3376e5dc62249c32c2622a3cf200d00c89a151d1802be5e2661596876315ddb04dd60d59263966c01f29ae29454a65a75ddf6c80bf8
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
jobs:
|
|
3
|
+
build:
|
|
4
|
+
docker:
|
|
5
|
+
# ruby:2.4.3
|
|
6
|
+
- image: circleci/ruby@sha256:9dff2778b666bb81be98cc362e93d07c26b4491399ab6e9d152a716c754a5a47
|
|
7
|
+
|
|
8
|
+
working_directory: ~/egd
|
|
9
|
+
|
|
10
|
+
steps:
|
|
11
|
+
- checkout
|
|
12
|
+
|
|
13
|
+
# Download and cache dependencies
|
|
14
|
+
- restore_cache:
|
|
15
|
+
keys:
|
|
16
|
+
- v1-dependencies-{{ checksum "Gemfile.lock" }}
|
|
17
|
+
# fallback to using the latest cache if no exact match is found
|
|
18
|
+
- v1-dependencies-
|
|
19
|
+
|
|
20
|
+
- run:
|
|
21
|
+
name: install dependencies
|
|
22
|
+
command: |
|
|
23
|
+
bundle install --jobs=4 --retry=3 --path vendor/bundle
|
|
24
|
+
|
|
25
|
+
- save_cache:
|
|
26
|
+
paths:
|
|
27
|
+
- ./vendor/bundle
|
|
28
|
+
key: v1-dependencies-{{ checksum "Gemfile.lock" }}
|
|
29
|
+
|
|
30
|
+
- run:
|
|
31
|
+
command: |
|
|
32
|
+
bundle exec rspec
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
|
10
|
+
orientation.
|
|
11
|
+
|
|
12
|
+
## Our Standards
|
|
13
|
+
|
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
|
15
|
+
include:
|
|
16
|
+
|
|
17
|
+
* Using welcoming and inclusive language
|
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
|
19
|
+
* Gracefully accepting constructive criticism
|
|
20
|
+
* Focusing on what is best for the community
|
|
21
|
+
* Showing empathy towards other community members
|
|
22
|
+
|
|
23
|
+
Examples of unacceptable behavior by participants include:
|
|
24
|
+
|
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
26
|
+
advances
|
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
28
|
+
* Public or private harassment
|
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
|
30
|
+
address, without explicit permission
|
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
32
|
+
professional setting
|
|
33
|
+
|
|
34
|
+
## Our Responsibilities
|
|
35
|
+
|
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
|
38
|
+
response to any instances of unacceptable behavior.
|
|
39
|
+
|
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
|
44
|
+
threatening, offensive, or harmful.
|
|
45
|
+
|
|
46
|
+
## Scope
|
|
47
|
+
|
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
|
49
|
+
when an individual is representing the project or its community. Examples of
|
|
50
|
+
representing a project or community include using an official project e-mail
|
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
|
53
|
+
further defined and clarified by project maintainers.
|
|
54
|
+
|
|
55
|
+
## Enforcement
|
|
56
|
+
|
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
58
|
+
reported by contacting the project team at augusts.bautra@gmail.com. All
|
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
|
63
|
+
|
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
|
66
|
+
members of the project's leadership.
|
|
67
|
+
|
|
68
|
+
## Attribution
|
|
69
|
+
|
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
|
72
|
+
|
|
73
|
+
[homepage]: http://contributor-covenant.org
|
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
egd (1.0.0)
|
|
5
|
+
pgn (~> 0.2.0)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: https://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
coderay (1.1.2)
|
|
11
|
+
coveralls (0.7.2)
|
|
12
|
+
multi_json (~> 1.3)
|
|
13
|
+
rest-client (= 1.6.7)
|
|
14
|
+
simplecov (>= 0.7)
|
|
15
|
+
term-ansicolor (= 1.2.2)
|
|
16
|
+
thor (= 0.18.1)
|
|
17
|
+
diff-lcs (1.3)
|
|
18
|
+
docile (1.1.5)
|
|
19
|
+
json (2.1.0)
|
|
20
|
+
method_source (0.9.0)
|
|
21
|
+
mime-types (3.1)
|
|
22
|
+
mime-types-data (~> 3.2015)
|
|
23
|
+
mime-types-data (3.2016.0521)
|
|
24
|
+
multi_json (1.13.1)
|
|
25
|
+
pgn (0.2.0)
|
|
26
|
+
whittle
|
|
27
|
+
pry (0.11.3)
|
|
28
|
+
coderay (~> 1.1.0)
|
|
29
|
+
method_source (~> 0.9.0)
|
|
30
|
+
rake (10.5.0)
|
|
31
|
+
rest-client (1.6.7)
|
|
32
|
+
mime-types (>= 1.16)
|
|
33
|
+
rspec (3.7.0)
|
|
34
|
+
rspec-core (~> 3.7.0)
|
|
35
|
+
rspec-expectations (~> 3.7.0)
|
|
36
|
+
rspec-mocks (~> 3.7.0)
|
|
37
|
+
rspec-core (3.7.1)
|
|
38
|
+
rspec-support (~> 3.7.0)
|
|
39
|
+
rspec-expectations (3.7.0)
|
|
40
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
41
|
+
rspec-support (~> 3.7.0)
|
|
42
|
+
rspec-mocks (3.7.0)
|
|
43
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
44
|
+
rspec-support (~> 3.7.0)
|
|
45
|
+
rspec-support (3.7.0)
|
|
46
|
+
simplecov (0.15.1)
|
|
47
|
+
docile (~> 1.1.0)
|
|
48
|
+
json (>= 1.8, < 3)
|
|
49
|
+
simplecov-html (~> 0.10.0)
|
|
50
|
+
simplecov-html (0.10.2)
|
|
51
|
+
term-ansicolor (1.2.2)
|
|
52
|
+
tins (~> 0.8)
|
|
53
|
+
thor (0.18.1)
|
|
54
|
+
tins (0.13.2)
|
|
55
|
+
whittle (0.0.8)
|
|
56
|
+
|
|
57
|
+
PLATFORMS
|
|
58
|
+
ruby
|
|
59
|
+
|
|
60
|
+
DEPENDENCIES
|
|
61
|
+
bundler (~> 1.16)
|
|
62
|
+
coveralls (~> 0.7.2)
|
|
63
|
+
egd!
|
|
64
|
+
pry (~> 0.11.3)
|
|
65
|
+
rake (~> 10.0)
|
|
66
|
+
rspec (~> 3.7)
|
|
67
|
+
simplecov (~> 0.15.1)
|
|
68
|
+
|
|
69
|
+
BUNDLED WITH
|
|
70
|
+
1.16.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
The BSD license.
|
|
2
|
+
Copyright (c) 2017, Augusts Bautra
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
* Redistributions of source code must retain the above copyright
|
|
8
|
+
notice, this list of conditions and the following disclaimer.
|
|
9
|
+
* Redistributions in binary form must reproduce the above copyright
|
|
10
|
+
notice, this list of conditions and the following disclaimer in the
|
|
11
|
+
documentation and/or other materials provided with the distribution.
|
|
12
|
+
* Neither the name of the EGD project nor the
|
|
13
|
+
names of its contributors may be used to endorse or promote products
|
|
14
|
+
derived from this software without specific prior written permission.
|
|
15
|
+
|
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
17
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
18
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
|
20
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
21
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
22
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
23
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
24
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
25
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
[](https://badge.fury.io/rb/egd)
|
|
2
|
+
[](https://circleci.com/gh/Epigene/egd/tree/master)
|
|
3
|
+
[](https://coveralls.io/github/Epigene/egd?branch=master)
|
|
4
|
+
|
|
5
|
+
# Extended Game Description
|
|
6
|
+
This gem implements conversion functionality between standart PGN and EGD (Extended Game Description).
|
|
7
|
+
EGD is a new chess game description format that is based on FEN and Extended Position Description (EPD).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
gem install 'egd'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
require "egd"
|
|
19
|
+
Egd::Builder.new(File.read("path/to/chess.pgn")).to_json #=> JSON string
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What is this??
|
|
23
|
+
|
|
24
|
+
It's all about computer analysis of chess games.
|
|
25
|
+
If we want answers to insight questions like "do I do well when I trade queens?",
|
|
26
|
+
or "what losing positions do I repeatedly find myself in?", simple PGNs are not enough.
|
|
27
|
+
|
|
28
|
+
PGN is what is called "normalized" game data - it holds all the information needed to
|
|
29
|
+
gain insight, but it would take a lot of time even for a computer to go through thousands of games
|
|
30
|
+
to get the data needed to answer these questions.
|
|
31
|
+
|
|
32
|
+
What we need for expedience is a way to de-"normalize" the data in PGN, to expand the details of moves and positions.
|
|
33
|
+
|
|
34
|
+
This is what EGD does. It is a denormalized way to represent a game of chess.
|
|
35
|
+
|
|
36
|
+
Please note that currently EGD supports regular chess only and assumes the standart starting position.
|
|
37
|
+
|
|
38
|
+
Denormalize the PGNs of your games, store them in a PostgreSQL database and do powerful queries on the data,
|
|
39
|
+
like [Chess Sense](https://github.com/Epigene/chess_sense) does.
|
|
40
|
+
|
|
41
|
+
### Position equality
|
|
42
|
+
Currently EGD satisfies itself with using [Forsyth–Edwards Notation (FEN)](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation) __diagrams__ as representations of position, since they encode the three crucial pieces of information:
|
|
43
|
+
|
|
44
|
+
1. What pieces are on which squares,
|
|
45
|
+
2. Castling rights of both players,
|
|
46
|
+
2. En-passant capture square (irrespective of any legal possibility to execute such a capture).
|
|
47
|
+
|
|
48
|
+
However, a high-level competetive analysis may require the additional tracking of:
|
|
49
|
+
1. Threefold repetition counter,
|
|
50
|
+
3. Fifty-move rule counter,
|
|
51
|
+
4. 75-move rule counter.
|
|
52
|
+
|
|
53
|
+
## EGD details
|
|
54
|
+
EGD is inspired by [Extended Position Description (EPD)](https://chessprogramming.wikispaces.com/Extended+Position+Description)
|
|
55
|
+
but goes further than just tracking positions and adds meta-information to moves as well, specifically, it uses a combination of Long algebraic and Reversible algebraic [chess notations](https://en.wikipedia.org/wiki/Chess_notation) to specify, without ambiguity, what was moved to where and what capture or promotion took place.
|
|
56
|
+
|
|
57
|
+
The general form of EGD is a (JSON) hash of game headers and moves:
|
|
58
|
+
|
|
59
|
+
```rb
|
|
60
|
+
some_egd.to_h =>
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
# PGN game headers, in the future may contain additional meta-information
|
|
64
|
+
# about the game as a whole
|
|
65
|
+
"game_tags" => {
|
|
66
|
+
"Event"=>"Eurotel Trophy",
|
|
67
|
+
"Key" => "value"
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
# the moveset, each move is identified by order number and which side is moving
|
|
71
|
+
"moves" => {
|
|
72
|
+
# 1st move, by white
|
|
73
|
+
"1w"=>{
|
|
74
|
+
# what was the starting position
|
|
75
|
+
"start_position"=>{
|
|
76
|
+
# FEN diagram of the starting position
|
|
77
|
+
"fen"=>"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
|
78
|
+
# details about the position, see "Position features" chapter
|
|
79
|
+
"features"=>{}
|
|
80
|
+
},
|
|
81
|
+
# details about the move
|
|
82
|
+
"move"=>{
|
|
83
|
+
"player"=>"w", "san"=>"e4", "lran"=>"e2-e4", "from_square"=>"e2",
|
|
84
|
+
"to_square"=>"e4", "piece"=>"p", "move_type"=>"move"
|
|
85
|
+
},
|
|
86
|
+
# what was the resulting position
|
|
87
|
+
"end_position"=>{
|
|
88
|
+
# FEN diagram of the resulting position
|
|
89
|
+
"fen"=>"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
|
|
90
|
+
"features"=>{}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A (very short) game that can be represented in algebraic notation as `1. e4 e5`
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
```rb
|
|
101
|
+
egd = Egd::Builder.new("1. e4 e5").to_json
|
|
102
|
+
|
|
103
|
+
egd.to_h #=>
|
|
104
|
+
# {
|
|
105
|
+
# "game_tags" => {},
|
|
106
|
+
# "moves" => {
|
|
107
|
+
# "1w"=>{
|
|
108
|
+
# "start_position"=>{
|
|
109
|
+
# "fen"=>"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
|
110
|
+
# "features"=>{}
|
|
111
|
+
# },
|
|
112
|
+
# "move"=>{
|
|
113
|
+
# "player"=>"w", "san"=>"e4", "lran"=>"e2-e4", "from_square"=>"e2",
|
|
114
|
+
# "to_square"=>"e4", "piece"=>"p", "move_type"=>"move"
|
|
115
|
+
# },
|
|
116
|
+
# "end_position"=>{
|
|
117
|
+
# "fen"=>"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
|
|
118
|
+
# "features"=>{}
|
|
119
|
+
# }
|
|
120
|
+
# },
|
|
121
|
+
# "1b"=>{
|
|
122
|
+
# "start_position"=>{
|
|
123
|
+
# "fen"=>"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1",
|
|
124
|
+
# "features"=>{}
|
|
125
|
+
# },
|
|
126
|
+
# "move"=>{
|
|
127
|
+
# "player"=>"b", "san"=>"e5", "lran"=>"e7-e5",
|
|
128
|
+
# "from_square"=>"e7", "to_square"=>"e5", "piece"=>"p",
|
|
129
|
+
# "move_type"=>"move"
|
|
130
|
+
# },
|
|
131
|
+
# "end_position"=>{
|
|
132
|
+
# "fen"=>"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2",
|
|
133
|
+
# "features"=>{}
|
|
134
|
+
# }
|
|
135
|
+
# }
|
|
136
|
+
# }
|
|
137
|
+
# }
|
|
138
|
+
|
|
139
|
+
egd.to_json
|
|
140
|
+
#=> "{\"game_tags\":{},\"moves\":{\"1w\":{\"start_position\": ... \"features\":{}}}}}"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Take a look at `spec/egd_spec.rb` `"when initialized with the 02 PGN, a real immortal game"`
|
|
144
|
+
test for the structure you get when parsing PGNs you might get from chess.com.
|
|
145
|
+
|
|
146
|
+
### Move specifics
|
|
147
|
+
EGD tries to provide the maximum of meta-information about a move a programmed system can.
|
|
148
|
+
|
|
149
|
+
Currently outputted keys are:
|
|
150
|
+
```rb
|
|
151
|
+
"move" => {
|
|
152
|
+
"san" => "exd6", # the Short Algebraic Notation from provided PGN
|
|
153
|
+
"lran" => "e5xd6", # EGDs semi-custom Long Reversible Algebraic Notation
|
|
154
|
+
"from_square" => "e5",
|
|
155
|
+
"to_square" => "d6",
|
|
156
|
+
"piece" => "p", # moved piece. Piece codes are [p, R, N, Bl, Bd, Q, K] Bl is for (L)ight square Bishop and Bd is for (D)ark square bishop.
|
|
157
|
+
"move_type" => "ep_capture", # currently distinguished types are: [move, capture, ep_capture, promotion_capture, short_castle, long_castle, promotion]
|
|
158
|
+
"captured_piece" => "p", # only shows up if a capture occured
|
|
159
|
+
"promotion" => "Q", # only shows up if promotion occured
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Please note that, unlike PGN representation, EGD does not treat check(+) and checkmate(#)
|
|
164
|
+
as part of a move, instead, they are treated as part of a position.
|
|
165
|
+
|
|
166
|
+
### Position specifics
|
|
167
|
+
Currently positions are very minimalist - a FEN string and a "features" hash that currently can only have two keys - "check" and "checkmate".
|
|
168
|
+
|
|
169
|
+
Please note that since the treatment of checkmate event is not uniform across online PGN generators (Lichess represents checkmate as "+", whereas chess.com as "#"), a checkmate event denoted by "#" also adds the "check" => true feature tag.
|
|
170
|
+
|
|
171
|
+
Example position hash:
|
|
172
|
+
|
|
173
|
+
```rb
|
|
174
|
+
"end_position"=>{
|
|
175
|
+
"fen"=>"rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2",
|
|
176
|
+
"features"=>{
|
|
177
|
+
"check" => true,
|
|
178
|
+
"checkmate" => true
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Future Ideas
|
|
184
|
+
1. Expose the move's :meta key to [annotations](https://en.wikipedia.org/wiki/Chess_annotation_symbols) like "!!", "+/-", and "$1"
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
1. Fork and clone the repo
|
|
189
|
+
2. install ruby and bundler, bundle
|
|
190
|
+
3. run `rspec` to see if tests pass
|
|
191
|
+
4. work on feature, test it, make a PR
|
|
192
|
+
|
|
193
|
+
## Contributing
|
|
194
|
+
|
|
195
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Epigene/egd. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
The gem uses the [BSD-3 license](https://opensource.org/licenses/BSD-3-Clause),
|
|
200
|
+
you may use the gem in your own work, provided you reproduce the LICENSE.txt in it.
|
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "egd"
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require "irb"
|
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/egd.gemspec
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require "egd/version"
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "egd"
|
|
8
|
+
spec.version = Egd::VERSION
|
|
9
|
+
spec.required_ruby_version = '>= 2.4.2'
|
|
10
|
+
spec.authors = ["Epigene"]
|
|
11
|
+
spec.email = ["augusts.bautra@gmail.com"]
|
|
12
|
+
|
|
13
|
+
spec.summary = %q|Extended Game Description|
|
|
14
|
+
spec.description = %q|Convert chess PGNs into Extended Game Description JSON|
|
|
15
|
+
spec.homepage = "https://github.com/Epigene/egd"
|
|
16
|
+
spec.license = "BSD"
|
|
17
|
+
|
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
|
20
|
+
end
|
|
21
|
+
spec.bindir = "exe"
|
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
23
|
+
spec.require_paths = ["lib"]
|
|
24
|
+
|
|
25
|
+
spec.add_dependency "pgn", "~> 0.2.0" # for FEN handling
|
|
26
|
+
|
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.7"
|
|
30
|
+
spec.add_development_dependency "pry", "~> 0.11.3"
|
|
31
|
+
spec.add_development_dependency "simplecov", "~> 0.15.1"
|
|
32
|
+
spec.add_development_dependency "coveralls", "~> 0.7.2"
|
|
33
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
class Egd::FenBuilder
|
|
3
|
+
# This service takes in a FEN string and a chess move in algebraic notation.
|
|
4
|
+
# Outputs the FEN of the resulting position
|
|
5
|
+
|
|
6
|
+
attr_reader :start_fen, :move
|
|
7
|
+
|
|
8
|
+
NULL_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1".freeze
|
|
9
|
+
|
|
10
|
+
# Egd::FenBuilder.new(start_fen: nil, move:).call
|
|
11
|
+
def initialize(start_fen: nil, move: nil)
|
|
12
|
+
@start_fen = start_fen || NULL_FEN
|
|
13
|
+
@move = move.to_s.gsub(%r'\A\d+\.\s*\.*\s*', "")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
@fen ||= (
|
|
18
|
+
move != "" ?
|
|
19
|
+
PGN::FEN.new(start_fen).to_position.move(move).to_fen.to_s :
|
|
20
|
+
@start_fen
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
class Egd::FenDifferenceDiscerner
|
|
2
|
+
# This service takes in a start and an end FEN string,
|
|
3
|
+
# and the move in SAN,
|
|
4
|
+
# and a tells you what kind of move occured, in more detail than SAN
|
|
5
|
+
|
|
6
|
+
# Theoretically a start and end fen would suffice, but having move in SAN,
|
|
7
|
+
# which we do, allows skipping some hard procesing parts.
|
|
8
|
+
|
|
9
|
+
attr_reader :start_fen, :move, :end_fen
|
|
10
|
+
|
|
11
|
+
# Egd::FenDifferenceDiscerner.new(start_fen:, end_fen:).call
|
|
12
|
+
def initialize(start_fen: nil, move:, end_fen:)
|
|
13
|
+
@start_fen = start_fen || Egd::FenBuilder::NULL_FEN
|
|
14
|
+
@move = move
|
|
15
|
+
@end_fen = end_fen
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
# quickreturn with possible special cases
|
|
20
|
+
case move
|
|
21
|
+
when "O-O"
|
|
22
|
+
return special_case_short_castle(Egd::Procedures.parse_fen(start_fen)[:to_move])
|
|
23
|
+
when "O-O-O"
|
|
24
|
+
return special_case_long_castle(Egd::Procedures.parse_fen(start_fen)[:to_move])
|
|
25
|
+
when %r'\A[a-h]x[a-h]\d' # pawn x pawn, possible en-passant
|
|
26
|
+
return special_case_ep_capture if special_case_ep_capture
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# entering long processing of regular moves
|
|
30
|
+
|
|
31
|
+
changes = {
|
|
32
|
+
"lran" => lran, # FEN
|
|
33
|
+
"from_square" => from_square, # FEN # b2
|
|
34
|
+
"to_square" => to_square, # move b3
|
|
35
|
+
"piece" => piece, # move p
|
|
36
|
+
"move_type" => move_type,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
changes.merge!("captured_piece" => captured_piece) if captured_piece
|
|
40
|
+
changes.merge!("promotion" => promotion) if promotion
|
|
41
|
+
|
|
42
|
+
changes
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def special_case_ep_capture
|
|
48
|
+
return @ep_capture if defined?(@ep_capture)
|
|
49
|
+
|
|
50
|
+
return @ep_capture = false if Egd::Procedures.parse_fen(start_fen)[:ep_square] != to_square
|
|
51
|
+
|
|
52
|
+
from = move[0] + (move[-1] == "6" ? "5" : "4")
|
|
53
|
+
|
|
54
|
+
@ep_capture = {
|
|
55
|
+
"lran" => "#{from}x#{to_square}", # FEN
|
|
56
|
+
"from_square" => from, # FEN
|
|
57
|
+
"to_square" => to_square, # move
|
|
58
|
+
"piece" => "p", # move
|
|
59
|
+
"move_type" => "ep_capture", # FEN
|
|
60
|
+
"captured_piece" => "p", # move
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def special_case_short_castle(player)
|
|
65
|
+
{
|
|
66
|
+
"lran" => "O-O", # FEN
|
|
67
|
+
"from_square" => (player == "w" ? "e1" : "e8"), # FEN
|
|
68
|
+
"to_square" => (player == "w" ? "g1" : "g8"), # move
|
|
69
|
+
"piece" => "K", # move
|
|
70
|
+
"move_type" => "short_castle", # FEN
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def special_case_long_castle(player)
|
|
75
|
+
{
|
|
76
|
+
"lran" => "O-O-O", # FEN
|
|
77
|
+
"from_square" => (player == "w" ? "e1" : "e8"), # FEN
|
|
78
|
+
"to_square" => (player == "w" ? "c1" : "c8"), # move
|
|
79
|
+
"piece" => "K", # move
|
|
80
|
+
"move_type" => "long_castle", # FEN
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def board1
|
|
85
|
+
@board1 ||= Egd::FenToBoard.new(start_fen)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def board2
|
|
89
|
+
@board2 ||= Egd::FenToBoard.new(end_fen)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def changed_squares
|
|
93
|
+
return @changed_squares if defined?(@changed_squares)
|
|
94
|
+
|
|
95
|
+
@changed_squares = []
|
|
96
|
+
|
|
97
|
+
(1..64).to_a.each do |fen_index|
|
|
98
|
+
i = fen_index - 1
|
|
99
|
+
|
|
100
|
+
if board1.boardline[i] != board2.boardline[i]
|
|
101
|
+
square = Egd::Procedures.fen_index_to_square(fen_index)
|
|
102
|
+
|
|
103
|
+
@changed_squares << {
|
|
104
|
+
square: square, from: board1.boardline[i], to: board2.boardline[i]
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@changed_squares
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def from_square
|
|
113
|
+
@from_square ||= changed_squares.reject do |hash|
|
|
114
|
+
hash[:square] == to_square
|
|
115
|
+
end.detect do |hash|
|
|
116
|
+
hash[:to] == "-"
|
|
117
|
+
end[:square]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def to_square
|
|
121
|
+
@to_square ||= move.match(%r'\A(?<basemove>.*\d)(?<drek>.*)?\z')[:basemove][-2..-1]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def piece
|
|
125
|
+
return @piece if defined?(@piece)
|
|
126
|
+
|
|
127
|
+
possible_piece = move[0]
|
|
128
|
+
|
|
129
|
+
@piece = (Egd::SAN_CHESS_PIECES.include?(possible_piece) ? possible_piece : "p" )
|
|
130
|
+
|
|
131
|
+
@piece << bishop_color(to_square) if @piece == "B"
|
|
132
|
+
|
|
133
|
+
@piece
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def move_type
|
|
137
|
+
@move_type ||=
|
|
138
|
+
case move
|
|
139
|
+
when %r'x.*='i
|
|
140
|
+
"promotion_capture"
|
|
141
|
+
when %r'x'i
|
|
142
|
+
"capture"
|
|
143
|
+
when %r'='i
|
|
144
|
+
"promotion"
|
|
145
|
+
else
|
|
146
|
+
"move"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def captured_piece
|
|
151
|
+
@captured_piece ||=
|
|
152
|
+
if move_type[%r'capture']
|
|
153
|
+
captured_piece = changed_squares.detect do |hash|
|
|
154
|
+
hash[:square] == to_square
|
|
155
|
+
end[:from].upcase
|
|
156
|
+
|
|
157
|
+
captured_piece.downcase! if captured_piece[%r'p'i]
|
|
158
|
+
|
|
159
|
+
captured_piece << bishop_color(to_square) if captured_piece == "B"
|
|
160
|
+
|
|
161
|
+
captured_piece
|
|
162
|
+
else
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def promotion
|
|
168
|
+
@promotion ||=
|
|
169
|
+
if move_type[%r'promotion']
|
|
170
|
+
promoted_to = move.match(%r'=(?<promo>.)\z')[:promo]
|
|
171
|
+
|
|
172
|
+
promoted_to << bishop_color(to_square) if promoted_to == "B"
|
|
173
|
+
|
|
174
|
+
promoted_to
|
|
175
|
+
else
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def lran
|
|
181
|
+
return @lran if defined?(@lran)
|
|
182
|
+
|
|
183
|
+
@lran = "#{piece_in_lran(piece)}#{from_square}"
|
|
184
|
+
|
|
185
|
+
@lran << (
|
|
186
|
+
captured_piece ?
|
|
187
|
+
"x#{piece_in_lran(captured_piece)}#{to_square}" :
|
|
188
|
+
"-#{to_square}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@lran << "=#{promotion[0]}" if promotion
|
|
192
|
+
|
|
193
|
+
@lran
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def piece_in_lran(pc)
|
|
197
|
+
pc == "p" ? "" : pc[0]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def bishop_color(on_square)
|
|
201
|
+
Egd::Procedures.square_color(on_square) == "w" ? "l" : "d"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
class Egd::FenToBoard
|
|
2
|
+
# this service parses a FEN string into a hash-representation of a chess board and pieces
|
|
3
|
+
# So you can do
|
|
4
|
+
# board = Egd::FenToBoard.new(fen_string)
|
|
5
|
+
# board["b3"] #=> "P" # as in white pawn
|
|
6
|
+
|
|
7
|
+
attr_reader :fen
|
|
8
|
+
|
|
9
|
+
LETTER_VALUES = %w|_ a b c d e f g h|.freeze
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Egd::FenToBoard.new(fen_string)["b2"]
|
|
13
|
+
def initialize(fen)
|
|
14
|
+
@fen = fen
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def [](square)
|
|
18
|
+
board_hash[square]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def boardline
|
|
22
|
+
# this replaces numbers with corresponding amount of dashes
|
|
23
|
+
@boardline ||= parsed_fen[:board].gsub(%r'\d') do |match|
|
|
24
|
+
"-" * match.to_i
|
|
25
|
+
end.gsub("/", "")
|
|
26
|
+
end #=> "rnbqkbnrpppp...."
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
def parsed_fen
|
|
30
|
+
@parsed_fen ||= Egd::Procedures.parse_fen(fen)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def board_hash
|
|
34
|
+
return @board_hash if defined?(@board_hash)
|
|
35
|
+
|
|
36
|
+
look_up_square_behavior = ->(hash, key) {
|
|
37
|
+
hash[key] = boardline[
|
|
38
|
+
Egd::Procedures.square_to_fen_index(key) - 1
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@board_hash = Hash.new(&look_up_square_behavior)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Thanks to https://github.com/jedld/pgn_parser
|
|
2
|
+
|
|
3
|
+
class Egd::PgnParser
|
|
4
|
+
# This service takes in a PGN string and parses it
|
|
5
|
+
# Returns the game tags (headers of the PGN file) and
|
|
6
|
+
# the *actual* moves made in SAN
|
|
7
|
+
|
|
8
|
+
attr_reader :headers, :pgn_content
|
|
9
|
+
|
|
10
|
+
def initialize(pgn_content)
|
|
11
|
+
@pgn_content = pgn_content
|
|
12
|
+
@headers = []
|
|
13
|
+
@movelist = []
|
|
14
|
+
@game_attributes = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
current_index = 0
|
|
19
|
+
state = :initial
|
|
20
|
+
buffer = ''
|
|
21
|
+
|
|
22
|
+
while (current_index < @pgn_content.size)
|
|
23
|
+
current_char = @pgn_content[current_index]
|
|
24
|
+
current_index += 1
|
|
25
|
+
|
|
26
|
+
if state == :initial
|
|
27
|
+
if current_char == '['
|
|
28
|
+
state = :start_parse_header
|
|
29
|
+
next
|
|
30
|
+
elsif (current_char == ' ' || current_char == "\n" || current_char == "\r")
|
|
31
|
+
next
|
|
32
|
+
else
|
|
33
|
+
break
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if state == :start_parse_header
|
|
38
|
+
if current_char == ']'
|
|
39
|
+
state = :initial
|
|
40
|
+
hd = parse_header(buffer)
|
|
41
|
+
@headers << hd
|
|
42
|
+
@game_attributes[hd[:type]] = hd[:value]
|
|
43
|
+
buffer = ''
|
|
44
|
+
next
|
|
45
|
+
else
|
|
46
|
+
buffer << current_char
|
|
47
|
+
next
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@movelist = simple_parse_moves
|
|
53
|
+
|
|
54
|
+
hash = {moves: @movelist}
|
|
55
|
+
hash.merge!(game_tags: @game_attributes) if @game_attributes.any?
|
|
56
|
+
hash
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def parse_header(header)
|
|
62
|
+
event_type = ""
|
|
63
|
+
event_value = ""
|
|
64
|
+
state = :parse_type
|
|
65
|
+
current_index = 0
|
|
66
|
+
buffer = ''
|
|
67
|
+
|
|
68
|
+
while (current_index < header.size)
|
|
69
|
+
current_char = header[current_index]
|
|
70
|
+
current_index += 1
|
|
71
|
+
|
|
72
|
+
if state == :parse_type
|
|
73
|
+
if current_char == ' '
|
|
74
|
+
event_type = buffer.dup
|
|
75
|
+
buffer = ''
|
|
76
|
+
state = :start_parse_value
|
|
77
|
+
next
|
|
78
|
+
else
|
|
79
|
+
buffer << current_char
|
|
80
|
+
next
|
|
81
|
+
end
|
|
82
|
+
elsif state == :start_parse_value
|
|
83
|
+
if current_char == '"'
|
|
84
|
+
state = :parse_value
|
|
85
|
+
next
|
|
86
|
+
else
|
|
87
|
+
next
|
|
88
|
+
end
|
|
89
|
+
elsif state == :parse_value
|
|
90
|
+
if current_char=='"'
|
|
91
|
+
event_value = buffer.dup
|
|
92
|
+
buffer = ''
|
|
93
|
+
else
|
|
94
|
+
buffer << current_char
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
{type: event_type, value: event_value}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def simple_parse_moves
|
|
103
|
+
move_line =
|
|
104
|
+
pgn_content.split("\n").map do |line|
|
|
105
|
+
line unless line.strip[0] == "["
|
|
106
|
+
end.compact.join(" ").
|
|
107
|
+
gsub(%r'((1\-0)|(0\-1)|(1/2\-1/2)|(\*))\s*\z', "") # cut away game termination
|
|
108
|
+
|
|
109
|
+
# strip out comments and alternatives
|
|
110
|
+
while move_line.gsub!(%r'\{[^{}]*\}', ""); end
|
|
111
|
+
while move_line.gsub!(%r'\([^()]*\)', ""); end
|
|
112
|
+
|
|
113
|
+
# strip out "$n"-like annotations
|
|
114
|
+
move_line.gsub!(%r'\$\d+ ', " ")
|
|
115
|
+
|
|
116
|
+
# strip out ?! -like annotations
|
|
117
|
+
move_line.gsub!(%r'[?!]+ ', " ")
|
|
118
|
+
|
|
119
|
+
# strip out +/- like annotations
|
|
120
|
+
move_line.gsub!(%r'(./.)|(= )|(\+\−)|(\-\+)|(\∞)', "")
|
|
121
|
+
|
|
122
|
+
# squish whitespace
|
|
123
|
+
move_line = move_line.strip.gsub(%r'\s{2,}', " ")
|
|
124
|
+
|
|
125
|
+
# check if move line consists of legit chars only
|
|
126
|
+
if !move_line.match?(%r'\A(?:[[:alnum:]]|[=\-+.#\* ])+\z')
|
|
127
|
+
raise(
|
|
128
|
+
"The PGN move portion has weird characters even after cleaning it.\n"\
|
|
129
|
+
"Is the PGN valid?\n"\
|
|
130
|
+
"The moves after cleaning came out as:\n#{move_line}"
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
moves = []
|
|
135
|
+
|
|
136
|
+
while move_line.match?(%r'\d+\.')
|
|
137
|
+
parsed_moves = move_line.match(%r'\A
|
|
138
|
+
(?<move_number>\d+)\.(?<move_chunk>.*?)(?:(?<remainder>\d+\..*\z)|\z)
|
|
139
|
+
'x)
|
|
140
|
+
|
|
141
|
+
move_number = parsed_moves[:move_number]
|
|
142
|
+
move_chunk = parsed_moves[:move_chunk] #=> " e4 c5 "
|
|
143
|
+
move_line = parsed_moves[:remainder].to_s.strip
|
|
144
|
+
|
|
145
|
+
# a good place to DEBUG
|
|
146
|
+
|
|
147
|
+
next if !move_chunk
|
|
148
|
+
|
|
149
|
+
move_chunk = move_chunk.to_s.gsub(%r'\.{2}\s?', ".. ") # formats Black move ".."
|
|
150
|
+
move_line = move_line.to_s.strip
|
|
151
|
+
|
|
152
|
+
number_var = "@_#{move_number}"
|
|
153
|
+
|
|
154
|
+
w = move_chunk.strip.split(" ")[0]
|
|
155
|
+
b = move_chunk.strip.split(" ")[1]
|
|
156
|
+
|
|
157
|
+
options = {}
|
|
158
|
+
options.merge!(:w=>w) unless w.match?(%r'\.{2}')
|
|
159
|
+
options.merge!(:b=>b) if b
|
|
160
|
+
|
|
161
|
+
instance_variable_set(
|
|
162
|
+
"@_#{move_number}",
|
|
163
|
+
instance_variable_get("@_#{move_number}") ?
|
|
164
|
+
instance_variable_get("@_#{move_number}").merge(options) :
|
|
165
|
+
{:num=>move_number.to_i}.merge(options)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
moves << instance_variable_get("@_#{move_number}") if instance_variable_get("@_#{move_number}")[:b]
|
|
169
|
+
@last = instance_variable_get("@_#{move_number}")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# offload last to moves since there may not have been a black move
|
|
173
|
+
moves << @last if !@last[:b]
|
|
174
|
+
|
|
175
|
+
moves
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class Egd::PositionFeatureDiscerner
|
|
2
|
+
# This service takes in a move and the resulting FEN string
|
|
3
|
+
# and outputs a hash of features of the resulting position
|
|
4
|
+
|
|
5
|
+
# Currently minimal function,
|
|
6
|
+
# Only looks at supplied move and tells whether the position is a check or checkmate.
|
|
7
|
+
|
|
8
|
+
attr_reader :move, :end_fen
|
|
9
|
+
|
|
10
|
+
def initialize(move:, end_fen:)
|
|
11
|
+
@move = move
|
|
12
|
+
@end_fen = end_fen
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
return @features if defined?(@features)
|
|
17
|
+
|
|
18
|
+
@features = {}
|
|
19
|
+
|
|
20
|
+
@features.merge!("check" => true, "checkmate" => true) if move[%r'#\z']
|
|
21
|
+
@features.merge!("check" => true) if move[%r'\+\z']
|
|
22
|
+
|
|
23
|
+
@features
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Egd::Procedures
|
|
2
|
+
# This module has global methods for ease of working with chess data
|
|
3
|
+
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
# Egd::Procedures.parse_fen(fen)
|
|
7
|
+
def parse_fen(fen)
|
|
8
|
+
match = fen.split(%r'\s+') # FEN lines are delimited with whitespace, splitting on that
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
board: match[0],
|
|
12
|
+
to_move: match[1],
|
|
13
|
+
castling: match[2],
|
|
14
|
+
ep_square: match[3],
|
|
15
|
+
halfmove: match[4],
|
|
16
|
+
fullmove: match[5]
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Egd::Procedures.square_to_fen_index("b2")
|
|
21
|
+
def square_to_fen_index(square)
|
|
22
|
+
column = square[0]
|
|
23
|
+
row = square[1]
|
|
24
|
+
|
|
25
|
+
row_value = Egd::COLUMN_HEIGHT - row.to_i
|
|
26
|
+
row_value * Egd::ROW_LENGTH + Egd::FenToBoard::LETTER_VALUES.index(column)
|
|
27
|
+
end # a8 -> 1, a7 -> 9, h1 -> 64
|
|
28
|
+
|
|
29
|
+
# Egd::Procedures.fen_index_to_square(index)
|
|
30
|
+
def fen_index_to_square(index)
|
|
31
|
+
# 3 -> c8
|
|
32
|
+
row = Egd::COLUMN_HEIGHT - ((index - 1) / Egd::COLUMN_HEIGHT) # => 8
|
|
33
|
+
|
|
34
|
+
column_index = index - ((Egd::COLUMN_HEIGHT - row) * Egd::ROW_LENGTH)
|
|
35
|
+
|
|
36
|
+
column = Egd::FenToBoard::LETTER_VALUES[column_index] # => "c"
|
|
37
|
+
|
|
38
|
+
"#{column}#{row}"
|
|
39
|
+
end # 1 -> "a8", 64 -> "h1"
|
|
40
|
+
|
|
41
|
+
# Egd::Procedures.square_color("a7")
|
|
42
|
+
def square_color(square)
|
|
43
|
+
column = square[0] #=> a
|
|
44
|
+
row = square[1] #=> 8
|
|
45
|
+
|
|
46
|
+
if %w|a c e g|.include?(column)
|
|
47
|
+
row.to_i.even? ? "w" : "b"
|
|
48
|
+
else
|
|
49
|
+
row.to_i.odd? ? "w" : "b"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
data/lib/egd/version.rb
ADDED
data/lib/egd.rb
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "pgn"
|
|
3
|
+
|
|
4
|
+
require "egd/procedures"
|
|
5
|
+
require "egd/fen_builder"
|
|
6
|
+
require "egd/fen_to_board"
|
|
7
|
+
require "egd/fen_difference_discerner"
|
|
8
|
+
require "egd/position_feature_discerner"
|
|
9
|
+
require "egd/pgn_parser"
|
|
10
|
+
require "egd/version"
|
|
11
|
+
|
|
12
|
+
module Egd
|
|
13
|
+
ROW_LENGTH = 8.freeze
|
|
14
|
+
COLUMN_HEIGHT = 8.freeze
|
|
15
|
+
SAN_CHESS_PIECES = %w|R N B Q K|.freeze
|
|
16
|
+
|
|
17
|
+
def self.root
|
|
18
|
+
Pathname.new(File.expand_path('../..', __FILE__))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Builder
|
|
22
|
+
# This is the real deal
|
|
23
|
+
# Takes in a PGN string and returns a Ruby or JSON hash representation of the game in EGD
|
|
24
|
+
|
|
25
|
+
attr_reader :pgn
|
|
26
|
+
|
|
27
|
+
def initialize(pgn)
|
|
28
|
+
@pgn = pgn
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
return @to_h if defined?(@to_h)
|
|
33
|
+
|
|
34
|
+
@to_h = {}
|
|
35
|
+
@to_h["game_tags"] = game_tags
|
|
36
|
+
@to_h["moves"] = {}
|
|
37
|
+
|
|
38
|
+
@previous_fen = Egd::FenBuilder::NULL_FEN
|
|
39
|
+
|
|
40
|
+
moves.each_with_object(@to_h) do |move, mem|
|
|
41
|
+
transition_key = "#{move[%r'\A\d+']}#{move.match?(%r'\.\.') ? "b" : "w"}" #=> "1w"
|
|
42
|
+
|
|
43
|
+
san = move.match(%r'\A(?:\d+\.(?:\s*\.\.)?\s+)(?<san>\S+)\z')[:san] #=> "e4"
|
|
44
|
+
end_fen = Egd::FenBuilder.new(start_fen: @previous_fen, move: move).call
|
|
45
|
+
|
|
46
|
+
current_transition = {
|
|
47
|
+
"start_position" => {
|
|
48
|
+
"fen" => @previous_fen,
|
|
49
|
+
"features" => {}, # TODO, no features can be discerned before the move yet
|
|
50
|
+
},
|
|
51
|
+
"move" => {
|
|
52
|
+
"player" => transition_key[%r'\D\z'], #=> "w"
|
|
53
|
+
"san" => san,
|
|
54
|
+
}.merge(
|
|
55
|
+
Egd::FenDifferenceDiscerner.new(
|
|
56
|
+
start_fen: @previous_fen, move: san, end_fen: end_fen
|
|
57
|
+
).call
|
|
58
|
+
),
|
|
59
|
+
"end_position" => {
|
|
60
|
+
"fen" => end_fen,
|
|
61
|
+
"features" => Egd::PositionFeatureDiscerner.new(
|
|
62
|
+
move: move, end_fen: end_fen
|
|
63
|
+
).call
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# leave this breadcrumb for next run through loop
|
|
68
|
+
@previous_fen = current_transition.dig("end_position", "fen")
|
|
69
|
+
|
|
70
|
+
mem["moves"][transition_key] = current_transition
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_json
|
|
75
|
+
@to_json ||= to_h.to_json
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
def game_tags
|
|
80
|
+
@game_tags ||= parsed_pgn[:game_tags] || {}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def moves
|
|
84
|
+
return @moves if defined?(@moves)
|
|
85
|
+
|
|
86
|
+
@moves = []
|
|
87
|
+
|
|
88
|
+
parsed_pgn[:moves].each do |move_row|
|
|
89
|
+
@moves << "#{move_row[:num]}. #{move_row[:w]}"
|
|
90
|
+
@moves << "#{move_row[:num]}. .. #{move_row[:b]}" if move_row[:b]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
@moves
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parsed_pgn
|
|
97
|
+
@parsed_pgn ||= Egd::PgnParser.new(pgn).call
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
end
|
|
101
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: egd
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Epigene
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-01-27 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: pgn
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.2.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.2.0
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: bundler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.16'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.16'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '10.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '10.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.7'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.7'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: pry
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 0.11.3
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 0.11.3
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: simplecov
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 0.15.1
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 0.15.1
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: coveralls
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 0.7.2
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: 0.7.2
|
|
111
|
+
description: Convert chess PGNs into Extended Game Description JSON
|
|
112
|
+
email:
|
|
113
|
+
- augusts.bautra@gmail.com
|
|
114
|
+
executables: []
|
|
115
|
+
extensions: []
|
|
116
|
+
extra_rdoc_files: []
|
|
117
|
+
files:
|
|
118
|
+
- ".circleci/config.yml"
|
|
119
|
+
- ".gitignore"
|
|
120
|
+
- ".rspec"
|
|
121
|
+
- ".travis.yml"
|
|
122
|
+
- CODE_OF_CONDUCT.md
|
|
123
|
+
- Gemfile
|
|
124
|
+
- Gemfile.lock
|
|
125
|
+
- LICENSE.txt
|
|
126
|
+
- README.md
|
|
127
|
+
- Rakefile
|
|
128
|
+
- bin/console
|
|
129
|
+
- bin/setup
|
|
130
|
+
- egd.gemspec
|
|
131
|
+
- lib/egd.rb
|
|
132
|
+
- lib/egd/fen_builder.rb
|
|
133
|
+
- lib/egd/fen_difference_discerner.rb
|
|
134
|
+
- lib/egd/fen_to_board.rb
|
|
135
|
+
- lib/egd/pgn_parser.rb
|
|
136
|
+
- lib/egd/position_feature_discerner.rb
|
|
137
|
+
- lib/egd/procedures.rb
|
|
138
|
+
- lib/egd/version.rb
|
|
139
|
+
homepage: https://github.com/Epigene/egd
|
|
140
|
+
licenses:
|
|
141
|
+
- BSD
|
|
142
|
+
metadata: {}
|
|
143
|
+
post_install_message:
|
|
144
|
+
rdoc_options: []
|
|
145
|
+
require_paths:
|
|
146
|
+
- lib
|
|
147
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - ">="
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: 2.4.2
|
|
152
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
|
+
requirements:
|
|
154
|
+
- - ">="
|
|
155
|
+
- !ruby/object:Gem::Version
|
|
156
|
+
version: '0'
|
|
157
|
+
requirements: []
|
|
158
|
+
rubyforge_project:
|
|
159
|
+
rubygems_version: 2.6.13
|
|
160
|
+
signing_key:
|
|
161
|
+
specification_version: 4
|
|
162
|
+
summary: Extended Game Description
|
|
163
|
+
test_files: []
|