tclog 0.1.0

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.
data/README.mkd ADDED
@@ -0,0 +1,40 @@
1
+ # TCLog: parser for etconsole.log of TrueCombat:Elite (TC:E)
2
+
3
+ ## Install
4
+
5
+ $ gem install tclog
6
+
7
+ Coming soon!
8
+
9
+ ## Usage
10
+
11
+ require 'tclog'
12
+
13
+ g = TCLog.analyze("/path/to/etconsole.log")
14
+
15
+ p g.players
16
+ p g.rounds[0]
17
+
18
+ ## License
19
+
20
+ The MIT License
21
+
22
+ (c) Shota Fukumori (sora_h) 2010-
23
+
24
+ Permission is hereby granted, free of charge, to any person obtaining a copy
25
+ of this software and associated documentation files (the "Software"), to deal
26
+ in the Software without restriction, including without limitation the rights
27
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
28
+ copies of the Software, and to permit persons to whom the Software is
29
+ furnished to do so, subject to the following conditions:
30
+
31
+ The above copyright notice and this permission notice shall be included in
32
+ all copies or substantial portions of the Software.
33
+
34
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
40
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "tclog"
5
+ gemspec.summary = "Parser for etconsole.log of TrueCombat:Elite (TC:E)"
6
+ gemspec.email = "sorah@tubusu.net"
7
+ gemspec.homepage = "http://github.com/sorah/tclog"
8
+ gemspec.description = "Parser for etconsole.log of TrueCombat:Elite (TC:E)"
9
+ gemspec.authors = ["Shota Fukumori"]
10
+ end
11
+ rescue LoadError
12
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
13
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/tclog.rb ADDED
@@ -0,0 +1,289 @@
1
+ module TCLog
2
+ class Game
3
+ def initialize(orders = [], gametype = :obj)
4
+ @orders = orders
5
+ @gametype = gametype
6
+ @rounds = []
7
+ @round_n = -1
8
+ @round_r = -1
9
+ @players = {}
10
+ class << @players
11
+ def [](x)
12
+ if x.kind_of?(Integer)
13
+ self.map{|v|v}[x]
14
+ else
15
+ super x
16
+ end
17
+ end
18
+
19
+ def each
20
+ super &(Proc.new{|k,v| yield v })
21
+ end
22
+ end
23
+ end
24
+
25
+
26
+ def add_map(map_name)
27
+ @round_r += 1
28
+ @rounds << Round.new(self, nil, @round_r, :map, true, map_name); self
29
+ end
30
+
31
+ def add_round(specops,terrorists,won)
32
+ @round_n += 1
33
+ @round_r += 1
34
+ r = Round.new(self, @round_n, @round_r, won)
35
+ r.specops = specops.dup
36
+ r.terrorists = terrorists.dup
37
+ @rounds << r
38
+ self
39
+ end
40
+
41
+ def add_player(name)
42
+ @players[name] = Player.new(name)
43
+ if 0 <= @round_r
44
+ @players[name].push_result(@round_r)
45
+ end
46
+ self
47
+ end
48
+
49
+
50
+ def round; @round_n; end
51
+ def [](i); @rounds[i]; end
52
+ attr_reader :rounds, :players, :orders, :gametype, :round_r
53
+ end
54
+ class Round
55
+ def initialize(game, n, rn, win, map_changing = false, map_name = nil)
56
+ @game = game
57
+ @won = win
58
+ @round_number = n
59
+ @real_round_number = rn
60
+ @map_changing = map_changing
61
+ @specops = {}
62
+ @terrorists = {}
63
+ @map_name = map_name
64
+ end
65
+
66
+ def players
67
+ @game.players.map do |g|
68
+ g if @round_number && g.results[@real_round_number]
69
+ end.compact
70
+ end
71
+
72
+ def player_results
73
+ @game.players.map do |g|
74
+ g.results[@real_round_number] if @round_number
75
+ end.compact
76
+ end
77
+
78
+ def map_changing?; @map_changing; end
79
+ def map_changing=(x); @map_changing = x; end
80
+ attr_accessor :map_name, :specops, :terrorists, :won
81
+ attr_reader :round_number
82
+ end
83
+ class Player
84
+ def initialize(name)
85
+ @name = name
86
+ @results = []
87
+ end
88
+
89
+ def add_result(n, score)
90
+ @results[n] = score.merge(:round => n); self
91
+ end
92
+
93
+ def total
94
+ a = @results.compact.inject({
95
+ :name => @name,
96
+ :kill => 0,
97
+ :death => 0,
98
+ :sui => 0,
99
+ :tk => 0,
100
+ :eff => 0,
101
+ :dg => 0,
102
+ :dr => 0,
103
+ :td => 0,
104
+ :score => 0,
105
+ :rate => 0
106
+ }) do |r,i|
107
+ [:kill,:death,:sui,:tk,:eff,:dg,:dr,:td,:score].each do |l|
108
+ r[l] += i[l]
109
+ end
110
+ r
111
+ end
112
+ a[:rate] = (a[:dg].to_i-a[:dr].to_i)/100.0
113
+ a[:kd] = a[:kill].to_f / a[:death].to_f
114
+ a
115
+ end
116
+
117
+ def push_result(i)
118
+ @results[i] = nil
119
+ end
120
+ attr_reader :name, :results
121
+ end
122
+
123
+ # logfile = String: filename
124
+ # IO: io
125
+ #
126
+ # gametype = :obj => "Objective"
127
+ # :ctf => "Capture The Flag"
128
+ # :bc => "BodyCount"
129
+ def self.analyze(logfile, gametype = :obj)
130
+ # Load file
131
+ log = case logfile
132
+ when File, IO
133
+ logfile.readlines.map(&:chomp)
134
+ when String
135
+ File.readlines(logfile).map(&:chomp)
136
+ else
137
+ raise ArgumentError, "logfile must be kind of String, File or IO"
138
+ end
139
+ # Drop waste lines / Drop waste message
140
+ renames = {}
141
+ orders = log.map do |x|
142
+ next unless /^(\[skipnotify\])(\^.)?(.+renamed|Timelimit hit)/ =~ x ||
143
+ /^(\[skipnotify\])?(\^.)?(Specops|Terrorists)/ =~ x ||
144
+ /^(\[skipnotify\])?(\^.)?(Planted|Defused)/ =~ x ||
145
+ /^The .+ have completed the objective!/ =~ x ||
146
+ /^(\[skipnotify\])(\^.)(Overall stats for: |)/ =~ x ||
147
+ /^(LOADING\.\.\. maps|Match starting)/ =~ x
148
+ x.gsub!(/\[skipnotify\]/,"")
149
+ # Player Score
150
+ r = x.scan(/\^.(Terrorists|Specops)\^. *\^.(.+?)\^. *([\d\-]+) +([\d\-]+) +([\d\-]+) +([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)/).flatten
151
+ if r.empty? # If the line not player's score
152
+ case x
153
+ when /^LOADING\.\.\. maps\/(.+?)\.bsp$/ # Map Changing?
154
+ m = $~.captures
155
+ ["Map",m[0]]
156
+ when /^Match starting/ # Match starting
157
+ ["Match"]
158
+ when /^(Planted) at (.+?) \[(.)\]/, /^The Terrorists have completed the objective!/ # TeroWin
159
+ m = $~.captures
160
+ m[0] = "TerroristsWin"
161
+ m
162
+ when /^(Defused) at (.+?) \[(.)\]/, /^The Specops have completed the objective!/ # Specops Win
163
+ m = $~.captures
164
+ m[0] = "SpecopsWin"
165
+ m
166
+ when /Timelimit hit/
167
+ if gametype == :obj
168
+ ["SpecopsWin"]
169
+ else
170
+ ["UnknownWin"]
171
+ end
172
+ when /^\^7Overall stats for:/ # /^(\[skipnotify\])(\^.)(Overall stats for: |)/
173
+ if [:bc, :obj].include?(gametype)
174
+ ["UnknownWin"]
175
+ else
176
+ nil
177
+ end
178
+ when /\^.(.+?)\^. \^.(Totals) *([\d\-]+) +([\d\-]+) +([\d\-]+) +([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)\^. *([\d\-]+)/ # Team total score?
179
+ r = $~.captures
180
+ r[0] << "Total"
181
+ m = [r[0]]
182
+ m << {
183
+ :name => r[1],
184
+ :kill => r[2].to_i,
185
+ :death => r[3].to_i,
186
+ :sui => r[4].to_i,
187
+ :tk => r[5].to_i,
188
+ :eff => r[6].to_i,
189
+ :aa => r[7].to_i,
190
+ :dg => r[8].to_i,
191
+ :dr => r[9].to_i,
192
+ :td => r[10].to_i,
193
+ :score => r[11].to_i,
194
+ :rate => (r[8].to_i-r[9].to_i)/100.0,
195
+ }
196
+ when /^(.+) renamed to (.+)/ # nick renamed?
197
+ m = $~.captures
198
+ m.map!{|n|n.gsub(/ +$/,"")[0..14]}
199
+ renames[m[1]] = renames[m[0]] || m[0]
200
+ nil
201
+ end
202
+ else
203
+ # Process player's score
204
+ if r[1].kind_of?(String)
205
+ r[1].gsub!(/ +$/,"")
206
+ r[1] = r[1][0..14]
207
+ r[1] = renames[r[1]] || r[1]
208
+ end
209
+ m = [r[0]]
210
+ m << {
211
+ :name => r[1],
212
+ :kill => r[2].to_i,
213
+ :death => r[3].to_i,
214
+ :sui => r[4].to_i,
215
+ :tk => r[5].to_i,
216
+ :eff => r[6].to_i,
217
+ :aa => r[7].to_i,
218
+ :dg => r[8].to_i,
219
+ :dr => r[9].to_i,
220
+ :td => r[10].to_i,
221
+ :score => r[11].to_i,
222
+ :rate => (r[8].to_i-r[9].to_i)/100.0,
223
+ }
224
+ end
225
+ end.compact
226
+
227
+ #pp orders
228
+
229
+ match_flag = false
230
+ game = Game.new(orders, gametype)
231
+ match = [""]
232
+ match_wins = nil
233
+ terrorists_total = nil
234
+ specops_total = nil
235
+ vm = Proc.new do |o|
236
+ case o[0]
237
+ when "Match"
238
+ if match_flag
239
+ if specops_total && match_wins
240
+ game.add_round specops_total, terrorists_total, match_wins
241
+ match_wins = nil
242
+ terrorists_total = nil
243
+ specops_total = nil
244
+ end
245
+ else
246
+ match_flag = true
247
+ end
248
+ if match[-1][0] == "Map"
249
+ game.add_map match[-1][1]
250
+ end
251
+ when "UnknownWin"
252
+ match_wins = :unknown if match_flag
253
+ when "TerroristsWin"
254
+ match_wins = :terrorists if match_flag
255
+ when "SpecopsWin"
256
+ match_wins = :specops if match_flag
257
+ when "Terrorists", "Specops"
258
+ if match_flag
259
+ unless game.players[o[1][:name]]
260
+ game.add_player(o[1][:name])
261
+ end
262
+ game.players[o[1][:name]].add_result(game.round_r+1,o[1])
263
+ end
264
+ when "TerroristsTotal"
265
+ terrorists_total = o[1] if match_flag
266
+ when "SpecopsTotal"
267
+ if match_flag
268
+ specops_total = o[1]
269
+ match_wins = compare_score(terrorists_total, specops_total) if match_wins == :unknown
270
+ end
271
+ end
272
+ match << o
273
+ end
274
+ orders.each(&vm)
275
+
276
+ vm.call(["Match"]) if match_flag
277
+ game
278
+ end
279
+
280
+ def self.compare_score(terrorists, specops)
281
+ if terrorists[:score] > specops[:score]
282
+ :terrorists
283
+ else
284
+ :specops
285
+ end
286
+ end
287
+ end
288
+
289
+