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
data/lib/pgn/position.rb
ADDED
@@ -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
|
+
|
30
|
+
class Position
|
31
|
+
PLAYERS = %i[white black].freeze
|
32
|
+
CASTLING = %w[K Q k q].freeze
|
33
|
+
|
34
|
+
attr_accessor :board
|
35
|
+
attr_accessor :player
|
36
|
+
attr_accessor :castling
|
37
|
+
attr_accessor :en_passant
|
38
|
+
attr_accessor :halfmove
|
39
|
+
attr_accessor :fullmove
|
40
|
+
|
41
|
+
# @return [PGN::Position] the starting position of a chess game
|
42
|
+
#
|
43
|
+
def self.start
|
44
|
+
PGN::Position.new(
|
45
|
+
PGN::Board.start,
|
46
|
+
PLAYERS.first
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @param board [PGN::Board] the board for the position
|
51
|
+
# @param player [Symbol] the player who moves next
|
52
|
+
# @param castling [Array<String>] the castling moves that are still
|
53
|
+
# available
|
54
|
+
# @param en_passant [String, nil] the en passant square if applicable
|
55
|
+
# @param halfmove [Integer] the number of halfmoves since the last pawn
|
56
|
+
# move or capture
|
57
|
+
# @param fullmove [Integer] the number of fullmoves made so far
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# PGN::Position.new(
|
61
|
+
# PGN::Board.start,
|
62
|
+
# :white,
|
63
|
+
# )
|
64
|
+
#
|
65
|
+
def initialize(board, player, castling = CASTLING, en_passant = nil, halfmove = 0, fullmove = 1)
|
66
|
+
self.board = board
|
67
|
+
self.player = player
|
68
|
+
self.castling = castling
|
69
|
+
self.en_passant = en_passant
|
70
|
+
self.halfmove = halfmove
|
71
|
+
self.fullmove = fullmove
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param str [String] the move to make in SAN
|
75
|
+
# @return [PGN::Position] the resulting position
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# queens_pawn = PGN::Position.start.move("d4")
|
79
|
+
#
|
80
|
+
def move(str)
|
81
|
+
move = PGN::Move.new(str, player)
|
82
|
+
calculator = PGN::MoveCalculator.new(board, move)
|
83
|
+
|
84
|
+
new_castling = castling - calculator.castling_restrictions
|
85
|
+
new_halfmove = if calculator.increment_halfmove?
|
86
|
+
halfmove + 1
|
87
|
+
else
|
88
|
+
0
|
89
|
+
end
|
90
|
+
new_fullmove = if calculator.increment_fullmove?
|
91
|
+
fullmove + 1
|
92
|
+
else
|
93
|
+
fullmove
|
94
|
+
end
|
95
|
+
no_move = str == '--'
|
96
|
+
PGN::Position.new(
|
97
|
+
no_move ? board : calculator.result_board,
|
98
|
+
next_player,
|
99
|
+
new_castling,
|
100
|
+
calculator.en_passant_square,
|
101
|
+
new_halfmove,
|
102
|
+
new_fullmove
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
# @return [Symbol] the next player to move
|
107
|
+
#
|
108
|
+
def next_player
|
109
|
+
(PLAYERS - [player]).first
|
110
|
+
end
|
111
|
+
|
112
|
+
def inspect
|
113
|
+
"\n" + board.inspect
|
114
|
+
end
|
115
|
+
|
116
|
+
# @return [PGN::FEN] a {PGN::FEN} object representing the current position
|
117
|
+
#
|
118
|
+
def to_fen
|
119
|
+
PGN::FEN.from_attributes(
|
120
|
+
board: board,
|
121
|
+
active: player == :white ? 'w' : 'b',
|
122
|
+
castling: castling.join(''),
|
123
|
+
en_passant: en_passant,
|
124
|
+
halfmove: halfmove.to_s,
|
125
|
+
fullmove: fullmove.to_s
|
126
|
+
)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/pgn/version.rb
ADDED
data/lib/pgn.rb
ADDED
@@ -0,0 +1,26 @@
|
|
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
|
+
# @param pgn [String] a pgn representation of one or more chess games
|
12
|
+
# @return [Array<PGN::Game>] a list of games
|
13
|
+
#
|
14
|
+
# @note The PGN spec specifies Latin-1 as the encoding for PGN files, so
|
15
|
+
# this is default.
|
16
|
+
#
|
17
|
+
# @see http://www.chessclub.com/help/PGN-spec PGN Specification
|
18
|
+
#
|
19
|
+
def self.parse(pgn, encoding = Encoding::ISO_8859_1)
|
20
|
+
pgn.force_encoding(encoding) if encoding
|
21
|
+
|
22
|
+
PGN::Parser.new.parse(pgn).map do |game|
|
23
|
+
PGN::Game.new(game[:moves], game[:tags], game[:result], game[:pgn], game[:comment])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/pgn3.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
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 = 'pgn3'
|
8
|
+
spec.version = PGN::VERSION
|
9
|
+
spec.authors = ['Stacey Touset', 'Murilo Vasconcelos', 'Alexis Vargas']
|
10
|
+
spec.email = ['stacey@touset.org', 'muriloime@gmail.com', 'lexisvar@gmail.comm']
|
11
|
+
spec.description = 'A PGN parser and FEN generator for Ruby'
|
12
|
+
spec.summary = 'A PGN parser for Ruby'
|
13
|
+
spec.homepage = 'https://github.com/lexisvar/pgn.git'
|
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', '~> 2.3'
|
24
|
+
spec.add_development_dependency 'pry'
|
25
|
+
spec.add_development_dependency 'rake'
|
26
|
+
spec.add_development_dependency 'rspec'
|
27
|
+
end
|
data/spec/fen_spec.rb
ADDED
@@ -0,0 +1,100 @@
|
|
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 remove one castling option when a rook is taken" do
|
25
|
+
pos = PGN::FEN.new("rn1qkbnr/pbpppppp/1p6/8/6P1/2N4P/PPPPPP2/R1BQKBNR b KQkq - 2 3").to_position
|
26
|
+
next_pos = pos.move("Bxh1")
|
27
|
+
next_pos.to_fen.castling.should_not match(/K/)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should change to a hyphen once no side can castle" do
|
31
|
+
pos = PGN::FEN.new("r1bq1rk1/pp1nbppp/3ppn2/8/2PP1N2/P1N5/1P2BPPP/R1BQK2R w KQ - 2 9").to_position
|
32
|
+
pos.to_fen.castling.should_not == "-"
|
33
|
+
next_pos = pos.move("O-O")
|
34
|
+
next_pos.to_fen.castling.should == "-"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "en passant" do
|
39
|
+
it "should display the en passant square whenever a pawn moves two spaces" do
|
40
|
+
pos = PGN::FEN.new("rnbqkb1r/pppppppp/5n2/8/8/5N2/PPPPPPPP/RNBQKB1R w KQkq - 2 1").to_position
|
41
|
+
next_pos = pos.move("c4")
|
42
|
+
next_pos.to_fen.en_passant.should == "c3"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should be a hyphen if no pawn moved two spaces the previous move" do
|
46
|
+
pos = PGN::FEN.new("rnbqkb1r/pppppppp/5n2/8/2P5/5N2/PP1PPPPP/RNBQKB1R b KQkq c3 0 1").to_position
|
47
|
+
next_pos = pos.move("d6")
|
48
|
+
next_pos.to_fen.en_passant.should == "-"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "halfmove counter" do
|
53
|
+
it "should reset after a pawn advance" do
|
54
|
+
pos = PGN::FEN.new("2b2rk1/2pp1ppp/1p6/r3P2q/3Q4/2P5/PP3PPP/RN3RK1 w - - 3 15").to_position
|
55
|
+
pos.to_fen.halfmove.should == "3"
|
56
|
+
next_pos = pos.move("f4")
|
57
|
+
next_pos.to_fen.halfmove.should == "0"
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should reset after a capture" do
|
61
|
+
pos = PGN::FEN.new("2r2rk1/1p2ppbp/1q1p1np1/pN4B1/Pnb1PP2/2N5/1PP1B1PP/R2Q1R1K w - - 5 14").to_position
|
62
|
+
pos.to_fen.halfmove.should == "5"
|
63
|
+
next_pos = pos.move("Bxc4")
|
64
|
+
next_pos.to_fen.halfmove.should == "0"
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should not reset otherwise" do
|
68
|
+
pos = PGN::FEN.new("2k2b1r/p4p1p/q1p2p2/8/2br4/5P2/PPQB2PP/R1N1K2R w KQ - 0 17").to_position
|
69
|
+
pos.to_fen.halfmove.should == "0"
|
70
|
+
moves = %w{Qf5+ Rd7 Bc3 Bh6 Qa5 Re8+ Kf2 Be3+ Kg3 Rg8+ Kh4}
|
71
|
+
moves.each_with_index do |move, i|
|
72
|
+
pos = pos.move(move)
|
73
|
+
pos.to_fen.halfmove.should == (i+1).to_s
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "fullmove counter" do
|
79
|
+
it "should not increase after white moves" do
|
80
|
+
pos = PGN::FEN.new("4br1k/ppqnr1b1/3p3p/P1pP1p2/2P1pB2/6PP/1P2BP1N/R2QR1K1 w - - 3 25").to_position
|
81
|
+
pos.to_fen.fullmove.should == "25"
|
82
|
+
next_pos = pos.move("Qd2")
|
83
|
+
next_pos.to_fen.fullmove.should == "25"
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should increase after black moves" do
|
87
|
+
pos = PGN::FEN.new("4br1k/ppqnr1b1/3p3p/P1pP1p2/2P1pB2/6PP/1P1QBP1N/R3R1K1 b - - 4 25").to_position
|
88
|
+
pos.to_fen.fullmove.should == "25"
|
89
|
+
next_pos = pos.move("Kh7")
|
90
|
+
next_pos.to_fen.fullmove.should == "26"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "displaying FEN notation" do
|
95
|
+
it "should return a string on inspect" do
|
96
|
+
fen = PGN::FEN.start
|
97
|
+
fen.inspect.should be_a(String)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/spec/game_spec.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PGN::Game do
|
4
|
+
describe '#positions' do
|
5
|
+
it 'does 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
|
+
expect { game.positions }.not_to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'has fullmove 2 after 1.e4 c5' do
|
14
|
+
moves = %w[e4 c5]
|
15
|
+
game = PGN::Game.new(moves)
|
16
|
+
last_pos = game.positions.last
|
17
|
+
|
18
|
+
expect(last_pos.fullmove).to eq 2
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
describe PGN do
|
5
|
+
describe '.parse' do
|
6
|
+
it 'should return a list of games' do
|
7
|
+
games = PGN.parse(File.read('./examples/immortal_game.pgn'))
|
8
|
+
expect(games.length).to eq 1
|
9
|
+
game = games.first
|
10
|
+
expect(game.result).to eq '1-0'
|
11
|
+
expect(game.tags['White']).to eq 'Adolf Anderssen'
|
12
|
+
expect(game.moves.last).to eq 'Be7#'
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should handle alternate castling notation' do
|
16
|
+
games = PGN.parse(File.read('./spec/pgn_files/alternate_castling.pgn'))
|
17
|
+
game = games.first
|
18
|
+
expect(game.tags['White']).to eq 'Somebody'
|
19
|
+
expect(game.result).to eq '*'
|
20
|
+
expect(game.moves.last).to eq 'O-O-O'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should handle annotations' do
|
24
|
+
games = PGN.parse(File.read('./spec/pgn_files/annotations.pgn'))
|
25
|
+
games.each do |game|
|
26
|
+
expect(game.tags['White']).to eq 'Fool'
|
27
|
+
expect(game.result).to eq '0-1'
|
28
|
+
expect(game.moves.last).to eq 'Qh4#'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should handle many games in order' do
|
33
|
+
games = PGN.parse(File.read('./spec/pgn_files/two_games.pgn'))
|
34
|
+
expect(games.first.moves(&:notation)).to eq ['f3', 'e5', 'g4', 'Qh4#']
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should handle comments' do
|
38
|
+
games = PGN.parse(File.read('./spec/pgn_files/comments.pgn'))
|
39
|
+
game = games.first
|
40
|
+
expect(game.tags['White']).to eq 'Scholar'
|
41
|
+
expect(game.result).to eq '1-0'
|
42
|
+
expect(game.moves.last).to eq 'Qxf7#'
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should handle multiline comments' do
|
46
|
+
games = PGN.parse(File.read('./spec/pgn_files/multiline_comments.pgn'))
|
47
|
+
game = games.first
|
48
|
+
expect(game.tags['White']).to eq 'Scholar'
|
49
|
+
expect(game.result).to eq '1-0'
|
50
|
+
expect(game.moves.last).to eq 'Qxf7#'
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should handle nested comments' do
|
54
|
+
games = PGN.parse(File.read('./spec/pgn_files/nested_comments.pgn'))
|
55
|
+
game = games.first
|
56
|
+
expect(game.result).to eq '*'
|
57
|
+
expect(game.moves.last).to eq 'Nf6'
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'handles two annotations' do
|
61
|
+
games = PGN.parse(File.read('./spec/pgn_files/two_annotations.pgn'))
|
62
|
+
game = games.first
|
63
|
+
expect(game.moves[1].annotation).to eq ['$2', '$11']
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'returns empty array when no variations' do
|
67
|
+
games = PGN.parse(File.read('./spec/pgn_files/variations.pgn'))
|
68
|
+
game = games.first
|
69
|
+
expect(game.moves.first.variations).to eq []
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should handle variations' do
|
73
|
+
games = PGN.parse(File.read('./spec/pgn_files/variations.pgn'))
|
74
|
+
game = games.first
|
75
|
+
expect(game.tags['Black']).to eq 'Petrov'
|
76
|
+
expect(game.result).to eq '*'
|
77
|
+
expect(game.moves.last).to eq 'Nf6'
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should handle variations longer than 1' do
|
81
|
+
games = PGN.parse(File.read('./spec/pgn_files/variations.pgn'))
|
82
|
+
game = games.first
|
83
|
+
# puts game.moves.map{|x| x.variations.to_a.count }
|
84
|
+
expect(game.moves[-2].variations.first).to eq %w[f4 exf4]
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should handle empty variation moves' do
|
88
|
+
games = PGN.parse(File.read('./spec/pgn_files/empty_variation_move.pgn'))
|
89
|
+
game = games.first
|
90
|
+
expect(game.result).to eq '*'
|
91
|
+
expect(game.moves.last).to eq 'Ng5'
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should handle complex files' do
|
95
|
+
games = PGN.parse(File.read('./spec/pgn_files/test.pgn'))
|
96
|
+
game = games.first
|
97
|
+
expect(game.tags['Black']).to eq 'Gelfand, Boris'
|
98
|
+
expect(game.result).to eq '1-0'
|
99
|
+
expect(game.moves[13]).to eq 'Nfd7'
|
100
|
+
expect(game.moves[34]).to eq 'f3'
|
101
|
+
expect(game.moves[35].annotation).to eq ['$6']
|
102
|
+
expect(game.moves[35].comment).to eq 'Gelfand decide tomar medidas.'
|
103
|
+
expect(game.moves[35].variations.size).to eq 1
|
104
|
+
variation = game.moves[35].variations[0]
|
105
|
+
expect(variation.size).to eq 2
|
106
|
+
expect(variation[0]).to eq 'exf3'
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should handle files with starting position' do
|
110
|
+
games = PGN.parse(File.read('./spec/pgn_files/fen.pgn'))
|
111
|
+
game = games.first
|
112
|
+
first_pos = game.positions.first
|
113
|
+
last_pos = game.positions.last
|
114
|
+
expect(first_pos.to_fen.to_s).to eq game.tags['FEN']
|
115
|
+
expect(last_pos.to_fen.to_s).to eq '5rkn/5p1p/2b2NpP/8/2B5/1P6/2PP3P/1K6 b - - 0 4'
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'returns original game pgn' do
|
119
|
+
games = PGN.parse(File.read('./spec/pgn_files/fen.pgn'))
|
120
|
+
game = games.first
|
121
|
+
expect(game.pgn).to eq "[Event \"Event 1\"]\n[FEN \"4brkn/4bp1p/3q2pP/8/2B3N1/1P4N1/2PP3P/1K2Q3 w - - 0 1\"]\n[PlyCount \"7\"]\n\n1. Qxe7 Qxe7 2. Ne4 Bc6 3. Nef6+ Qxf6 4. Nxf6# 1-0"
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'returns original game pgn for second game' do
|
125
|
+
games = PGN.parse(File.read('./spec/pgn_files/two_games.pgn'))
|
126
|
+
game = games.last
|
127
|
+
expect(game.pgn).to eq "[White \"Fool 2\"]\n[Black \"Somebody else 2\"]\n\n1. e4 e5 0-1\n"
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'parses empty game' do
|
131
|
+
games = PGN.parse(File.read('./spec/pgn_files/no_moves.pgn'))
|
132
|
+
game = games.first
|
133
|
+
expect { game.positions }.not_to raise_error
|
134
|
+
expect(game.moves).not_to be_empty
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'gets game comment' do
|
138
|
+
games = PGN.parse(File.read('./spec/pgn_files/no_moves.pgn'))
|
139
|
+
game = games.last
|
140
|
+
expect(game.comment).to eq('{game comment}')
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
[White "Somebody"]
|
2
|
+
[Black "Somebody Else"]
|
3
|
+
|
4
|
+
1. d4 e6 2. c4 d5 3. Nc3 c6 4. Nf3 Nf6 5. Bg5 Nbd7 6. e3 Qa5 7. a3 (7. cxd5
|
5
|
+
Nxd5 8. Rc1 {Analyzer's primary variation: 8... Bb4 9. Qd2 e5 10. e4 Nxc3 11.
|
6
|
+
bxc3 Ba3 12. Rb1 h6 13. Be3 O-O 14. Bc4 exd4 15. cxd4 Qxd2+ 16. Nxd2 a5 17. O-O
|
7
|
+
b5 18. Be2 Rd8 19. d5 cxd5 20. exd5 Nc5 21. Rxb5 Rxd5 {+0.30/21} }) 7... Ne4 8.
|
8
|
+
Rc1 Nxg5 9. Nxg5 dxc4 10. Bxc4 Qxg5 11. O-O Nf6 12. Qf3 Qg4 13. Qxg4 Nxg4 14.
|
9
|
+
Ne4 Be7 15. b4 O-O 16. Rce1 Nf6 17. Ng5 ({Better is} 17. Nxf6+ gxf6 {-4.08/31}
|
10
|
+
(17... Bxf6) (17... gxf6)) (17. Nxf6+) 17... h6 18. Nf3 Ne4 ({Treatening} 18...
|
11
|
+
-- 19. Ra1 b5 {-4.05/31}) 19. Bd3 Ng5 *
|
@@ -0,0 +1,18 @@
|
|
1
|
+
% BOOKTITLE = book title
|
2
|
+
[Event "?"]
|
3
|
+
[Date "????.??.??"]
|
4
|
+
[Result "*"]
|
5
|
+
[Annotator "Mumu"]
|
6
|
+
[PlyCount "1"]
|
7
|
+
|
8
|
+
1. -- {Trying to make a long comments in an empty game.it
|
9
|
+
goes in two lines } (1.
|
10
|
+
-- {Just putting something here too.}) *
|
11
|
+
|
12
|
+
[Event "?"]
|
13
|
+
[Date "????.??.??"]
|
14
|
+
[Result "*"]
|
15
|
+
[Annotator "Mumu"]
|
16
|
+
[PlyCount "1"]
|
17
|
+
|
18
|
+
{game comment} *
|
@@ -0,0 +1,55 @@
|
|
1
|
+
[Event "Zurich Chess Challenge"]
|
2
|
+
[Site "Zurich"]
|
3
|
+
[Date "2014.01.30"]
|
4
|
+
[Round "1"]
|
5
|
+
[White "Carlsen, Magnus"]
|
6
|
+
[Black "Gelfand, Boris"]
|
7
|
+
[Result "1-0"]
|
8
|
+
[ECO "D78"]
|
9
|
+
[WhiteElo "2878"]
|
10
|
+
[BlackElo "2777"]
|
11
|
+
[Annotator "Iago Porto"]
|
12
|
+
[PlyCount "73"]
|
13
|
+
[EventDate "2014.01.30"]
|
14
|
+
[EventCountry "SUI"]
|
15
|
+
[SourceDate "2014.01.04"]
|
16
|
+
|
17
|
+
{Esta es la primera partida de Carlsen como campeón del mundo. La voy a
|
18
|
+
comentar para el blog porque me ha gustado una idea bastante inusual que
|
19
|
+
ejecuta el jugador noruego. El análisis no pretende ser exhaustivo ni en ideas
|
20
|
+
ni en variantes, sino tan sólo reflejar las ideas y posiciones que me han
|
21
|
+
llamado la atención. He visto el análisis de Daniel King y estoy claramente
|
22
|
+
influenciado por él. Sin embargo, cualquier error es mío.} 1. c4 g6 2. d4 Nf6
|
23
|
+
3. Nf3 Bg7 4. g3 c6 5. Bg2 d5 6. Qa4 O-O 7. O-O Nfd7 {No me voy a meter en la
|
24
|
+
apertura, no conozco nada de estas líneas, aunque es cierto que esta jugada
|
25
|
+
choca.} 8. Qc2 Nf6 9. Bf4 Bf5 10. Qb3 Qb6 11. Nbd2 Ne4 12. e3 {Hasta este
|
26
|
+
momento, la posición es bastante simétrica. El blanco tiene, en mi opinión,
|
27
|
+
una ligerísima ventaja gracias a tener más presión central, aunque la posición
|
28
|
+
no presenta ningún desbalance estratégico.} Qxb3 13. axb3 {Se ha creado un
|
29
|
+
desbalance en la posición. El blanco obtiene la columna a para su torre a
|
30
|
+
cambio de los peones doblados.} Na6 {Y ahora Carlsen debió de decir: "yo
|
31
|
+
quiero que mi alfil de g2 juegue" y sus siguientes jugadas son implacables
|
32
|
+
para lograr este objetivo.} 14. cxd5 cxd5 15. g4 $1 {Brillante sacrificio de
|
33
|
+
peón.} Bxg4 16. Nxe4 dxe4 17. Nd2 f5 18. f3 {Finalmente se consigue el
|
34
|
+
objetivo, el alfil de g2 juega y el blanco tiene una presión desagradable
|
35
|
+
sobre el ala de dama negra. Me recuerda a un gambito Benko.} e5 $6 {Gelfand
|
36
|
+
decide tomar medidas.} (18... exf3 19. Nxf3 {A pesar de tener un peón de menos,
|
37
|
+
todas las piezas del blanco están muy activas y son mejores que las de su
|
38
|
+
rival. El blanco debería estar mejor.}) 19. dxe5 exf3 20. Nxf3 {Con respecto a
|
39
|
+
la variante anterior, aquí Gelfand devuelve el peón para lograr que las piezas
|
40
|
+
blancas no están tan activas. Considero que el blanco está también mejor y no
|
41
|
+
hay que olvidar que el peón de e5 está pasado.} Rae8 21. Ra5 Nb4 {Tratando de
|
42
|
+
poner el caballo en juego vía c6, donde defenderá a7 y atacará e5.} 22. Nd4 {
|
43
|
+
Evita la amenaza negra de Cc6.} b6 {Gelfand cede ante la presión.} 23. Rxa7
|
44
|
+
Bxe5 {Se mantiene la igualdad de material, pero ahora el rey negro empieza a
|
45
|
+
parecer débil.} 24. Bh6 Rf6 (24... Rf7 25. Ra4 {y cuando el caballo se vaya,
|
46
|
+
Ad5 gana la torre de f7.}) 25. h3 Bh5 {Carlsen encierra un poco más el alfil
|
47
|
+
blanco de negras.} 26. Nc2 $1 {Otra bonita combinación.} g5 (26... Nxc2 27.
|
48
|
+
Bd5+ Kh8 28. Bg7# {En su análisis, Daniel King decía que la torre entra por la
|
49
|
+
banda, pase de la muerte hacia atrás al alfil blanco, el portero la detiene
|
50
|
+
pero queda muerta para que el alfil negro la empuje al jaque mate.}) 27. Bxg5
|
51
|
+
Rg6 28. Rxf5 h6 29. Bxh6 Rxh6 30. Nxb4 $18 {Con material de más, a partir de
|
52
|
+
ahora es cuestión de técnica.} Bxb2 31. Nd5 Kh8 32. Rb7 Bd1 33. b4 Rg8 34. Ne7
|
53
|
+
Rd8 35. Be4 {El alfil se suma al ataque. El rey negro sigue débil.} Bf6 36.
|
54
|
+
Rxb6 Kg7 37. Rf2 $1 {Preciso remate. El caballo de e7 no se puede comer porque}
|
55
|
+
(37. Rf2 Bxe7 38. Rg2+ {y cae la torre de h6.}) 1-0
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PGN::Position do
|
4
|
+
|
5
|
+
describe "start" do
|
6
|
+
|
7
|
+
it "should have fullmove 1" do
|
8
|
+
pos = PGN::Position.start
|
9
|
+
pos.fullmove.should == 1
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
context "disambiguating moves" do
|
15
|
+
describe "using SAN square disambiguation" do
|
16
|
+
pos = PGN::FEN.new("r1bqkb1r/pp1p1ppp/2n1pn2/8/3NP3/2N5/PPP2PPP/R1BQKB1R w KQkq - 3 6").to_position
|
17
|
+
next_pos = pos.move("Ndb5")
|
18
|
+
|
19
|
+
it "should move the specified piece" do
|
20
|
+
next_pos.board.at("d4").should be_nil
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should not move the other piece" do
|
24
|
+
next_pos.board.at("c3").should == "N"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "using discovered check" do
|
29
|
+
pos = PGN::FEN.new("rnbqk2r/p1pp1ppp/1p2pn2/8/1bPP4/2N1P3/PP3PPP/R1BQKBNR w KQkq - 0 5").to_position
|
30
|
+
next_pos = pos.move("Ne2")
|
31
|
+
|
32
|
+
it "should move the piece that doesn't give discovered check" do
|
33
|
+
next_pos.board.at("g1").should be_nil
|
34
|
+
end
|
35
|
+
|
36
|
+
it "shouldn't move the other piece" do
|
37
|
+
next_pos.board.at("c3").should == "N"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "with two pawns on the same file" do
|
42
|
+
pos = PGN::FEN.new("r2q1rk1/4bppp/p3n3/1p2n3/4N3/1B2BP2/PP3P1P/R2Q1RK1 w - - 4 19").to_position
|
43
|
+
next_pos = pos.move("f4")
|
44
|
+
|
45
|
+
it "should move the pawn in front" do
|
46
|
+
next_pos.board.at("f3").should be_nil
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should not move the other pawn" do
|
50
|
+
next_pos.board.at("f2").should == "P"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|