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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +122 -0
- data/Rakefile +1 -0
- data/TODO.md +4 -0
- data/examples/immortal_game.pgn +17 -0
- data/lib/pgn.rb +22 -0
- data/lib/pgn/board.rb +183 -0
- data/lib/pgn/fen.rb +146 -0
- data/lib/pgn/game.rb +82 -0
- data/lib/pgn/move.rb +167 -0
- data/lib/pgn/move_calculator.rb +339 -0
- data/lib/pgn/parser.rb +119 -0
- data/lib/pgn/position.rb +129 -0
- data/lib/pgn/version.rb +3 -0
- data/pgn.gemspec +26 -0
- data/spec/fen_spec.rb +87 -0
- data/spec/game_spec.rb +13 -0
- data/spec/parser_spec.rb +14 -0
- data/spec/position_spec.rb +44 -0
- data/spec/spec_helper.rb +19 -0
- metadata +129 -0
    
        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
         | 
    
        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 | 
            +
              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
         | 
    
        data/lib/pgn/version.rb
    ADDED
    
    
    
        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
         | 
    
        data/spec/parser_spec.rb
    ADDED
    
    | @@ -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
         |