pgn 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/pgn/parser.rb ADDED
@@ -0,0 +1,119 @@
1
+ require 'whittle'
2
+
3
+ module PGN
4
+ # {PGN::Parser} uses the whittle gem to parse pgn files based on their
5
+ # context free grammar.
6
+ #
7
+ class Parser < Whittle::Parser
8
+ rule(:wsp => /\s+/).skip!
9
+
10
+ rule("[")
11
+ rule("]")
12
+ rule("(")
13
+ rule(")")
14
+
15
+ start(:pgn_database)
16
+
17
+ rule(:pgn_database) do |r|
18
+ r[].as { [] }
19
+ r[:pgn_game, :pgn_database].as {|game, database| database << game }
20
+ end
21
+
22
+ rule(:pgn_game) do |r|
23
+ r[:tag_section, :movetext_section].as {|tags, moves| {tags: tags, result: moves.pop, moves: moves} }
24
+ end
25
+
26
+ rule(:tag_section) do |r|
27
+ r[:tag_pair, :tag_section].as {|pair, section| section.merge(pair) }
28
+ r[:tag_pair]
29
+ end
30
+
31
+ rule(:tag_pair) do |r|
32
+ r["[", :tag_name, :tag_value, "]"].as {|_, a, b, _| {a => b} }
33
+ end
34
+
35
+ rule(:tag_value) do |r|
36
+ r[:string].as {|value| value[1...-1] }
37
+ end
38
+
39
+ rule(:movetext_section) do |r|
40
+ r[:element_sequence, :game_termination].as {|a, b| a.reverse << b }
41
+ end
42
+
43
+ rule(:element_sequence) do |r|
44
+ r[:element, :element_sequence].as {|element, sequence| element.nil? ? sequence : sequence << element }
45
+ r[].as { [] }
46
+ #r[:recursive_variation, :element_sequence]
47
+ #r[:recursive_variation]
48
+ end
49
+
50
+ rule(:element) do |r|
51
+ r[:move_number_indication].as { nil }
52
+ r[:san_move]
53
+ #r[:numeric_annotation_glyph]
54
+ end
55
+
56
+ #rule(:recursive_variation) do |r|
57
+ #r["(", :element_sequence, ")"]
58
+ #end
59
+
60
+ rule(
61
+ :string => %r{
62
+ " # beginning of string
63
+ (
64
+ [[:print:]&&[^\\"]] | # printing characters except quote and backslash
65
+ \\\\ | # escaped backslashes
66
+ \\" # escaped quotation marks
67
+ )* # zero or more of the above
68
+ " # end of string
69
+ }x
70
+ )
71
+
72
+ rule(
73
+ :game_termination => %r{
74
+ 1-0 | # white wins
75
+ 0-1 | # black wins
76
+ 1\/2-1\/2 | # draw
77
+ \* # ?
78
+ }x
79
+ )
80
+
81
+ rule(
82
+ :move_number_indication => %r{
83
+ [[:digit:]]+\.* # one or more digits followed by zero or more periods
84
+ }x
85
+ )
86
+
87
+ rule(
88
+ :san_move => %r{
89
+ (
90
+ O(-O){1,2} | # castling (O-O, O-O-O)
91
+ [a-h][1-8] | # pawn moves (e4, d7)
92
+ [BKNQR][a-h1-8]?x?[a-h][1-8] | # major piece moves w/ optional specifier
93
+ # and capture
94
+ # (Bd2, N4c3, Raxc1)
95
+ [a-h][1-8]?x[a-h][1-8] # pawn captures
96
+ )
97
+ (
98
+ =[BNQR] # optional promotion (d8=Q)
99
+ )?
100
+ (
101
+ \+ | # check (g5+)
102
+ \# # checkmate (Qe7#)
103
+ )?
104
+ }x
105
+ )
106
+
107
+ rule(
108
+ :tag_name => %r{
109
+ [A-Za-z0-9_]+ # letters, digits and underscores only
110
+ }x
111
+ )
112
+
113
+ rule(
114
+ :numeric_annotation_glyph => %r{
115
+ \$\d+ # dollar sign followed by an integer from 0 to 255
116
+ }x
117
+ )
118
+ end
119
+ end
@@ -0,0 +1,129 @@
1
+ module PGN
2
+ # {PGN::Position} encapsulates all of the information necessary to
3
+ # completely understand a chess position. It can be turned into a FEN string
4
+ # or perform a move.
5
+ #
6
+ # @!attribute board
7
+ # @return [PGN::Board] the board for the position
8
+ #
9
+ # @!attribute player
10
+ # @return [Symbol] the player who moves next
11
+ # @example
12
+ # position.player #=> :white
13
+ #
14
+ # @!attribute castling
15
+ # @return [Array<String>] the castling moves that are still available
16
+ # @example
17
+ # position.castling #=> ["K", "k", "q"]
18
+ #
19
+ # @!attribute en_passant
20
+ # @return [String] the en passant square if applicable
21
+ #
22
+ # @!attribute halfmove
23
+ # @return [Integer] the number of halfmoves since the last pawn move or
24
+ # capture
25
+ #
26
+ # @!attribute fullmove
27
+ # @return [Integer] the number of fullmoves made so far
28
+ #
29
+ class Position
30
+ PLAYERS = [:white, :black]
31
+ CASTLING = %w{K Q k q}
32
+
33
+ attr_accessor :board
34
+ attr_accessor :player
35
+ attr_accessor :castling
36
+ attr_accessor :en_passant
37
+ attr_accessor :halfmove
38
+ attr_accessor :fullmove
39
+
40
+ # @return [PGN::Position] the starting position of a chess game
41
+ #
42
+ def self.start
43
+ PGN::Position.new(
44
+ PGN::Board.start,
45
+ PLAYERS.first,
46
+ castling: CASTLING,
47
+ en_passant: nil,
48
+ halfmove: 0,
49
+ fullmove: 0,
50
+ )
51
+ end
52
+
53
+ # @param board [PGN::Board] the board for the position
54
+ # @param player [Symbol] the player who moves next
55
+ # @param castling [Array<String>] the castling moves that are still
56
+ # available
57
+ # @param en_passant [String, nil] the en passant square if applicable
58
+ # @param halfmove [Integer] the number of halfmoves since the last pawn
59
+ # move or capture
60
+ # @param fullmove [Integer] the number of fullmoves made so far
61
+ #
62
+ # @example
63
+ # PGN::Position.new(
64
+ # PGN::Board.start,
65
+ # :white,
66
+ # )
67
+ #
68
+ def initialize(board, player, castling: CASTLING, en_passant: nil, halfmove: 0, fullmove: 0)
69
+ self.board = board
70
+ self.player = player
71
+ self.castling = castling
72
+ self.en_passant = en_passant
73
+ self.halfmove = halfmove
74
+ self.fullmove = fullmove
75
+ end
76
+
77
+ # @param str [String] the move to make in SAN
78
+ # @return [PGN::Position] the resulting position
79
+ #
80
+ # @example
81
+ # queens_pawn = PGN::Position.start.move("d4")
82
+ #
83
+ def move(str)
84
+ move = PGN::Move.new(str, self.player)
85
+ calculator = PGN::MoveCalculator.new(self.board, move)
86
+
87
+ new_castling = self.castling - calculator.castling_restrictions
88
+ new_halfmove = calculator.increment_halfmove? ?
89
+ self.halfmove + 1 :
90
+ 0
91
+ new_fullmove = calculator.increment_fullmove? ?
92
+ self.fullmove + 1 :
93
+ self.fullmove
94
+
95
+ PGN::Position.new(
96
+ calculator.result_board,
97
+ self.next_player,
98
+ castling: new_castling,
99
+ en_passant: calculator.en_passant_square,
100
+ halfmove: new_halfmove,
101
+ fullmove: new_fullmove,
102
+ )
103
+ end
104
+
105
+ # @return [Symbol] the next player to move
106
+ #
107
+ def next_player
108
+ (PLAYERS - [self.player]).first
109
+ end
110
+
111
+ def inspect
112
+ "\n" + self.board.inspect
113
+ end
114
+
115
+ # @return [PGN::FEN] a {PGN::FEN} object representing the current position
116
+ #
117
+ def to_fen
118
+ PGN::FEN.from_attributes(
119
+ board: self.board,
120
+ active: self.player == :white ? 'w' : 'b',
121
+ castling: self.castling.join(''),
122
+ en_passant: self.en_passant,
123
+ halfmove: self.halfmove.to_s,
124
+ fullmove: self.fullmove.to_s,
125
+ )
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,3 @@
1
+ module PGN
2
+ VERSION = "0.0.1"
3
+ end
data/pgn.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pgn/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pgn"
8
+ spec.version = PGN::VERSION
9
+ spec.authors = ["Stacey Touset"]
10
+ spec.email = ["stacey@touset.org"]
11
+ spec.description = %q{A PGN parser and FEN generator for Ruby}
12
+ spec.summary = %q{A PGN parser for Ruby}
13
+ spec.homepage = "https://github.com/capicue/pgn"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "whittle"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ end
data/spec/fen_spec.rb ADDED
@@ -0,0 +1,87 @@
1
+ require "spec_helper"
2
+
3
+ describe PGN::FEN do
4
+ describe "castling availability" do
5
+ it "should remove all castling availabilitiy after castling" do
6
+ pos = PGN::FEN.new("rnbqk2r/1p3pbp/p2p1np1/2pP4/P3PB2/2N2N2/1P3PPP/R2QKB1R b KQkq e3 0 8").to_position
7
+ next_pos = pos.move("O-O")
8
+ next_pos.to_fen.castling.should_not match(/k|q/)
9
+ end
10
+
11
+ it "should remove all castling availability after moving a king" do
12
+ pos = PGN::FEN.new("r1b1kb1r/pp2pppp/2n5/4P3/1nB5/P4N2/1P3PPP/RNBqK2R w KQkq - 0 9").to_position
13
+ next_pos = pos.move("Kxd1")
14
+ next_pos.to_fen.castling.should_not match(/K|Q/)
15
+ end
16
+
17
+ it "should remove only the one castling option after moving a rook" do
18
+ pos = PGN::FEN.new("r3k2r/1pp2pp1/p1pb1qn1/4p3/3PP1p1/8/PPPN1PPN/R1BQR1K1 b kq - 1 11").to_position
19
+ next_pos = pos.move("Rxh2")
20
+ next_pos.to_fen.castling.should_not match(/k/)
21
+ next_pos.to_fen.castling.should match(/q/)
22
+ end
23
+
24
+ it "should change to a hyphen once no side can castle" do
25
+ pos = PGN::FEN.new("r1bq1rk1/pp1nbppp/3ppn2/8/2PP1N2/P1N5/1P2BPPP/R1BQK2R w KQ - 2 9").to_position
26
+ pos.to_fen.castling.should_not == "-"
27
+ next_pos = pos.move("O-O")
28
+ next_pos.to_fen.castling.should == "-"
29
+ end
30
+ end
31
+
32
+ describe "en passant" do
33
+ it "should display the en passant square whenever a pawn moves two spaces" do
34
+ pos = PGN::FEN.new("rnbqkb1r/pppppppp/5n2/8/8/5N2/PPPPPPPP/RNBQKB1R w KQkq - 2 1").to_position
35
+ next_pos = pos.move("c4")
36
+ next_pos.to_fen.en_passant.should == "c3"
37
+ end
38
+
39
+ it "should be a hyphen if no pawn moved two spaces the previous move" do
40
+ pos = PGN::FEN.new("rnbqkb1r/pppppppp/5n2/8/2P5/5N2/PP1PPPPP/RNBQKB1R b KQkq c3 0 1").to_position
41
+ next_pos = pos.move("d6")
42
+ next_pos.to_fen.en_passant.should == "-"
43
+ end
44
+ end
45
+
46
+ describe "halfmove counter" do
47
+ it "should reset after a pawn advance" do
48
+ pos = PGN::FEN.new("2b2rk1/2pp1ppp/1p6/r3P2q/3Q4/2P5/PP3PPP/RN3RK1 w - - 3 15").to_position
49
+ pos.to_fen.halfmove.should == "3"
50
+ next_pos = pos.move("f4")
51
+ next_pos.to_fen.halfmove.should == "0"
52
+ end
53
+
54
+ it "should reset after a capture" do
55
+ pos = PGN::FEN.new("2r2rk1/1p2ppbp/1q1p1np1/pN4B1/Pnb1PP2/2N5/1PP1B1PP/R2Q1R1K w - - 5 14").to_position
56
+ pos.to_fen.halfmove.should == "5"
57
+ next_pos = pos.move("Bxc4")
58
+ next_pos.to_fen.halfmove.should == "0"
59
+ end
60
+
61
+ it "should not reset otherwise" do
62
+ pos = PGN::FEN.new("2k2b1r/p4p1p/q1p2p2/8/2br4/5P2/PPQB2PP/R1N1K2R w KQ - 0 17").to_position
63
+ pos.to_fen.halfmove.should == "0"
64
+ moves = %w{Qf5+ Rd7 Bc3 Bh6 Qa5 Re8+ Kf2 Be3+ Kg3 Rg8+ Kh4}
65
+ moves.each_with_index do |move, i|
66
+ pos = pos.move(move)
67
+ pos.to_fen.halfmove.should == (i+1).to_s
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "fullmove counter" do
73
+ it "should not increase after white moves" do
74
+ pos = PGN::FEN.new("4br1k/ppqnr1b1/3p3p/P1pP1p2/2P1pB2/6PP/1P2BP1N/R2QR1K1 w - - 3 25").to_position
75
+ pos.to_fen.fullmove.should == "25"
76
+ next_pos = pos.move("Qd2")
77
+ next_pos.to_fen.fullmove.should == "25"
78
+ end
79
+
80
+ it "should increase after black moves" do
81
+ pos = PGN::FEN.new("4br1k/ppqnr1b1/3p3p/P1pP1p2/2P1pB2/6PP/1P1QBP1N/R3R1K1 b - - 4 25").to_position
82
+ pos.to_fen.fullmove.should == "25"
83
+ next_pos = pos.move("Kh7")
84
+ next_pos.to_fen.fullmove.should == "26"
85
+ end
86
+ end
87
+ end
data/spec/game_spec.rb ADDED
@@ -0,0 +1,13 @@
1
+ require "spec_helper"
2
+
3
+ describe PGN::Game do
4
+ describe "#positions" do
5
+ it "should not raise an error" do
6
+ tags = {"White" => "Deep Blue", "Black" => "Kasparov"}
7
+ moves = %w{e4 c5 c3 d5 exd5 Qxd5 d4 Nf6 Nf3 Bg4 Be2 e6 h3 Bh5 O-O Nc6 Be3 cxd4 cxd4 Bb4 a3 Ba5 Nc3 Qd6 Nb5 Qe7 Ne5 Bxe2 Qxe2 O-O Rac1 Rac8 Bg5 Bb6 Bxf6 gxf6 Nc4 Rfd8 Nxb6 axb6 Rfd1 f5 Qe3 Qf6 d5 Rxd5 Rxd5 exd5 b3 Kh8 Qxb6 Rg8 Qc5 d4 Nd6 f4 Nxb7 Ne5 Qd5 f3 g3 Nd3 Rc7 Re8 Nd6 Re1+ Kh2 Nxf2 Nxf7+ Kg7 Ng5+ Kh6 Rxh7+}
8
+ result = "1-0"
9
+ game = PGN::Game.new(moves, tags, result)
10
+ lambda { game.positions }.should_not raise_error
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ describe PGN do
4
+ describe "parsing a file" do
5
+ it "should return a list of games" do
6
+ games = PGN.parse(File.read("./examples/immortal_game.pgn"))
7
+ games.length.should == 1
8
+ game = games.first
9
+ game.result.should == "1-0"
10
+ game.tags["White"].should == "Adolf Anderssen"
11
+ game.moves.last.should == "Be7#"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe PGN::Position do
4
+ context "disambiguating moves" do
5
+ describe "using SAN square disambiguation" do
6
+ pos = PGN::FEN.new("r1bqkb1r/pp1p1ppp/2n1pn2/8/3NP3/2N5/PPP2PPP/R1BQKB1R w KQkq - 3 6").to_position
7
+ next_pos = pos.move("Ndb5")
8
+
9
+ it "should move the specified piece" do
10
+ next_pos.board.at("d4").should be_nil
11
+ end
12
+
13
+ it "should not move the other piece" do
14
+ next_pos.board.at("c3").should == "N"
15
+ end
16
+ end
17
+
18
+ describe "using discovered check" do
19
+ pos = PGN::FEN.new("rnbqk2r/p1pp1ppp/1p2pn2/8/1bPP4/2N1P3/PP3PPP/R1BQKBNR w KQkq - 0 5").to_position
20
+ next_pos = pos.move("Ne2")
21
+
22
+ it "should move the piece that doesn't give discovered check" do
23
+ next_pos.board.at("g1").should be_nil
24
+ end
25
+
26
+ it "shouldn't move the other piece" do
27
+ next_pos.board.at("c3").should == "N"
28
+ end
29
+ end
30
+
31
+ describe "with two pawns on the same file" do
32
+ pos = PGN::FEN.new("r2q1rk1/4bppp/p3n3/1p2n3/4N3/1B2BP2/PP3P1P/R2Q1RK1 w - - 4 19").to_position
33
+ next_pos = pos.move("f4")
34
+
35
+ it "should move the pawn in front" do
36
+ next_pos.board.at("f3").should be_nil
37
+ end
38
+
39
+ it "should not move the other pawn" do
40
+ next_pos.board.at("f2").should == "P"
41
+ end
42
+ end
43
+ end
44
+ end