eco-classifier 0.5

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.
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $:.unshift(lib) unless $:.include?(lib)
5
+
6
+ require 'eco_classifier/version'
7
+
8
+ Gem::Specification.new do |s|
9
+ s.name = "eco-classifier"
10
+ s.version = EcoClassifier::VERSION
11
+ s.authors = ["Linmiao Xu"]
12
+ s.email = ["lin@robotmoon.com"]
13
+ s.homepage = "https://github.com/linrock/eco-classifier"
14
+
15
+ s.summary = "Classifies chess openings from a list of opening moves"
16
+ s.description = "Classifies chess openings from a list of opening moves using the SCID ECO data file"
17
+ s.license = "MIT"
18
+
19
+ s.files = `git ls-files -z`.split("\0")
20
+
21
+ s.add_development_dependency "rake", "~> 10.0"
22
+ s.add_development_dependency "minitest", "~> 5.0"
23
+ s.add_development_dependency "pry", "~> 0.10"
24
+ end
@@ -0,0 +1,20 @@
1
+ require "eco_classifier/eco_data_file_parser"
2
+ require "eco_classifier/opening_tree"
3
+ require "eco_classifier/version"
4
+
5
+ module EcoClassifier
6
+ def self.opening_tree
7
+ return @@opening_tree if defined? @@opening_tree
8
+ @@opening_tree = OpeningTree.new
9
+ @@opening_tree.generate!
10
+ @@opening_tree
11
+ end
12
+
13
+ def self.classify_moves(moves)
14
+ opening_tree.get_opening moves
15
+ end
16
+
17
+ def self.classify_pgn(pgn)
18
+ opening_tree.get_opening_from_pgn pgn
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # Parses the scid.eco data file into openings
2
+ #
3
+ class EcoDataFileParser
4
+ ROW_SCANNER = /\A(?<eco>[A-E]\d{2}[a-z]?)\s+"(?<name>.*)"\s+(?<pgn>.*)\z/
5
+
6
+ attr_accessor :openings
7
+
8
+ def initialize(eco_file)
9
+ @text = open(eco_file, 'r').read.strip
10
+ @text = @text.gsub(/\n /, ' ')
11
+ @openings = []
12
+ end
13
+
14
+ def scan_into_openings
15
+ @text.split(/\n/).each do |row|
16
+ match = row.match(ROW_SCANNER)
17
+ next unless match
18
+ @openings << Opening.new(match)
19
+ end
20
+ @openings
21
+ end
22
+ end
@@ -0,0 +1,54 @@
1
+ # Represents an opening as parsed from scid.eco
2
+ #
3
+ class Opening
4
+
5
+ attr_accessor :eco, :name, :pgn
6
+
7
+ def initialize(options)
8
+ self.eco = options[:eco]
9
+ self.name = options[:name]
10
+ self.pgn = options[:pgn]
11
+ end
12
+
13
+ def base_eco
14
+ self.eco[/\A([A-E]\d{2})/, 1]
15
+ end
16
+
17
+ def base_name
18
+ self.name.split(":").first
19
+ end
20
+
21
+ def variation
22
+ return @variation if defined?(@variation)
23
+ match = self.name[/:(.*)/, 1]
24
+ return unless match
25
+ @variation = match.strip
26
+ end
27
+
28
+ def variation_name
29
+ return unless variation
30
+ return @variation_name if defined?(@variation_name)
31
+ @variation_name =
32
+ variation.split(",").map(&:strip).select {|str| str[0] !~ /\d/ }.join(", ")
33
+ end
34
+
35
+ def variation_line
36
+ return unless variation
37
+ return @variation_line if defined?(@variation_line)
38
+ @variation_line =
39
+ variation.split(",").map(&:strip).select {|str| str[0] =~ /\d/ }.join(", ")
40
+ end
41
+
42
+ def move_list
43
+ pgn.gsub(/\d+\./, '').gsub(/\*/, '').strip.split(/\s+/)
44
+ end
45
+
46
+ def as_json(options = {})
47
+ {
48
+ eco: base_eco,
49
+ name: base_name,
50
+ variation: variation,
51
+ full_name: self.name
52
+ }
53
+ end
54
+ end
@@ -0,0 +1,90 @@
1
+ # Used for fast opening lookups, given a move list
2
+ #
3
+ require 'eco_classifier/opening'
4
+
5
+ class OpeningTree
6
+ DATA_FILE = File.join(File.dirname(__FILE__), '../../data/scid.eco')
7
+
8
+ # children -> maps move names to more nodes
9
+ #
10
+ class Node
11
+ attr_accessor :opening, :children
12
+
13
+ def initialize
14
+ @children = {}
15
+ end
16
+
17
+ def set_opening(opening)
18
+ @opening = opening
19
+ end
20
+
21
+ def is_leaf?
22
+ @children.size == 0
23
+ end
24
+
25
+ def inspect
26
+ if @opening
27
+ %(
28
+ <Node @eco="#{@opening.eco}"
29
+ @name="#{@opening.name}"
30
+ @n_children=#{@children.size}>
31
+ ).gsub(/\s+/, ' ')
32
+ else
33
+ %(<Node @n_children=#{@children.size}>)
34
+ end
35
+ end
36
+ end
37
+
38
+ attr_accessor :root, :size
39
+
40
+ def initialize
41
+ @root = Node.new
42
+ @size = 0
43
+ end
44
+
45
+ def build_tree(openings)
46
+ openings.each do |opening|
47
+ current = @root
48
+ moves = opening.move_list
49
+ while (move = moves.shift)
50
+ if !current.children[move]
51
+ current.children[move] = Node.new
52
+ end
53
+ current = current.children[move]
54
+ end
55
+ current.set_opening opening
56
+ @size += 1
57
+ end
58
+ end
59
+
60
+ def generate!
61
+ openings = EcoDataFileParser.new(DATA_FILE).scan_into_openings
62
+ build_tree(openings)
63
+ end
64
+
65
+ def search_for_opening(move_list)
66
+ current = @root
67
+ last_opening = nil
68
+ while (move = move_list.shift)
69
+ break unless current.children[move]
70
+ current = current.children[move]
71
+ last_opening = current.opening if current.opening
72
+ end
73
+ {
74
+ opening: last_opening,
75
+ search_done: current.is_leaf? || move_list.length > 0
76
+ }
77
+ end
78
+
79
+ def get_opening(move_list)
80
+ search_for_opening(move_list)[:opening]
81
+ end
82
+
83
+ def get_opening_from_pgn(pgn)
84
+ get_opening pgn.gsub(/\d+\./, '').gsub(/\*/, '').strip.split(/\s+/)
85
+ end
86
+
87
+ def inspect
88
+ %(<OpeningTree @size=#{@size}>)
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ module EcoClassifier
2
+ VERSION = "0.5"
3
+ end
@@ -0,0 +1,28 @@
1
+ require 'test_helper'
2
+
3
+ class EcoClassifierTest < Minitest::Test
4
+
5
+ def setup
6
+ @klass = EcoClassifier
7
+ end
8
+
9
+ def test_classifier_detects_queens_pawn_game
10
+ opening = @klass.classify_moves %w( d4 d5 )
11
+ assert opening.name == "Queen's Pawn Game"
12
+ end
13
+
14
+ def test_classifier_detects_kings_gambit
15
+ opening = @klass.classify_moves %w( e4 e5 f4)
16
+ assert opening.name == "King's Gambit"
17
+ end
18
+
19
+ def test_classifier_detects_queens_gambit
20
+ opening = @klass.classify_moves %w( d4 d5 c4)
21
+ assert opening.name == "Queen's Gambit"
22
+ end
23
+
24
+ def test_classifier_detects_ruy_lopez
25
+ opening = @klass.classify_moves %w( e4 e5 Nf3 Nc6 Bb5 )
26
+ assert opening.name == "Spanish (Ruy Lopez)"
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ gem 'minitest'
2
+ require 'minitest/autorun'
3
+
4
+ $:.unshift File.expand_path('../../lib', __FILE__)
5
+ require 'eco_classifier'
6
+
7
+ def read_fixture(path)
8
+ open("#{File.dirname(__FILE__)}/fixtures/#{path}").read
9
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eco-classifier
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.5'
5
+ platform: ruby
6
+ authors:
7
+ - Linmiao Xu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '10.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '10.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.10'
55
+ description: Classifies chess openings from a list of opening moves using the SCID
56
+ ECO data file
57
+ email:
58
+ - lin@robotmoon.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - Gemfile
64
+ - Gemfile.lock
65
+ - README.md
66
+ - Rakefile
67
+ - bin/console
68
+ - data/scid.eco
69
+ - eco-classifier.gemspec
70
+ - lib/eco_classifier.rb
71
+ - lib/eco_classifier/eco_data_file_parser.rb
72
+ - lib/eco_classifier/opening.rb
73
+ - lib/eco_classifier/opening_tree.rb
74
+ - lib/eco_classifier/version.rb
75
+ - test/eco_classifier_test.rb
76
+ - test/test_helper.rb
77
+ homepage: https://github.com/linrock/eco-classifier
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 2.7.6
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Classifies chess openings from a list of opening moves
101
+ test_files: []