pgn3 0.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/.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
|