hearthstone 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +43 -0
  6. data/Rakefile +2 -0
  7. data/bin/hearthstone-recorder +7 -0
  8. data/hearthstone.gemspec +28 -0
  9. data/lib/hearthstone.rb +7 -0
  10. data/lib/hearthstone/commands/recorder_command.rb +86 -0
  11. data/lib/hearthstone/log.rb +7 -0
  12. data/lib/hearthstone/log/configurator.rb +41 -0
  13. data/lib/hearthstone/log/data.rb +1 -0
  14. data/lib/hearthstone/log/data/game.rb +74 -0
  15. data/lib/hearthstone/log/data/game_player.rb +25 -0
  16. data/lib/hearthstone/log/data/game_turn.rb +32 -0
  17. data/lib/hearthstone/log/game_logger.rb +128 -0
  18. data/lib/hearthstone/log/parser.rb +276 -0
  19. data/lib/hearthstone/models.rb +3 -0
  20. data/lib/hearthstone/models/AllSetsAllLanguages.json +1 -0
  21. data/lib/hearthstone/models/card.rb +20 -0
  22. data/lib/hearthstone/models/card_store.rb +50 -0
  23. data/lib/hearthstone/models/entity.rb +67 -0
  24. data/lib/hearthstone/models/game.rb +151 -0
  25. data/lib/hearthstone/models/game_loader.rb +52 -0
  26. data/lib/hearthstone/models/player.rb +37 -0
  27. data/lib/hearthstone/version.rb +3 -0
  28. data/spec/fixtures/completed.config +8 -0
  29. data/spec/fixtures/empty_config.config +0 -0
  30. data/spec/fixtures/game1.json +2433 -0
  31. data/spec/fixtures/gamelog1.json +1585 -0
  32. data/spec/fixtures/gamelog1.log +25508 -0
  33. data/spec/fixtures/gamelog2.log +85357 -0
  34. data/spec/hearthstone/log/configurator_spec.rb +61 -0
  35. data/spec/hearthstone/log/game_logger_spec.rb +57 -0
  36. data/spec/hearthstone/log/parser_spec.rb +185 -0
  37. data/spec/hearthstone/models/card_store_spec.rb +16 -0
  38. data/spec/hearthstone/models/game_loader_spec.rb +153 -0
  39. data/spec/hearthstone/models/game_spec.rb +157 -0
  40. metadata +179 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6688c037c7bb15f4b7af8a2b6229c1862d227496
