Было бы полезно подобрать Руби для моего нового выступления, поэтому сегодня утром я написал следующее. Он берет PGN-файл с шахматными партиями, в которые я играл, и индексирует их по первому ходу. Буду признателен за любые предложения, как сделать его более «идиоматичным».

Поскольку он не принимает аргументов командной строки (например, для имени файла) и не является объектно-ориентированным, предложения по этим строкам, безусловно, приветствуются.

Имейте в виду, что я создаю индекс всех ходов (а не только первых ходов) из всех игр, потому что я хотел бы в конечном итоге индексировать не только первый ход.

Данные следуют за кодом.

games = []
file = File.new("jemptymethod.pgn", "r")

is_header = false
is_score = false

Game = Struct::new(:header, :score)

while (line = file.gets)
  if !line.chomp.empty?
    if !is_score && !is_header
      game = Game::new('','')
    end
    if /^\[/.match(line)
      is_header = true
      game.header << line
    else
      is_score = true
      game.score << line
    end
  else
    if is_score
      is_score = false
      is_header = false
      games << game
    end
  end
end

file.close
puts "# Games: " + games.length.to_s
moves_index = {}
first_moves = {}

games.each { |gm|
  #the following output should essentially be lossless
  #with the possible exception of beginning or ending newlines
  #
  #puts gm.header + "\n"
  #puts gm.score + "\n"

  score_tokens = gm.score.split(/\s+/);
  game_moves = []

  score_tokens.each_index{|i|
    if i%3 != 0
      move_token = score_tokens[i]
      if !moves_index.has_key?(move_token)
        moves_index[move_token] = moves_index.keys.length
      end
      game_moves << moves_index[move_token]
    end
  }

  first_move = moves_index.index(game_moves[0])

  if !first_moves.has_key?(first_move)
    first_moves[first_move] = 1
  else
    first_moves[first_move] = 1 + first_moves[first_move]
  end
}

# sorting hashes by value: http://nhw.pl/wp/2007/06/11/sorting-hash-by-values
first_moves.sort{|a,b| -1*(a[1]<=>b[1])}.each{|k,v|
  puts "1. #{k} occurred #{v} times" 
}

Данные (всего 3 игры, я работал с 25):

[Event "Enough With the Draws Already ;)"]
[Site "http://www.queenalice.com/game.php?id=533406"]
[Date "2009.2.1"]
[Round "-"]
[White "Troy"]
[Black "jemptymethod"]
[Result "1/2-1/2"]
[WhiteElo "1300"]
[BlackElo "2076"]
[ECO "C36"]

1. e4 e5 2. f4 exf4 3. Nf3 Be7 4. Bc4 Nf6 5. Qe2 d5 6. exd5 Nxd5 7. O-O Be6 8.
d4 Nc6 9. Nc3 O-O 10. Nxd5 Bxd5 11. Bxd5 Qxd5 12. Bxf4 Bd6 13. Qd2 Rae8 14. Bxd6
Qxd6 15. Rae1 h6 16. c3 Qd5 17. b3 Qa5 18. h3 a6 19. Rf2 Re7 20. Rxe7 Nxe7 21.
Ne5 Nd5 22. c4 Qxd2 1/2-1/2

[Event "AUTO-MASTER-620"]
[Site "http://www.queenalice.com/game.php?id=545265"]
[Date "2009.2.23"]
[Round "2"]
[White "testouverture"]
[Black "jemptymethod"]
[Result "1/2-1/2"]
[WhiteElo "2240"]
[BlackElo "2179"]
[ECO "A52"]

1. d4 Nf6 2. c4 e5 3. dxe5 Ng4 4. Nf3 Bc5 5. e3 Nc6 6. Be2 O-O 7. O-O Re8 8. b3
Ngxe5 9. Bb2 Nxf3+ 10. Bxf3 Ne5 11. Nc3 a5 12. Ne4 Bf8 13. Bh5 Ra6 14. f4 Ng6
15. Ng5 d5 16. Nxf7 Kxf7 17. f5 Kg8 18. fxg6 hxg6 19. Qd4 Qe7 20. Bf3 dxc4 21.
Qxc4+ Be6 22. Qc3 c6 23. Be2 Raa8 24. Bd3 Bf5 25. Bxf5 gxf5 26. Rf3 Qc5 27. Re1
Qxc3 28. Bxc3 g6 29. g4 Bg7 30. Bxg7 fxg4 31. Rg3 Kxg7 32. Rxg4 Rad8 33. Kf2
1/2-1/2

[Event "AUTO-MASTER-620"]
[Site "http://www.queenalice.com/game.php?id=545266"]
[Date "2009.2.23"]
[Round "2"]
[White "jemptymethod"]
[Black "testouverture"]
[Result "0-1"]
[WhiteElo "2079"]
[BlackElo "2306"]
[ECO "B22"]

1. e4 c5 2. c3 d5 3. exd5 Qxd5 4. d4 Nc6 5. dxc5 Qxd1+ 6. Kxd1 e5 7. Be3 Nf6 8.
b4 a5 9. b5 Ne7 10. Nf3 Ng4 11. Bc4 Nf5 12. Ke2 Nfxe3 13. fxe3 Bxc5 14. h3 Nxe3
15. Nxe5 f6 0-1
0
Dexygen 19 Дек 2009 в 17:00
Я не согласен с тем, чтобы отчитывать кого-то, кто плохо знаком с языком, за отзывы о коде. У него есть рабочий код, поэтому он не просит кого-то сделать за него работу, он просто ищет лучшие способы, и это здорово. Если вы собираетесь предложить ему поискать в другом месте, по крайней мере, предложите где.
 – 
mmrobins
19 Дек 2009 в 21:02
Да, это тоже не похоже на то, что он только что подписался на SO - он сделал свою долю «бесплатной» работы для других.
 – 
Alex Reisner
19 Дек 2009 в 22:35
И Alex R, я ценю ответы, они могут занять некоторое время, чтобы их переварить.
 – 
Dexygen
19 Дек 2009 в 23:55

2 ответа

Лучший ответ

Я не провел полного рефакторинга, потому что хочу, чтобы ваш исходный код оставался нетронутым, чтобы он не слишком запутался. Основное изменение - это введение класса Game, который обрабатывает синтаксический анализ. Реализация этого класса может быть значительно улучшена, но он работает без особых изменений вашего кода. Также несколько мелких моментов:

  • Вместо File.new прочтите файл с помощью File.open и дайте ему блок, который принимает параметр file. Файл автоматически закрывается в конце блока.

  • Использование a += 1 вместо a = a + 1.

Я составил простую нотацию и написал синтаксический анализатор для обработки деталей теннисного матча от игры к игре . Возможно, вы захотите взглянуть на этот код в качестве примера анализа игровых ходов. На самом деле это очень похоже на то, что вы делаете. Основная часть кода находится в каталоге /lib. Логика синтаксического анализа находится в parser.rb, а игровые компоненты - в других файлах. Я бы посоветовал вам разделить ваши шахматные партии аналогичным образом, добавив класс Move.

Во всяком случае, вот мой полурефакторинг вашего кода:

class Game
  attr_accessor :header, :score, :moves

  def initialize
    @header = ""
    @score  = ""
    @moves  = []
  end

  def first_move
    moves_index.index(moves[0])
  end

  def moves_index
    moves_index = {}
    score.split(/\s+/).each_with_index do |move,i|
      if i%3 != 0
        unless moves_index.has_key?(move)
          moves_index[move] = moves_index.keys.length
        end
        moves << moves_index[move]
      end
    end
    moves_index
  end
end

games     = []
is_header = false
is_score  = false

File.open("jemptymethod.pgn") do |file|
  while (line = file.gets)
    if !line.chomp.empty?
      if !is_score && !is_header
        game = Game.new
      end
      if line[0,1] == '['
        is_header = true
        game.header << line
      else
        is_score = true
        game.score << line
      end
    elsif is_score
      is_score = false
      is_header = false
      games << game
    end
  end
end

puts "# Games: " + games.length.to_s
first_moves = {}

#the following output should essentially be lossless
#with the possible exception of beginning or ending newlines
#
#puts gm.header + "\n"
#puts gm.score + "\n"
games.each do |gm|
  if !first_moves.has_key?(gm.first_move)
    first_moves[gm.first_move] = 1
  else
    first_moves[gm.first_move] += 1
  end
end

# sorting hashes by value: http://nhw.pl/wp/2007/06/11/sorting-hash-by-values
first_moves.sort{|a,b| -1*(a[1]<=>b[1])}.each{|k,v|
  puts "1. #{k} occurred #{v} times" 
}
1
Alex Reisner 19 Дек 2009 в 21:52
Я принимаю этот, потому что он дает тот же результат, что и моя исходная программа.
 – 
Dexygen
21 Дек 2009 в 15:13

Вот быстрое решение, как я бы это сделал. Здесь может быть много чего переварить, поэтому не стесняйтесь задавать вопросы, но читая Ruby Array или Enumerable должны ответить на большинство из них о некоторых вещах, которые я делал, и есть масса хороших руководств по рубиновым классам. Вот хороший пример для понимания аксессуаров, которые я использовал в этом классе вместо этого. структуры.

class Game
  attr_accessor :header, :moves
  def initialize
    self.header = []
  end
end

games = []
game = Game.new
File.open('jemptymethod.pgn').each_line do |line|
  next if line.chomp.empty?
  if game.moves
    games << game
    game = Game.new
  end
  if /^\[/.match(line)
    game.header << line
  else
    moves = line.split(/\d+\.\s*/) # splitting on the move numbers so that we don't have to iterate through to remove them
    moves.shift # getting rid of first empty move since the split on '1. ' created an array element before the '1. '
    game.moves = moves
  end
end
games << game # add last game since the first part of the file loop doesn't execute again to do it

puts "# Games: " + games.length.to_s

first_moves = games.map {|game| game.moves[0]} # Could easily iterate over the size of the longest game to get other moves (eg second move, etc)
first_moves_count = first_moves.inject(Hash.new(0)) {|h, move| h[move] += 1; h} # Read ruby documentation on inject to see how this works
first_moves_count.each do |move, count|
  puts "1. #{move} occurred #{count} times"
end
2
mmrobins 19 Дек 2009 в 21:06