pgn3 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +123 -0
- data/Rakefile +1 -0
- data/TODO.md +12 -0
- data/examples/immortal_game.pgn +17 -0
- data/lib/pgn/board.rb +163 -0
- data/lib/pgn/fen.rb +148 -0
- data/lib/pgn/game.rb +148 -0
- data/lib/pgn/move.rb +163 -0
- data/lib/pgn/move_calculator.rb +337 -0
- data/lib/pgn/parser.rb +208 -0
- data/lib/pgn/position.rb +129 -0
- data/lib/pgn/version.rb +3 -0
- data/lib/pgn.rb +26 -0
- data/pgn3.gemspec +27 -0
- data/spec/fen_spec.rb +100 -0
- data/spec/game_spec.rb +21 -0
- data/spec/parser_spec.rb +143 -0
- data/spec/pgn_files/alternate_castling.pgn +4 -0
- data/spec/pgn_files/annotations.pgn +9 -0
- data/spec/pgn_files/comments.pgn +8 -0
- data/spec/pgn_files/empty_variation_move.pgn +11 -0
- data/spec/pgn_files/fen.pgn +5 -0
- data/spec/pgn_files/multiline_comments.pgn +5 -0
- data/spec/pgn_files/nested_comments.pgn +4 -0
- data/spec/pgn_files/no_moves.pgn +18 -0
- data/spec/pgn_files/test.pgn +55 -0
- data/spec/pgn_files/two_annotations.pgn +4 -0
- data/spec/pgn_files/two_games.pgn +9 -0
- data/spec/pgn_files/variations.pgn +4 -0
- data/spec/position_spec.rb +54 -0
- data/spec/spec_helper.rb +19 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 20fb2f8fb76ff182d4a77f001b3bf55b8d6ba0abc452094bdfe896c754b62501
|
4
|
+
data.tar.gz: a898ca24052f81028014c24e49dd2b84d91e12096ab1d4783e71432d8b537d90
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 84dbdb900720a5766209979bacf401279873028d35e89ec83c3e0457e0c6ec9325362dccb5cf6e7a7e923a1d1314565627fa4d1cc102b0fed400275973059a67
|
7
|
+
data.tar.gz: 5d74af5952a5ad2b4015a666fc24c4cb88e065c7320fc56e576e299dd33eb891ae2501c7b51061cb50f71269f831a17fa2dcaaf48ca8591efd48cee78ae62883
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Stacey Touset
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# PGN2
|
2
|
+
|
3
|
+
This is a fork from [pgn](https://github.com/capicue/pgn) gem.
|
4
|
+
A PGN parser and FEN generator for ruby.
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
### Creating games from pgn files
|
9
|
+
|
10
|
+
On the command line, it is easy to read in and play through chess games
|
11
|
+
in [portable game notation](http://en.wikipedia.org/wiki/Portable_Game_Notation) format.
|
12
|
+
|
13
|
+
```
|
14
|
+
> games = PGN.parse(File.read("./examples/immortal_game.pgn"))
|
15
|
+
> game = games.first
|
16
|
+
> game.play
|
17
|
+
```
|
18
|
+
|
19
|
+
Play through the game using `a` or left arrow to move backward, and `d`
|
20
|
+
or right arrow to move forward. `q` or `^C` quits play mode.
|
21
|
+
|
22
|
+
♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
|
23
|
+
♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
|
24
|
+
_ _ _ _ _ _ _ _
|
25
|
+
_ _ _ _ _ _ _ _
|
26
|
+
_ _ _ _ _ _ _ _
|
27
|
+
_ _ _ _ _ _ _ _
|
28
|
+
♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
|
29
|
+
♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
|
30
|
+
|
31
|
+
♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
|
32
|
+
♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
|
33
|
+
_ _ _ _ _ _ _ _
|
34
|
+
_ _ _ _ _ _ _ _
|
35
|
+
_ _ _ _ _ _ _ _
|
36
|
+
_ _ _ _ ♙ _ _ _
|
37
|
+
_ _ _ _ _ _ _ _
|
38
|
+
♙ ♙ ♙ ♙ _ ♙ ♙ ♙
|
39
|
+
♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
|
40
|
+
|
41
|
+
...
|
42
|
+
|
43
|
+
You can also access all of the information about a game.
|
44
|
+
|
45
|
+
```
|
46
|
+
> game.positions.last
|
47
|
+
=>
|
48
|
+
♜ _ ♝ ♚ _ _ _ ♜
|
49
|
+
♟ _ _ ♟ ♗ ♟ ♘ ♟
|
50
|
+
♞ _ _ _ _ ♞ _ _
|
51
|
+
_ ♟ _ ♘ ♙ _ _ ♙
|
52
|
+
_ _ _ _ _ _ ♙ _
|
53
|
+
_ _ _ ♙ _ _ _ _
|
54
|
+
♙ _ ♙ _ ♔ _ _ _
|
55
|
+
♛ _ _ _ _ _ ♝ _
|
56
|
+
|
57
|
+
> game.positions.last.to_fen
|
58
|
+
=> r1bk3r/p2pBpNp/n4n2/1p1NP2P/6P1/3P4/P1P1K3/q5b1 b - - 1 22
|
59
|
+
|
60
|
+
> game.result
|
61
|
+
=> "1-0"
|
62
|
+
|
63
|
+
> game.tags["White"]
|
64
|
+
=> "Adolf Anderssen"
|
65
|
+
```
|
66
|
+
|
67
|
+
It is possible to create a game without parsing a pgn file.
|
68
|
+
|
69
|
+
```
|
70
|
+
moves = %w{e4 c5 c3 d5 exd5 Qxd5 d4 Nf6}
|
71
|
+
game = PGN::Game.new(moves)
|
72
|
+
```
|
73
|
+
|
74
|
+
Note that if you simply want an abstract syntax tree from the pgn file,
|
75
|
+
you can use `PGN::Parser.parse`.
|
76
|
+
|
77
|
+
### Dealing with FEN strings
|
78
|
+
|
79
|
+
[Forsyth Edwards Notation](http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)
|
80
|
+
is a compact way to represent all of the information about a given chess
|
81
|
+
position. It is easy to convert between FEN strings and chess positions.
|
82
|
+
|
83
|
+
```
|
84
|
+
> fen = PGN::FEN.start
|
85
|
+
=> rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
|
86
|
+
|
87
|
+
> fen = PGN::FEN.new("r1bk3r/p2pBpNp/n4n2/1p1NP2P/6P1/3P4/P1P1K3/q5b1 b - - 1 22")
|
88
|
+
> position = fen.to_position
|
89
|
+
=>
|
90
|
+
♜ _ ♝ ♚ _ _ _ ♜
|
91
|
+
♟ _ _ ♟ ♗ ♟ ♘ ♟
|
92
|
+
♞ _ _ _ _ ♞ _ _
|
93
|
+
_ ♟ _ ♘ ♙ _ _ ♙
|
94
|
+
_ _ _ _ _ _ ♙ _
|
95
|
+
_ _ _ ♙ _ _ _ _
|
96
|
+
♙ _ ♙ _ ♔ _ _ _
|
97
|
+
♛ _ _ _ _ _ ♝ _
|
98
|
+
|
99
|
+
> position.to_fen
|
100
|
+
=> r1bk3r/p2pBpNp/n4n2/1p1NP2P/6P1/3P4/P1P1K3/q5b1 b - - 1 22
|
101
|
+
```
|
102
|
+
|
103
|
+
## Installation
|
104
|
+
|
105
|
+
Add this line to your application's Gemfile:
|
106
|
+
|
107
|
+
gem 'pgn2'
|
108
|
+
|
109
|
+
And then execute:
|
110
|
+
|
111
|
+
$ bundle
|
112
|
+
|
113
|
+
Or install it yourself as:
|
114
|
+
|
115
|
+
$ gem install pgn2
|
116
|
+
|
117
|
+
## Contributing
|
118
|
+
|
119
|
+
1. Fork it
|
120
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
121
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
122
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
123
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/TODO.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
[Event "London"]
|
2
|
+
[Site "London"]
|
3
|
+
[Date "1851.??.??"]
|
4
|
+
[EventDate "?"]
|
5
|
+
[Round "?"]
|
6
|
+
[Result "1-0"]
|
7
|
+
[White "Adolf Anderssen"]
|
8
|
+
[Black "Kieseritzky"]
|
9
|
+
[ECO "C33"]
|
10
|
+
[WhiteElo "?"]
|
11
|
+
[BlackElo "?"]
|
12
|
+
[PlyCount "45"]
|
13
|
+
|
14
|
+
1.e4 e5 2.f4 exf4 3.Bc4 Qh4+ 4.Kf1 b5 5.Bxb5 Nf6 6.Nf3 Qh6 7.d3 Nh5 8.Nh4 Qg5
|
15
|
+
9.Nf5 c6 10.g4 Nf6 11.Rg1 cxb5 12.h4 Qg6 13.h5 Qg5 14.Qf3 Ng8 15.Bxf4 Qf6
|
16
|
+
16.Nc3 Bc5 17.Nd5 Qxb2 18.Bd6 Bxg1 19. e5 Qxa1+ 20. Ke2 Na6 21.Nxg7+ Kd8
|
17
|
+
22.Qf6+ Nxf6 23.Be7# 1-0
|
data/lib/pgn/board.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
module PGN
|
2
|
+
# {PGN::Board} represents the squares of a chess board and the pieces on
|
3
|
+
# each square. It is responsible for translating between a human readable
|
4
|
+
# format (white queen's rook on the bottom left) and the obvious
|
5
|
+
# internal representation (white queen's rook is position [0,0]). It
|
6
|
+
# takes care of converting square names (e4) to actual locations, and
|
7
|
+
# can convert to unicode chess pieces for display purposes.
|
8
|
+
#
|
9
|
+
# @!attribute squares
|
10
|
+
# @return [Array<Array<String>>] the pieces on the board
|
11
|
+
#
|
12
|
+
|
13
|
+
class Board
|
14
|
+
# The starting, internal representation of a chess board
|
15
|
+
#
|
16
|
+
START = [
|
17
|
+
['R', 'P', nil, nil, nil, nil, 'p', 'r'],
|
18
|
+
['N', 'P', nil, nil, nil, nil, 'p', 'n'],
|
19
|
+
['B', 'P', nil, nil, nil, nil, 'p', 'b'],
|
20
|
+
['Q', 'P', nil, nil, nil, nil, 'p', 'q'],
|
21
|
+
['K', 'P', nil, nil, nil, nil, 'p', 'k'],
|
22
|
+
['B', 'P', nil, nil, nil, nil, 'p', 'b'],
|
23
|
+
['N', 'P', nil, nil, nil, nil, 'p', 'n'],
|
24
|
+
['R', 'P', nil, nil, nil, nil, 'p', 'r']
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
FILE_TO_INDEX = ('a'..'h').each_with_index.to_h
|
28
|
+
INDEX_TO_FILE = FILE_TO_INDEX.map(&:reverse).to_h
|
29
|
+
|
30
|
+
RANK_TO_INDEX = ('1'..'8').each_with_index.to_h
|
31
|
+
INDEX_TO_RANK = RANK_TO_INDEX.map(&:reverse).to_h
|
32
|
+
|
33
|
+
# algebraic to unicode piece lookup
|
34
|
+
#
|
35
|
+
UNICODE_PIECES = {
|
36
|
+
'k' => "\u{265A}",
|
37
|
+
'q' => "\u{265B}",
|
38
|
+
'r' => "\u{265C}",
|
39
|
+
'b' => "\u{265D}",
|
40
|
+
'n' => "\u{265E}",
|
41
|
+
'p' => "\u{265F}",
|
42
|
+
'K' => "\u{2654}",
|
43
|
+
'Q' => "\u{2655}",
|
44
|
+
'R' => "\u{2656}",
|
45
|
+
'B' => "\u{2657}",
|
46
|
+
'N' => "\u{2658}",
|
47
|
+
'P' => "\u{2659}",
|
48
|
+
nil => '_'
|
49
|
+
}.freeze
|
50
|
+
|
51
|
+
attr_accessor :squares
|
52
|
+
|
53
|
+
# @return [PGN::Board] a board in the starting position
|
54
|
+
#
|
55
|
+
def self.start
|
56
|
+
PGN::Board.new(START)
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param squares [<Array<Array<String>>>] the squares of the board
|
60
|
+
# @example
|
61
|
+
# PGN::Board.new(
|
62
|
+
# [
|
63
|
+
# ["R", "P", nil, nil, nil, nil, "p", "r"],
|
64
|
+
# ["N", "P", nil, nil, nil, nil, "p", "n"],
|
65
|
+
# ["B", "P", nil, nil, nil, nil, "p", "b"],
|
66
|
+
# ["Q", "P", nil, nil, nil, nil, "p", "q"],
|
67
|
+
# ["K", "P", nil, nil, nil, nil, "p", "k"],
|
68
|
+
# ["B", "P", nil, nil, nil, nil, "p", "b"],
|
69
|
+
# ["N", "P", nil, nil, nil, nil, "p", "n"],
|
70
|
+
# ["R", "P", nil, nil, nil, nil, "p", "r"],
|
71
|
+
# ]
|
72
|
+
# )
|
73
|
+
#
|
74
|
+
def initialize(squares)
|
75
|
+
self.squares = squares
|
76
|
+
end
|
77
|
+
|
78
|
+
# @overload at(str)
|
79
|
+
# Looks up a piece based on the string representation of a square (e4)
|
80
|
+
# @param str [String] the square in algebraic notation
|
81
|
+
# @overload at(file, rank)
|
82
|
+
# Looks up a piece based on zero-indexed coordinates (4, 3)
|
83
|
+
# @param file [Integer] the file the piece is on
|
84
|
+
# @param rank [Integer] the rank the piece is on
|
85
|
+
# @return [String, nil] the piece on the square, or nil if it is
|
86
|
+
# empty
|
87
|
+
# @example
|
88
|
+
# board.at(4,3) #=> "P"
|
89
|
+
# board.at("e4") #=> "P"
|
90
|
+
#
|
91
|
+
def at(*args)
|
92
|
+
case args.length
|
93
|
+
when 1
|
94
|
+
at(*coordinates_for(args.first))
|
95
|
+
when 2
|
96
|
+
squares[args[0]][args[1]]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# @param changes [Hash<String, <String, nil>>] changes to make to the board
|
101
|
+
# @return [self]
|
102
|
+
# @example
|
103
|
+
# board.change!({"e2" => nil, "e4" => "P"})
|
104
|
+
#
|
105
|
+
def change!(changes)
|
106
|
+
changes.each do |square, piece|
|
107
|
+
update(square, piece)
|
108
|
+
end
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
# @param square [String] the square in algebraic notation
|
113
|
+
# @param piece [String, nil] the piece to put on the square
|
114
|
+
# @return [self]
|
115
|
+
# @example
|
116
|
+
# board.update("e4", "P")
|
117
|
+
#
|
118
|
+
def update(square, piece)
|
119
|
+
coords = coordinates_for(square)
|
120
|
+
squares[coords[0]][coords[1]] = piece
|
121
|
+
self
|
122
|
+
end
|
123
|
+
|
124
|
+
# @param position [String] the square in algebraic notation
|
125
|
+
# @return [Array<Integer>] the coordinates of the square
|
126
|
+
# @example
|
127
|
+
# board.coordinates_for("e4") #=> [4, 3]
|
128
|
+
#
|
129
|
+
def coordinates_for(position)
|
130
|
+
file_chr, rank_chr = position.chars.to_a
|
131
|
+
file = FILE_TO_INDEX[file_chr]
|
132
|
+
rank = RANK_TO_INDEX[rank_chr]
|
133
|
+
[file, rank]
|
134
|
+
end
|
135
|
+
|
136
|
+
# @param coordinates [Array<Integer>] the coordinates of the square
|
137
|
+
# @return [String] the square in algebraic notation
|
138
|
+
# @example
|
139
|
+
# board.position_for([4, 3]) #=> "e4"
|
140
|
+
#
|
141
|
+
def position_for(coordinates)
|
142
|
+
file, rank = coordinates
|
143
|
+
file_chr = INDEX_TO_FILE[file]
|
144
|
+
rank_chr = INDEX_TO_RANK[rank]
|
145
|
+
[file_chr, rank_chr].join('')
|
146
|
+
end
|
147
|
+
|
148
|
+
# @return [String] the board in human readable format with unicode
|
149
|
+
# pieces
|
150
|
+
#
|
151
|
+
def inspect
|
152
|
+
squares.transpose.reverse.map do |row|
|
153
|
+
row.map { |chr| UNICODE_PIECES[chr] }.join(' ')
|
154
|
+
end.join("\n")
|
155
|
+
end
|
156
|
+
|
157
|
+
# @return [PGN::Board] a copy of self with duplicated squares
|
158
|
+
#
|
159
|
+
def dup
|
160
|
+
PGN::Board.new(squares.map(&:dup))
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/lib/pgn/fen.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
module PGN
|
2
|
+
# {PGN::FEN} is responsible for translating between strings in FEN
|
3
|
+
# notation and an internal representation of the board.
|
4
|
+
#
|
5
|
+
# @see http://en.wikipedia.org/wiki/Forsyth-Edwards_Notation
|
6
|
+
# Forsyth-Edwards notation
|
7
|
+
#
|
8
|
+
# @!attribute board
|
9
|
+
# @return [PGN::Board] a {PGN::Board} object for the current board
|
10
|
+
# state
|
11
|
+
#
|
12
|
+
# @!attribute active
|
13
|
+
# @return ['w', 't'] the current player
|
14
|
+
#
|
15
|
+
# @!attribute castling
|
16
|
+
# @return [String] the castling availability
|
17
|
+
# @example
|
18
|
+
# "Kq" # white can castle kingside and black queenside
|
19
|
+
# @example
|
20
|
+
# "-" # no one can castle
|
21
|
+
#
|
22
|
+
# @!attribute en_passant
|
23
|
+
# @return [String] the current en passant square
|
24
|
+
# @example
|
25
|
+
# "e3" # white just moved e2 -> e4
|
26
|
+
# "-" # no current en passant square
|
27
|
+
#
|
28
|
+
# @!attribute halfmove
|
29
|
+
# @return [String] the halfmove clock
|
30
|
+
# @note This is the number of halfmoves since the last pawn advance or capture
|
31
|
+
#
|
32
|
+
# @!attribute fullmove
|
33
|
+
# @return [String] the fullmove counter
|
34
|
+
# @note The number of full moves. This is incremented after black
|
35
|
+
# plays.
|
36
|
+
#
|
37
|
+
class FEN
|
38
|
+
# The FEN string representing the starting position in chess
|
39
|
+
#
|
40
|
+
INITIAL = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
41
|
+
|
42
|
+
attr_accessor :board, :active, :castling, :en_passant, :halfmove, :fullmove
|
43
|
+
|
44
|
+
# @return [PGN::FEN] a {PGN::FEN} object representing the starting
|
45
|
+
# position
|
46
|
+
#
|
47
|
+
def self.start
|
48
|
+
PGN::FEN.new(INITIAL)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [PGN::FEN] a {PGN::FEN} object with the given attributes
|
52
|
+
#
|
53
|
+
def self.from_attributes(attrs)
|
54
|
+
fen = PGN::FEN.new
|
55
|
+
attrs.each do |key, val|
|
56
|
+
fen.send("#{key}=", val)
|
57
|
+
end
|
58
|
+
fen
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param fen_string [String] a string in Forsyth-Edwards Notation
|
62
|
+
#
|
63
|
+
def initialize(fen_string = nil)
|
64
|
+
if fen_string
|
65
|
+
self.board_string,
|
66
|
+
self.active,
|
67
|
+
self.castling,
|
68
|
+
self.en_passant,
|
69
|
+
self.halfmove,
|
70
|
+
self.fullmove = fen_string.split
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def en_passant=(val)
|
75
|
+
@en_passant = val.nil? ? "-" : val
|
76
|
+
end
|
77
|
+
|
78
|
+
def castling=(val)
|
79
|
+
@castling = (val.nil? || val.empty?) ? "-" : val
|
80
|
+
end
|
81
|
+
|
82
|
+
# @param board_fen [String] the fen representation of the board
|
83
|
+
# @example
|
84
|
+
# fen.board_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
|
85
|
+
#
|
86
|
+
def board_string=(board_fen)
|
87
|
+
squares = board_fen.gsub(/\d/) {|match| "_" * match.to_i }
|
88
|
+
.split("/")
|
89
|
+
.map {|row| row.split('') }
|
90
|
+
.map {|row| row.map {|e| e == "_" ? nil : e } }
|
91
|
+
.reverse
|
92
|
+
.transpose
|
93
|
+
self.board = PGN::Board.new(squares)
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return [String] the fen representation of the board
|
97
|
+
# @example
|
98
|
+
# PGN::FEN.start.board_string #=> "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
|
99
|
+
#
|
100
|
+
def board_string
|
101
|
+
self.board
|
102
|
+
.squares
|
103
|
+
.transpose
|
104
|
+
.reverse
|
105
|
+
.map {|row| row.map {|e| e.nil? ? "_" : e } }
|
106
|
+
.map {|row| row.join }
|
107
|
+
.join("/")
|
108
|
+
.gsub(/_+/) {|match| match.length }
|
109
|
+
end
|
110
|
+
|
111
|
+
# @return [PGN::Position] a {PGN::Position} representing the current
|
112
|
+
# position
|
113
|
+
#
|
114
|
+
def to_position
|
115
|
+
player = self.active == 'w' ? :white : :black
|
116
|
+
castling = self.castling.split('') - ['-']
|
117
|
+
en_passant = self.en_passant == '-' ? nil : en_passant
|
118
|
+
|
119
|
+
PGN::Position.new(
|
120
|
+
self.board,
|
121
|
+
player,
|
122
|
+
castling,
|
123
|
+
en_passant,
|
124
|
+
self.halfmove.to_i,
|
125
|
+
self.fullmove.to_i,
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
# @return [String] the FEN string
|
130
|
+
# @example
|
131
|
+
# PGN::FEN.start.to_s #=> "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
132
|
+
#
|
133
|
+
def to_s
|
134
|
+
[
|
135
|
+
self.board_string,
|
136
|
+
self.active,
|
137
|
+
self.castling,
|
138
|
+
self.en_passant,
|
139
|
+
self.halfmove,
|
140
|
+
self.fullmove,
|
141
|
+
].join(" ")
|
142
|
+
end
|
143
|
+
|
144
|
+
def inspect
|
145
|
+
self.to_s
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/lib/pgn/game.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'io/console'
|
2
|
+
|
3
|
+
module PGN
|
4
|
+
class MoveText
|
5
|
+
attr_accessor :notation, :annotation, :comment, :variations
|
6
|
+
|
7
|
+
def initialize(notation, annotation = nil, comment = nil, variations = [])
|
8
|
+
@notation = notation
|
9
|
+
@annotation = annotation
|
10
|
+
@comment = clean_text(comment)
|
11
|
+
@variations = variations
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(m)
|
15
|
+
to_s == m.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def eql?(m)
|
19
|
+
self == m
|
20
|
+
end
|
21
|
+
|
22
|
+
def hash
|
23
|
+
@notation.hash
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
@notation
|
28
|
+
end
|
29
|
+
|
30
|
+
def clean_text(text)
|
31
|
+
text&.gsub(/{(.*)}/, '\1')&.gsub(/\s+/, ' ')&.strip
|
32
|
+
end
|
33
|
+
end
|
34
|
+
# {PGN::Game} holds all of the information about a game. It is either
|
35
|
+
# the result of parsing a PGN file, or created by hand.
|
36
|
+
#
|
37
|
+
# A {PGN::Game} has an interactive {#play} method, and can also return
|
38
|
+
# a list of positions in {PGN::Position} format or FEN.
|
39
|
+
#
|
40
|
+
# @!attribute tags
|
41
|
+
# @return [Hash<String, String>] metadata about the game
|
42
|
+
# @example
|
43
|
+
# game.tags #=> {"White" => "Kasparov", "Black" => "Deep Blue"}
|
44
|
+
#
|
45
|
+
# @!attribute moves
|
46
|
+
# @return [Array<String>] a list of the moves in standard algebraic
|
47
|
+
# notation
|
48
|
+
# @example
|
49
|
+
# game.moves #=> ["e4", "c5", "Nf3", "d6", "d4", "cxd4"]
|
50
|
+
#
|
51
|
+
# @!attribute result
|
52
|
+
# @return [String] the outcome of the game
|
53
|
+
# @example
|
54
|
+
# game.result #=> "1-0"
|
55
|
+
#
|
56
|
+
class Game
|
57
|
+
attr_accessor :tags, :result, :pgn, :comment
|
58
|
+
attr_reader :moves
|
59
|
+
|
60
|
+
LEFT = /(a|\x1B\[D)\z/.freeze
|
61
|
+
RIGHT = /(d|\x1B\[C)\z/.freeze
|
62
|
+
EXIT = /(q|\x03)\z/.freeze
|
63
|
+
|
64
|
+
# @param moves [Array<String>] a list of moves in SAN
|
65
|
+
# @param tags [Hash<String, String>] metadata about the game
|
66
|
+
# @param result [String] the outcome of the game
|
67
|
+
#
|
68
|
+
def initialize(moves, tags = nil, result = nil, pgn = nil, comment = nil)
|
69
|
+
self.moves = moves
|
70
|
+
self.tags = tags
|
71
|
+
self.result = result
|
72
|
+
self.pgn = pgn
|
73
|
+
self.comment = comment
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param moves [Array<String>] a list of moves in SAN
|
77
|
+
#
|
78
|
+
# Standardize castling moves to use O's instead of 0's
|
79
|
+
#
|
80
|
+
def moves=(moves)
|
81
|
+
@moves =
|
82
|
+
moves.map do |m|
|
83
|
+
if m.is_a? String
|
84
|
+
MoveText.new(m.gsub('0', 'O'))
|
85
|
+
else
|
86
|
+
MoveText.new(m.notation.gsub('0', 'O'), m.annotation, m.comment, m.variations)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def initial_fen
|
92
|
+
tags && tags['FEN']
|
93
|
+
end
|
94
|
+
|
95
|
+
def starting_position
|
96
|
+
@starting_position ||= if initial_fen
|
97
|
+
PGN::FEN.new(initial_fen).to_position
|
98
|
+
else
|
99
|
+
PGN::Position.start
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [Array<PGN::Position>] list of the {PGN::Position}s in the game
|
104
|
+
#
|
105
|
+
def positions
|
106
|
+
@positions ||= begin
|
107
|
+
position = starting_position
|
108
|
+
arr = [position]
|
109
|
+
moves.each do |move|
|
110
|
+
new_pos = position.move(move.notation)
|
111
|
+
arr << new_pos
|
112
|
+
position = new_pos
|
113
|
+
end
|
114
|
+
arr
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# @return [Array<String>] list of the fen representations of the positions
|
119
|
+
#
|
120
|
+
def fen_list
|
121
|
+
positions.map { |p| p.to_fen.inspect }
|
122
|
+
end
|
123
|
+
|
124
|
+
# Interactively step through the game
|
125
|
+
#
|
126
|
+
# Use +d+ to move forward, +a+ to move backward, and +^C+ to exit.
|
127
|
+
#
|
128
|
+
def play
|
129
|
+
index = 0
|
130
|
+
hist = Array.new(3, '')
|
131
|
+
|
132
|
+
loop do
|
133
|
+
puts "\e[H\e[2J"
|
134
|
+
puts positions[index].inspect
|
135
|
+
hist[0..2] = (hist[1..2] << STDIN.getch)
|
136
|
+
|
137
|
+
case hist.join
|
138
|
+
when LEFT
|
139
|
+
index -= 1 if index > 0
|
140
|
+
when RIGHT
|
141
|
+
index += 1 if index < moves.length
|
142
|
+
when EXIT
|
143
|
+
break
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|