4
+ data.tar.gz: 03f39b68809b08190ee873aa82b3fae9d77bc2c3
5
+ SHA512:
6
+ metadata.gz: 7279bb27c93d6944e1110c38c38241632ca61466f51544649d3de57fe3abd64faa97688b666d9836dd98d092e7d402167770ebcde4bceb5f35960169b6e2f92b
7
+ data.tar.gz: d0a7adce7b9fec83076513a41f5c20abb7e046b0639d465c8f912b5a4ebf6c462801a29ce784bb2b6fbb9521c2466e2de8ce8f648574850dba499d67fd752e3c
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hearthstone.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Francis Chong
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,43 @@
1
+ # Hearthstone
2
+
3
+ Utlities for Hearthstone.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'hearthstone'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install hearthstone
20
+
21
+ ## Usage
22
+
23
+ To record a game, run following command:
24
+
25
+ $ hearthstone-recorder
26
+
27
+ Start hearthstone, any games will be saved to ~/Documents/hearthstone-recorder/
28
+
29
+ ## Attributions
30
+
31
+ Card names and text are all copyright Blizzard Entertainment.
32
+
33
+ This app is not affiliated with Blizzard Entertainment in any way.
34
+
35
+ Used data from [hearthstonejson.com](http://hearthstonejson.com/).
36
+
37
+ ## Contributing
38
+
39
+ 1. Fork it ( https://github.com/[my-github-username]/hearthstone/fork )
40
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
41
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
42
+ 4. Push to the branch (`git push origin my-new-feature`)
43
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'claide'
4
+
5
+ require_relative '../lib/hearthstone'
6
+
7
+ RecorderCommand.run(ARGV)
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hearthstone/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hearthstone"
8
+ spec.version = Hearthstone::VERSION
9
+ spec.authors = ["Francis Chong"]
10
+ spec.email = ["francis@ignition.hk"]
11
+ spec.summary = %q{Utlities for Hearthstone.}
12
+ spec.description = %q{Utlities for Hearthstone. First, a game recorder.}
13
+ spec.homepage = "https://github.com/siuying/hearthstone"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
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_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 3.1"
24
+ spec.add_development_dependency "pry", "~> 0"
25
+
26
+ spec.add_dependency "claide", "~> 0.7"
27
+ spec.add_dependency "file-tail", "~> 1.0"
28
+ end
@@ -0,0 +1,7 @@
1
+ require_relative "./hearthstone/version"
2
+
3
+ module Hearthstone
4
+ end
5
+
6
+ require_relative "./hearthstone/log"
7
+ require_relative "./hearthstone/commands/recorder_command"
@@ -0,0 +1,86 @@
1
+ require 'claide'
2
+ require 'json'
3
+
4
+ require 'fileutils'
5
+ require 'file/tail'
6
+
7
+ require_relative '../log'
8
+
9
+ class RecorderCommand < CLAide::Command
10
+ attr_reader :input, :output, :config, :complete
11
+
12
+ self.description = 'Record hearthstone games.'
13
+ self.command = 'hearthstone-recorder'
14
+ self.arguments = []
15
+
16
+ def self.options
17
+ [
18
+ ['--input-path', 'File path of the Hearthstone log files.'],
19
+ ['--config-path', 'Hearthstone config file path. (Default: ~/Library/Preferences/Blizzard/Hearthstone/log.config)'],
20
+ ['--output-path', 'File path of the output record files. (Default: ~/Documents/hearthstone-recorder)'],
21
+ ['--complete', 'Read the log file completely and re-create every games. Default: only watch for new games.']
22
+ ].concat(super)
23
+ end
24
+
25
+ def initialize(argv)
26
+ @input = argv.option('input-path', "#{Dir.home}/Library/Logs/Unity/Player.log")
27
+ @output = argv.option('output-path', "#{Dir.home}/Documents/hearthstone-recorder")
28
+ @config = argv.option('config-path', "#{Dir.home}/Library/Preferences/Blizzard/Hearthstone/log.config")
29
+ @complete = argv.flag?('complete', false)
30
+ super
31
+ end
32
+
33
+ def validate!
34
+ super
35
+
36
+ unless Dir.exists? self.output
37
+ Dir.mkdir(self.output)
38
+ end
39
+
40
+ configurator = Hearthstone::Log::Configurator.new(self.config)
41
+ if configurator.needs_config?
42
+ puts "Configure Hearthstone, if hearthstone is running, please restart it. (#{self.config})"
43
+ configurator.configure
44
+ end
45
+ end
46
+
47
+ def run
48
+ logger = Hearthstone::Log::GameLogger.new(self)
49
+
50
+ if self.complete
51
+ logger.log_file(File.open(self.input).read)
52
+ end
53
+
54
+ params = {}
55
+ params[:backward] = 10 unless self.complete
56
+
57
+ File::Tail::Logfile.open(self.input, params) do |file|
58
+ begin
59
+ file.interval = 10
60
+ file.tail do |line|
61
+ logger.log_line(line)
62
+ end
63
+ rescue Interrupt
64
+ end
65
+ end
66
+ end
67
+
68
+ # when game over, write the logs to output folder
69
+ def on_game_over(game)
70
+ json = game.to_json
71
+ filename = game.filename + ".json"
72
+
73
+ puts "Game recorded: #{game.players.first.name} vs #{game.players.last.name}"
74
+ File.open(File.join(self.output, filename), "w") do |f|
75
+ f.write(json)
76
+ end
77
+ end
78
+
79
+ def on_game_mode(mode)
80
+ puts "Game mode detected: #{mode}"
81
+ end
82
+
83
+ def on_event(event, data)
84
+ # puts "event: #{event.to_s} -> #{data}"
85
+ end
86
+ end
@@ -0,0 +1,7 @@
1
+ module Hearthstone
2
+ module Log
3
+ end
4
+ end
5
+
6
+ require_relative "./log/game_logger"
7
+ require_relative "./log/configurator"
@@ -0,0 +1,41 @@
1
+ module Hearthstone
2
+ module Log
3
+ class Configurator
4
+ attr_reader :path
5
+
6
+ def initialize(path="~/Library/Preferences/Blizzard/Hearthstone/log.config")
7
+ @path = path
8
+ end
9
+
10
+ def needs_config?
11
+ unless File.exists?(path)
12
+ return true
13
+ end
14
+
15
+ data = File.read(path)
16
+ !(data =~ /\[Zone\]/ && data =~ /\[Power\]/)
17
+ end
18
+
19
+ def configure
20
+ config = """[Zone]
21
+ LogLevel=1
22
+ FilePrinting=false
23
+ ConsolePrinting=true
24
+ ScreenPrinting=false
25
+
26
+ [Power]
27
+ LogLevel=1
28
+ ConsolePrinting=true
29
+ """
30
+ mode = "w"
31
+ if File.exists?(path)
32
+ mode = "a"
33
+ end
34
+
35
+ File.open(path, mode) do |f|
36
+ f.write(config)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1 @@
1
+ require_relative "./data/game"
@@ -0,0 +1,74 @@
1
+ require_relative "./game_turn"
2
+ require_relative "./game_player"
3
+ require 'json'
4
+
5
+ module Hearthstone
6
+ module Log
7
+ class Game
8
+ attr_accessor :mode
9
+ attr_reader :turns, :players, :results
10
+
11
+ def initialize(mode)
12
+ @mode = mode
13
+
14
+ @results = {}
15
+ @players = []
16
+ @turns = []
17
+
18
+ add_turn(number: 0, player: nil, timestamp: nil)
19
+ end
20
+
21
+ # proceed to next turn
22
+ def add_turn(number: nil, player: nil, timestamp: nil)
23
+ @turns << GameTurn.new(number: number, player: player, timestamp: timestamp)
24
+ end
25
+
26
+ # current turn
27
+ def current_turn
28
+ @turns.last
29
+ end
30
+
31
+ # create or get the player, with given id or name
32
+ def player_with_id_or_name(id: nil, name: nil)
33
+ player = players.detect {|p| (id && p.id == id) || (name && p.name == name) }
34
+ unless player
35
+ player = GamePlayer.new(name: name, id: id)
36
+ players << player
37
+ end
38
+ player
39
+ end
40
+
41
+ # if the game is completed
42
+ def completed?
43
+ self.results.count == 2
44
+ end
45
+
46
+ # convert the game into hash
47
+ def to_hash
48
+ players_hash = players.collect(&:to_hash)
49
+ turns_hash = turns.collect(&:to_hash)
50
+
51
+ {
52
+ mode: mode,
53
+ players: players_hash,
54
+ turns: turns_hash,
55
+ results: results
56
+ }
57
+ end
58
+
59
+ # convert the game into JSON
60
+ def to_json
61
+ JSON.pretty_generate(to_hash)
62
+ end
63
+
64
+ # A unique filename for this Game
65
+ def filename
66
+ timestamp = turns.detect {|t| t.timestamp != nil }.timestamp rescue Time.now.to_i
67
+ time = Time.at(timestamp).strftime("%Y%m%d_%H%M%S")
68
+ player1 = players.first.name
69
+ player2 = players.last.name
70
+ "#{time}_#{mode}_#{player1}_vs_#{player2}"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,25 @@
1
+ module Hearthstone
2
+ module Log
3
+ class GamePlayer
4
+ attr_accessor :id, :name, :first_player
5
+ attr_accessor :hero, :hero_power
6
+
7
+ def initialize(id: nil, name: nil)
8
+ @name = name
9
+ @id = id
10
+ @first_player = false
11
+ end
12
+
13
+ def to_hash
14
+ {
15
+ id: id,
16
+ name: name,
17
+ first_player: first_player,
18
+ hero: hero,
19
+ hero_power: hero_power
20
+ }
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ module Hearthstone
2
+ module Log
3
+ class GameTurn
4
+ attr_accessor :number, :player, :timestamp
5
+ attr_reader :events
6
+
7
+ def initialize(number: nil, player: nil, timestamp: nil)
8
+ @events = []
9
+ @number = number
10
+ @player = player
11
+ @timestamp = timestamp
12
+ end
13
+
14
+ def add_event(event, data, line=nil)
15
+ if line
16
+ @events.push([event, data, line])
17
+ else
18
+ @events.push([event, data])
19
+ end
20
+ end
21
+
22
+ def to_hash
23
+ {
24
+ number: number,
25
+ player: player,
26
+ timestamp: timestamp,
27
+ events: events
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,128 @@
1
+ require_relative "./parser"
2
+ require_relative "./data"
3
+
4
+ module Hearthstone
5
+ module Log
6
+ class GameLogger
7
+ attr_reader :parser, :debug
8
+ attr_accessor :game, :delegate, :mode
9
+
10
+ def initialize(delegate, debug: false)
11
+ @delegate = delegate
12
+ @parser = Parser.new
13
+ @game = Game.new(nil)
14
+ @debug = debug
15
+ @mode = nil
16
+ end
17
+
18
+ def log_file(io)
19
+ io.each_line do |line|
20
+ log_line(line)
21
+ end
22
+ end
23
+
24
+ def log_line(line)
25
+ result = parser.parse_line(line)
26
+ if result
27
+ name = result[0]
28
+ data = result[1]
29
+ log_line = line if self.debug
30
+ process_event(name, data, log_line)
31
+ end
32
+ end
33
+
34
+ private
35
+ def process_event(event, data, line=nil)
36
+ case event
37
+ when :game_start
38
+ # ignored
39
+ when :game_over
40
+ on_game_over(data)
41
+ when :turn_start
42
+ on_turn_start(data)
43
+ when :turn
44
+ on_turn(data)
45
+ when :begin_spectator_mode, :end_spectator_mode
46
+ # ignored
47
+ when :mode
48
+ on_game_mode(data)
49
+ when :player_id
50
+ on_player_id(data)
51
+ when :first_player
52
+ on_first_player(data)
53
+ when :attack, :attacked
54
+ # ignored
55
+ when :set_hero
56
+ on_set_hero(data)
57
+ when :set_hero_power
58
+ on_set_hero_power(data)
59
+ when :hero_destroyed
60
+ on_hero_destroyed(data)
61
+ else
62
+ if event
63
+ on_event(event, data, line)
64
+ end
65
+ end
66
+ end
67
+
68
+ def on_game_mode(mode)
69
+ self.mode = mode
70
+ self.game = Game.new(mode)
71
+
72
+ if delegate.respond_to?(:on_game_mode)
73
+ delegate.on_game_mode(mode)
74
+ end
75
+ end
76
+
77
+ def on_player_id(name: nil, player_id: nil)
78
+ player = self.game.player_with_id_or_name(name: name, id: player_id)
79
+ player.name = name
80
+ player.id = player_id
81
+ end
82
+
83
+ def on_first_player(name: nil)
84
+ self.game.player_with_id_or_name(name: name).first_player = true
85
+ end
86
+
87
+ def on_set_hero(player_id: nil, id: nil, card_id: nil)
88
+ self.game.player_with_id_or_name(id: player_id).hero = {id: id, card_id: card_id}
89
+ end
90
+
91
+ def on_set_hero_power(player_id: nil, id: nil, card_id: nil)
92
+ self.game.player_with_id_or_name(id: player_id).hero_power = {id: id, card_id: card_id}
93
+ end
94
+
95
+ def on_game_over(name: nil, state: nil)
96
+ self.game.results[name] = state
97
+
98
+ if delegate.respond_to?(:on_game_over)
99
+ if self.game.completed? && self.game.results.count == 2
100
+ delegate.on_game_over(self.game)
101
+ end
102
+ end
103
+ end
104
+
105
+ def on_hero_destroyed(player_id: nil, id: nil, card_id: nil)
106
+ # when hero destroyed, the game is ended, we should proceed to next game
107
+ self.game = Game.new(self.mode)
108
+ end
109
+
110
+ def on_turn_start(name: nil, timestamp: nil)
111
+ self.game.current_turn.player = name
112
+ self.game.current_turn.timestamp = timestamp
113
+ end
114
+
115
+ def on_turn(turn)
116
+ self.game.add_turn(number: turn)
117
+ end
118
+
119
+ def on_event(event, data, line=nil)
120
+ self.game.current_turn.add_event(event, data, line)
121
+
122
+ if delegate.respond_to?(:on_event)
123
+ delegate.on_event(event, data)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end