pgn 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b151e9dfbad65addcdbcc73985f280a1b7b42a55
4
+ data.tar.gz: 1aecd46315e114eec514ff627c442dd559bf5237
5
+ SHA512:
6
+ metadata.gz: 4bb8e0a0e1ca54fb4fb4644e96a68ef3162a16c3dfc6e602993fe6e195dd1fcbe9b31e42239a61f608ca15698ff126a51ea5f0f202433e83aa52e2b5cd92e8d5
7
+ data.tar.gz: 6f070630bebabc25cd9261819cd2beb0d3d7a0309dcba56244f6712fcd339fab0db154c1ae785b3d33ca1e3f1888576d0b4143d7ddd8d1fe52f7941dd585ee84
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pgn.gemspec
4
+ gemspec
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,122 @@
1
+ # PGN
2
+
3
+ A PGN parser and FEN generator for ruby.
4
+
5
+ ## Usage
6
+
7
+ ### Creating games from pgn files
8
+
9
+ On the command line, it is easy to read in and play through chess games
10
+ in [portable game notation](http://en.wikipedia.org/wiki/Portable_Game_Notation) format.
11
+
12
+ ```
13
+ > games = PGN.parse(File.read("./examples/immortal_game.pgn"))
14
+ > game = games.first
15
+ > game.play
16
+ ```
17
+
18
+ Play through the game using `a` to move backward and `d` to move
19
+ forward. `^C` quits play mode.
20
+
21
+ ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
22
+ ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
23
+ _ _ _ _ _ _ _ _
24
+ _ _ _ _ _ _ _ _
25
+ _ _ _ _ _ _ _ _
26
+ _ _ _ _ _ _ _ _
27
+ ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
28
+ ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
29
+
30
+ ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
31
+ ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
32
+ _ _ _ _ _ _ _ _
33
+ _ _ _ _ _ _ _ _
34
+ _ _ _ _ _ _ _ _
35
+ _ _ _ _ ♙ _ _ _
36
+ _ _ _ _ _ _ _ _
37
+ ♙ ♙ ♙ ♙ _ ♙ ♙ ♙
38
+ ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
39
+
40
+ ...
41
+
42
+ You can also access all of the information about a game.
43
+
44
+ ```
45
+ > game.positions.last
46
+ =>
47
+ ♜ _ ♝ ♚ _ _ _ ♜
48
+ ♟ _ _ ♟ ♗ ♟ ♘ ♟
49
+ ♞ _ _ _ _ ♞ _ _
50
+ _ ♟ _ ♘ ♙ _ _ ♙
51
+ _ _ _ _ _ _ ♙ _
52
+ _ _ _ ♙ _ _ _ _
53
+ ♙ _ ♙ _ ♔ _ _ _
54
+ ♛ _ _ _ _ _ ♝ _
55
+
56
+ > game.positions.last.to_fen
57
+ => r1bk3r/p2pBpNp/n4n2/1p1NP2P/6P1/3P4/P1P1K3/q5b1 b - - 1 22
58
+
59
+ > game.result
60
+ => "1-0"
61
+
62
+ > game.tags["White"]
63
+ => "Adolf Anderssen"
64
+ ```
65
+
66
+ It is possible to create a game without parsing a pgn file.
67
+
68
+ ```
69
+ moves = %w{e4 c5 c3 d5 exd5 Qxd5 d4 Nf6}
70
+ game = PGN::Game.new(moves)
71
+ ```
72
+
73
+ Note that if you simply want an abstract syntax tree from the pgn file,
74
+ you can use `PGN::Parser.parse`.
75
+
76
+ ### Dealing with FEN strings
77
+
78
+ [Forsyth Edwards Notation](http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation)
79
+ is a compact way to represent all of the information about a given chess
80
+ position. It is easy to convert between FEN strings and chess positions.
81
+
82
+ ```
83
+ > fen = PGN::FEN.start
84
+ => rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
85
+
86
+ > fen = PGN::FEN.new("r1bk3r/p2pBpNp/n4n2/1p1NP2P/6P1/3P4/P1P1K3/q5b1 b - - 1 22")
87
+ > position = fen.to_position
88
+ =>
89
+ ♜ _ ♝ ♚ _ _ _ ♜
90
+ ♟ _ _ ♟ ♗ ♟ ♘ ♟
91
+ ♞ _ _ _ _ ♞ _ _
92
+ _ ♟ _ ♘ ♙ _ _ ♙
93
+ _ _ _ _ _ _ ♙ _
94
+ _ _ _ ♙ _ _ _ _
95
+ ♙ _ ♙ _ ♔ _ _ _
96
+ ♛ _ _ _ _ _ ♝ _
97
+
98
+ > position.to_fen
99
+ => r1bk3r/p2pBpNp/n4n2/1p1NP2P/6P1/3P4/P1P1K3/q5b1 b - - 1 22
100
+ ```
101
+
102
+ ## Installation
103
+
104
+ Add this line to your application's Gemfile:
105
+
106
+ gem 'pgn'
107
+
108
+ And then execute:
109
+
110
+ $ bundle
111
+
112
+ Or install it yourself as:
113
+
114
+ $ gem install pgn
115
+
116
+ ## Contributing
117
+
118
+ 1. Fork it
119
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
120
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
121
+ 4. Push to the branch (`git push origin my-new-feature`)
122
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/TODO.md ADDED
@@ -0,0 +1,4 @@
1
+ # TODOs
2
+
3
+ - `Game#to_pgn`
4
+ - Speed up parsing
@@ -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.rb ADDED
@@ -0,0 +1,22 @@
1
+ require "pgn/board"
2
+ require "pgn/fen"
3
+ require "pgn/game"
4
+ require "pgn/move"
5
+ require "pgn/move_calculator"
6
+ require "pgn/parser"
7
+ require "pgn/position"
8
+ require "pgn/version"
9
+
10
+ module PGN
11
+
12
+ # @param pgn [String] a pgn representation of one or more chess games
13
+ # @return [Array<PGN::Game>] a list of games
14
+ #
15
+ def self.parse(pgn)
16
+ pgn.force_encoding(Encoding::ISO_8859_1)
17
+
18
+ PGN::Parser.new.parse(pgn).map do |game|
19
+ PGN::Game.new(game[:moves], game[:tags], game[:result])
20
+ end
21
+ end
22
+ end
data/lib/pgn/board.rb ADDED
@@ -0,0 +1,183 @@
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
+ class Board
13
+ # The starting, internal representation of a chess board
14
+ #
15
+ START = [
16
+ ["R", "P", "_", "_", "_", "_", "p", "r"],
17
+ ["N", "P", "_", "_", "_", "_", "p", "n"],
18
+ ["B", "P", "_", "_", "_", "_", "p", "b"],
19
+ ["Q", "P", "_", "_", "_", "_", "p", "q"],
20
+ ["K", "P", "_", "_", "_", "_", "p", "k"],
21
+ ["B", "P", "_", "_", "_", "_", "p", "b"],
22
+ ["N", "P", "_", "_", "_", "_", "p", "n"],
23
+ ["R", "P", "_", "_", "_", "_", "p", "r"],
24
+ ]
25
+
26
+ FILE_TO_INDEX = {
27
+ 'a' => 0,
28
+ 'b' => 1,
29
+ 'c' => 2,
30
+ 'd' => 3,
31
+ 'e' => 4,
32
+ 'f' => 5,
33
+ 'g' => 6,
34
+ 'h' => 7,
35
+ }
36
+ INDEX_TO_FILE = Hash[FILE_TO_INDEX.map(&:reverse)]
37
+
38
+ RANK_TO_INDEX = {
39
+ '1' => 0,
40
+ '2' => 1,
41
+ '3' => 2,
42
+ '4' => 3,
43
+ '5' => 4,
44
+ '6' => 5,
45
+ '7' => 6,
46
+ '8' => 7,
47
+ }
48
+ INDEX_TO_RANK = Hash[RANK_TO_INDEX.map(&:reverse)]
49
+
50
+ # algebraic to unicode piece lookup
51
+ #
52
+ UNICODE_PIECES = {
53
+ 'k' => "\u{265A}",
54
+ 'q' => "\u{265B}",
55
+ 'r' => "\u{265C}",
56
+ 'b' => "\u{265D}",
57
+ 'n' => "\u{265E}",
58
+ 'p' => "\u{265F}",
59
+ 'K' => "\u{2654}",
60
+ 'Q' => "\u{2655}",
61
+ 'R' => "\u{2656}",
62
+ 'B' => "\u{2657}",
63
+ 'N' => "\u{2658}",
64
+ 'P' => "\u{2659}",
65
+ '_' => '_',
66
+ }
67
+
68
+ attr_accessor :squares
69
+
70
+ # @return [PGN::Board] a board in the starting position
71
+ #
72
+ def self.start
73
+ PGN::Board.new(START)
74
+ end
75
+
76
+ # @param squares [<Array<Array<String>>>] the squares of the board
77
+ # @example
78
+ # PGN::Board.new(
79
+ # [
80
+ # ["R", "P", "_", "_", "_", "_", "p", "r"],
81
+ # ["N", "P", "_", "_", "_", "_", "p", "n"],
82
+ # ["B", "P", "_", "_", "_", "_", "p", "b"],
83
+ # ["Q", "P", "_", "_", "_", "_", "p", "q"],
84
+ # ["K", "P", "_", "_", "_", "_", "p", "k"],
85
+ # ["B", "P", "_", "_", "_", "_", "p", "b"],
86
+ # ["N", "P", "_", "_", "_", "_", "p", "n"],
87
+ # ["R", "P", "_", "_", "_", "_", "p", "r"],
88
+ # ]
89
+ # )
90
+ #
91
+ def initialize(squares)
92
+ self.squares = squares
93
+ end
94
+
95
+ # @overload at(str)
96
+ # Looks up a piece based on the string representation of a square (e4)
97
+ # @param str [String] the square in algebraic notation
98
+ # @overload at(file, rank)
99
+ # Looks up a piece based on zero-indexed coordinates (4, 3)
100
+ # @param file [Integer] the file the piece is on
101
+ # @param rank [Integer] the rank the piece is on
102
+ # @return [String, nil] the piece on the square, or nil if it is
103
+ # empty
104
+ # @example
105
+ # board.at(4,3) #=> "P"
106
+ # board.at("e4") #=> "P"
107
+ #
108
+ def at(*args)
109
+ str = case args.length
110
+ when 1
111
+ self.at(*coordinates_for(args.first))
112
+ when 2
113
+ self.squares[args[0]][args[1]]
114
+ end
115
+
116
+ str == "_" ? nil : str
117
+ end
118
+
119
+ # @param changes [Hash<String, <String, nil>>] changes to make to the board
120
+ # @return [self]
121
+ # @example
122
+ # board.change!({"e2" => nil, "e4" => "P"})
123
+ #
124
+ def change!(changes)
125
+ changes.each do |square, piece|
126
+ self.update(square, piece)
127
+ end
128
+ self
129
+ end
130
+
131
+ # @param square [String] the square in algebraic notation
132
+ # @param piece [String, nil] the piece to put on the square
133
+ # @return [self]
134
+ # @example
135
+ # board.update("e4", "P")
136
+ #
137
+ def update(square, piece)
138
+ coords = coordinates_for(square)
139
+ self.squares[coords[0]][coords[1]] = piece || "_"
140
+ self
141
+ end
142
+
143
+ # @param position [String] the square in algebraic notation
144
+ # @return [Array<Integer>] the coordinates of the square
145
+ # @example
146
+ # board.coordinates_for("e4") #=> [4, 3]
147
+ #
148
+ def coordinates_for(position)
149
+ file_chr, rank_chr = position.chars
150
+ file = FILE_TO_INDEX[file_chr]
151
+ rank = RANK_TO_INDEX[rank_chr]
152
+ [file, rank]
153
+ end
154
+
155
+ # @param coordinates [Array<Integer>] the coordinates of the square
156
+ # @return [String] the square in algebraic notation
157
+ # @example
158
+ # board.position_for([4, 3]) #=> "e4"
159
+ #
160
+ def position_for(coordinates)
161
+ file, rank = coordinates
162
+ file_chr = INDEX_TO_FILE[file]
163
+ rank_chr = INDEX_TO_RANK[rank]
164
+ [file_chr, rank_chr].join('')
165
+ end
166
+
167
+ # @return [String] the board in human readable format with unicode
168
+ # pieces
169
+ #
170
+ def inspect
171
+ self.squares.transpose.reverse.map do |row|
172
+ row.map{|chr| UNICODE_PIECES[chr] }.join(' ')
173
+ end.join("\n")
174
+ end
175
+
176
+ # @return [PGN::Board] a copy of self with duplicated squares
177
+ #
178
+ def dup
179
+ PGN::Board.new(self.squares.map(&:dup))
180
+ end
181
+
182
+ end
183
+ end
data/lib/pgn/fen.rb ADDED
@@ -0,0 +1,146 @@
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
+ .reverse
91
+ .transpose
92
+ self.board = PGN::Board.new(squares)
93
+ end
94
+
95
+ # @return [String] the fen representation of the board
96
+ # @example
97
+ # PGN::FEN.start.board_string #=> "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
98
+ #
99
+ def board_string
100
+ self.board
101
+ .squares
102
+ .transpose
103
+ .reverse
104
+ .map {|row| row.join }
105
+ .join("/")
106
+ .gsub(/_+/) {|match| match.length }
107
+ end
108
+
109
+ # @return [PGN::Position] a {PGN::Position} representing the current
110
+ # position
111
+ #
112
+ def to_position
113
+ player = self.active == 'w' ? :white : :black
114
+ castling = self.castling.split('') - ['-']
115
+ en_passant = self.en_passant == '-' ? nil : en_passant
116
+
117
+ PGN::Position.new(
118
+ self.board,
119
+ player,
120
+ castling: castling,
121
+ en_passant: en_passant,
122
+ halfmove: self.halfmove.to_i,
123
+ fullmove: self.fullmove.to_i,
124
+ )
125
+ end
126
+
127
+ # @return [String] the FEN string
128
+ # @example
129
+ # PGN::FEN.start.to_s #=> "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
130
+ #
131
+ def to_s
132
+ [
133
+ self.fen_string,
134
+ self.active,
135
+ self.castling,
136
+ self.en_passant,
137
+ self.halfmove,
138
+ self.fullmove,
139
+ ].join(" ")
140
+ end
141
+
142
+ def inspect
143
+ self.to_s
144
+ end
145
+ end
146
+ end