Class: ChessData::Game

Inherits:
Object
  • Object
show all
Defined in:
lib/chess_data/game.rb

Overview

Represents a chess game, as read in from a PGN file.

Header information in a pgn is stored in a hash table, and method_missing used to provide an accessor-like mechanism for storing/retrieving header information. For example:

db = Database.new
db.add_games_from "test/data/fischer.pgn"
game = db[0]
puts game.event
puts game.white
puts game.black

Each of the ‘event’, ‘white’, ‘black’ values are retrieved from the PGN header.

New key values can be created and assigned to, to construct a header for a game. For example:

db = Database.new
game = Game.new
game.white = "Peter"
game.black = "Paul"
game.result = "1-0"
game << "e4"
db << game
db.to_file "mygames.pgn"

And the pgn file will contain:

[Result "1-0"]
[White "Peter"]
[Black "Paul"]

1. e4  1-0

Constant Summary collapse

MatchHeader =

Regular expression used to match a PGN header line.

/\[(\w+) \"(.*)\"\]/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeGame

Returns a new instance of Game.



44
45
46
47
# File 'lib/chess_data/game.rb', line 44

def initialize
  @header = {}
  @moves = []
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(iname, *args) ⇒ Object

method_missing is used for accessing key-value terms in the header.

  • Any unknown method call is checked if it is the key of a header item and, if so, the value for that key is returned.

  • If the unknown method has an ‘=’ sign in it, a new key is created and assigned a value, which must be an argument to method.



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/chess_data/game.rb', line 54

def method_missing iname, *args
  name = iname.to_s 
  if @header.has_key? name
    @header[name]
  elsif name.include? "=" # assign, so create new key
    key = name.delete "="
    @header[key] = args[0]
  else
    puts "Unknown key '#{name}' for header #{@header}"
    super 
  end
end

Instance Attribute Details

#movesObject

Stores the sequence of moves in the game.



42
43
44
# File 'lib/chess_data/game.rb', line 42

def moves
  @moves
end

Class Method Details

.from_pgn(stream) ⇒ Object

Reads a single game from a given IO stream. Returns nil if failed to read a game or its moves.



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/chess_data/game.rb', line 143

def Game.from_pgn stream
  game = Game.new
  moves = []
  # ignore blank lines
  begin
    line = stream.gets
    return nil if line.nil? # failed to read game/empty file
  end while line.strip.empty?
  # read the header
  while MatchHeader =~ line
    game.send "#{$1.downcase}=", $2
    line = stream.gets.strip
  end
  # ignore blank lines
  begin
    line = stream.gets
    return nil if line.nil? # failed to read moves for game
  end while line.strip.empty?
  # read the moves
  begin
    next if line.start_with? "%"
    semi_index = line.index ";" # look for ; comment start
    line = line[0...semi_index] if semi_index # and strip it
    moves << line.strip 
    line = stream.gets
  end until line.nil? || line.strip.empty? || MatchHeader =~ line
  # return an event line if it immediately follows the moves
  # so can be read for next game
  stream.ungetc line if MatchHeader =~ line
  # parse the moves and add to game
  move_str = moves.join(" ")
  if /{.*}/.match(move_str) # remove { } comments
    move_str = $` + " " + $'
  end
  move_str.split(" ").each do |token|
    case token
    when "1-0", "0-1", "1/2-1/2", "*" then game.result = token
    when /^\d+/ then ; # ignore the move number
    when /^$\d+/ then ; # ignore NAG
    when Moves::LegalMove then game << Moves.new_move(token)
    end
  end

  return game
end

Instance Method Details

#<<(move) ⇒ Object

Append given move to list of moves. Given move can be a valid move type or a string.

An InvalidMoveError is raised if the move is not valid.



72
73
74
75
76
77
78
79
80
# File 'lib/chess_data/game.rb', line 72

def << move
  if move.respond_to? :make_move
    @moves << move
  elsif move.kind_of? String
    @moves << Moves.new_move(move)
  else
    raise InvalidMoveError.new("Invalid type of move: #{move}")
  end
end

#half_movesObject

Return the number of half-moves in the game.



83
84
85
# File 'lib/chess_data/game.rb', line 83

def half_moves 
  @moves.size
end

#play_game {|board, result| ... } ⇒ Object

Step through the game from start position, one half-move at a time. Yields to a block the current board position and the next move. Yields final board position and result at end of game.

Yields:

  • (board, result)


100
101
102
103
104
105
106
107
# File 'lib/chess_data/game.rb', line 100

def play_game
  board = start_position
  @moves.each do |move|
    yield board, move
    board = move.make_move board
  end
  yield board, result
end

#search(&block) ⇒ Object

Test if game meets the position definition given in the block using Game#play_game to step through the game.



111
112
113
114
115
116
117
# File 'lib/chess_data/game.rb', line 111

def search &block
  defn = ChessData::PositionDefinition.new(&block)
  play_game do |board|
    return true if defn.check board
  end
  return false
end

#start_positionObject

Return the start position for the game. PGN games may provide a start-position, if they do not begin from the start position.



89
90
91
92
93
94
95
# File 'lib/chess_data/game.rb', line 89

def start_position
  if @header.has_key? "fen"
    ChessData::Board.from_fen @header["fen"]
  else
    ChessData::Board.start_position
  end
end

#to_pgn(stream) ⇒ Object

Write game in PGN format to given IO stream. This method is usually called from Database#to_file but can also be called directly.



123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/chess_data/game.rb', line 123

def to_pgn stream
  @header.keys.each do |key|
    stream.puts "[#{key.capitalize} \"#{@header[key]}\"]"
  end
  stream.puts # blank separating line
  move_str = ""
  move_number = 1
  @moves.each_slice(2) do |full_move|
    move_str += "#{move_number}. #{full_move[0]} #{full_move[1]} "
    move_number += 1
  end
  move_str += result
  stream.puts WordWrap.ww(move_str, 80)
end