Hi,
'Cuz I'm such a curious bunny, for long I've longed (ha, get it? Longed.. Long..) to have accurate, complete unit statistics from replays on my server. The virtues of such a widget would be that it can be ran on replays with headless Spring and produce an outcome in some readable format. If it could recognize unique units independently of the game being played, that would be amazing. It should have at least the following, per-unit type stats: Total produced amount, metal cost, energy cost, health, weapon(s) damage, total damage dealt, total damage taken, time of first unit built, preferably time of each individual unit being built (yeah, will be a lot of data for some units, but data is good!), accumulated XP, time alive on the average, total amount of enemies destroyed.. And so on!
If anyone can suggest an existing widget or an existing widget that was easy to modify for this purpose, that would be great.
But even if no existing widgets provide for these purposes, any pointers to how one should proceed are welcomed as well, such as any particularly helpful engine functions or examples. My Lua-fu is not master level, but I'm sure I'll manage to whip something up if I can find the time to.
Thanks,
- tzaeru
Advice on gathering extended unit statistics?
Moderator: Moderators
Re: Advice on gathering extended unit statistics?
I used this for logging XP at time of death:
edit:
Also some filty python code to run multiple instances of headless:
Edit2: more filthy python to generate graphs from the stats:
Code: Select all
function widget:GetInfo()
return {
name = "stats-unitkilled",
desc = "saves unit xp",
author = "beherith",
date = "2011 may",
license = "PD",
layer = 0,
enabled = false -- loaded by default?
}
end
function widget:Initialize()
gameid=os.time()
Spring.Echo(gameid)
mapname=Game.mapName
modname=Game.modName
fname=modname .. "$"..mapname.."$"..gameid .."$stats.txt"
fd= io.open("stats/"..fname,"w")
if fd then
fileopened=true
else
Spring.Echo("Error, opening stats/"..fname.." failed!")
widgetHandler:RemoveWidget()
end
Spring.SendCommands("setminspeed 30")
end
function widget:UnitDestroyed(unitID, unitDefID, unitTeamID)
_,_,_,_,buildpercent=Spring.GetUnitHealth(unitID)
if (buildpercent == nil) then
Spring.Echo('Nil encountered on buildpercent check after unitdestroyed!')
return
end
if (buildpercent <1.0) then
--Spring.Echo('Nanoframe destroyed')
return
end
xp=Spring.GetUnitExperience(unitID)
unitDefID = Spring.GetUnitDefID(unitID)
unitDef = UnitDefs[unitDefID or -1]
gf=Spring.GetGameFrame()
--name='unknownxxx'
--playerinfo=Spring.GetPlayerInfo( unitTeamID)
-- if (playerinfo) then
fd:write(unitDef.name .. " " .. unitTeamID .. " " .. xp .. " " .. gf .. "\n")
--Spring.Echo("Unit "..unitID.." " .. unitDef.name .. " from team "..unitTeamID.." just got destroyed by enemy unit " .. xp)
end
function widget:GameOver()
allunits=Spring.GetAllUnits()
fd:write("Game Ended!\n")
for i=1, #allunits do
unitID=allunits[i]
xp=Spring.GetUnitExperience(unitID)
unitDefID = Spring.GetUnitDefID(unitID)
unitDef = UnitDefs[unitDefID or -1]
unitTeamID=Spring.GetUnitTeam(unitID)
fd:write(unitDef.name .. " " .. unitTeamID .. " " .. xp .. "\n")
end
fd:close()
widgetHandler:RemoveWidget()
Spring.SendCommands("quitforce")
end
Also some filty python code to run multiple instances of headless:
Code: Select all
import os
import time
import sys
import subprocess
#2 more things need to be done as well,
time.clock()
i=0
procs=[]
maxprocesses=3
cmd=[]
for filename in os.listdir(os.getcwd()+'\\replays\\'):
if '.sdf' not in filename:
continue
# cmd='copy .\\tehdemos\\'+filename+' '+filename
# print cmd
# os.system(cmd)
command='spring-headless.exe --config headless.cfg ./statsinput/'+filename
#print cmd
# cmd='del '+filename
# print cmd
# os.system(cmd)
cmd.append(command)
print 'Number, total secs', i, int(time.clock())
print 'loaded',len(cmd),'replays'
while i<len(cmd) or len(procs)!=0:
if len(procs)<maxprocesses and i<len(cmd):
print 'starting',cmd[i],i,len(procs)
luaf=open('LuaUI/Widgets/stats_unitdestroyed.lua')
lualines=luaf.readlines()
luaf.close()
luaf=open('LuaUI/Widgets/stats_unitdestroyed.lua','w')
lualines[12] = 'local myname = \"'+cmd[i].split(' ')[3].strip()+'\"\n'
luaf.write(''.join(lualines))
luaf.close()
exit(1)
procs.append(subprocess.Popen(cmd[i].split(' ')))
i+=1
time.sleep(20) # time to load and path and get shit done!
for p in procs:
p.poll()
if p.returncode != None:
procs.remove(p)
print 'Number, total secs', i, int(time.clock())
time.sleep(1)
print 'Number, total secs', i, int(time.clock())
Code: Select all
import os
import sys
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from numpy import *
from matplotlib.colors import LogNorm
from pylab import *
names={'aafus':"Advanced Fusion Reactor", 'ajuno':"Arm Juno", 'amgeo':"Moho Geothermal Powerplant", 'armaak':"Archangel", 'armaap':"Advanced Aircraft Plant", 'armaas':"Archer", 'armaca':"Advanced Construction Aircraft", 'armack':"Advanced Construction Kbot", 'armacsub':"Advanced Construction Sub", 'armacv':"Advanced Construction Vehicle", 'armadvsol':"Advanced Solar Collector", 'armah':"Swatter", 'armalab':"Advanced Kbot Lab", 'armamb':"Ambusher", 'armamd':"Protector", 'armamex':"Twilight", 'armamph':"Pelican", 'armanac':"Anaconda", 'armanni':"Annihilator", 'armap':"Aircraft Plant", 'armarad':"Advanced Radar Tower", 'armaser':"Eraser", 'armason':"Advanced Sonar Station", 'armasp':"Air Repair Pad", 'armasy':"Advanced Shipyard", 'armatl':"Moray", 'armatlas':"Atlas", 'armavp':"Advanced Vehicle Plant", 'armawac':"Eagle", 'armbanth':"Bantha", 'armbats':"Millennium", 'armbeaver':"Beaver", 'armbrawl':"Brawler", 'armbrtha':"Big Bertha", 'armbull':"Bulldog", 'armca':"Construction Aircraft", 'armcarry':"Colossus", 'armch':"Construction Hovercraft", 'armcir':"Chainsaw", 'armck':"Construction Kbot", 'armckfus':"Cloakable Fusion Reactor", 'armclaw':"Dragon's Claw", 'armcom':"Commander", 'armcroc':"Triton", 'armcrus':"Conqueror", 'armcs':"Construction Ship", 'armcsa':"Construction Seaplane", 'armcv':"Construction Vehicle", 'armcybr':"Liche", 'armdecom':"Commander", 'armdf':"Fusion Reactor", 'armdfly':"Dragonfly", 'armdl':"Anemone", 'armdrag':"Dragon's Teeth", 'armemp':"Detonator", 'armestor':"Energy Storage", 'armeyes':"Dragon's Eye", 'armfark':"Fark", 'armfast':"Zipper", 'armfatf':"Floating Targeting Facility", 'armfav':"Jeffy", 'armfboy':"Fatboy", 'armfdrag':"Shark's Teeth", 'armfflak':"Flakker NS", 'armfhlt':"Stingray", 'armfhp':"Floating Hovercraft Platform", 'armfido':"Fido", 'armfig':"Freedom Fighter", 'armflak':"Flakker", 'armflash':"Flash", 'armflea':"Flea", 'armfmine3':"Mega NS", 'armfmkr':"Floating Energy Converter", 'armfort':"Fortification Wall", 'armfrad':"Floating Radar Tower", 'armfrt':"Sentry", 'armfus':"Fusion Reactor", 'armgate':"Keeper", 'armgeo':"Geothermal Powerplant", 'armgmm':"Prude", 'armguard':"Guardian", 'armham':"Hammer", 'armhawk':"Hawk", 'armhlt':"Sentinel", 'armhp':"Hovercraft Platform", 'armjam':"Jammer", 'armjamt':"Sneaky Pete", 'armjanus':"Janus", 'armjeth':"Jethro", 'armkam':"Banshee", 'armlab':"Kbot Lab", 'armlance':"Lancet", 'armlatnk':"Panther", 'armllt':"LLT", 'armmakr':"Energy Converter", 'armmanni':"Penetrator", 'armmark':"Marky", 'armmart':"Luger", 'armmav':"Maverick", 'armmerl':"Merl", 'armmex':"Metal Extractor", 'armmh':"Wombat", 'armmine1':"Micro", 'armmine2':"Kilo", 'armmine3':"Mega", 'armmls':"Valiant", 'armmlv':"Podger", 'armmmkr':"Moho Energy Converter", 'armmoho':"Moho Mine", 'armmship':"Ranger", 'armmstor':"Metal Storage", 'armnanotc':"Nano Turret", 'armorco':"Orcone", 'armpb':"Pit Bull", 'armpeep':"Peeper", 'armpincer':"Pincer", 'armplat':"Seaplane Platform", 'armpnix':"Phoenix", 'armpt':"Skeeter", 'armpw':"Peewee", 'armrad':"Radar Tower", 'armraven':"Catapult", 'armraz':"Razorback", 'armrecl':"Grim Reaper", 'armrectr':"Rector", 'armrl':"Defender", 'armrock':"Rocko", 'armroy':"Crusader", 'armsaber':"Sabre", 'armsam':"Samson", 'armsb':"Tsunami", 'armscab':"Scarab", 'armsd':"Tracer", 'armseap':"Albatross", 'armseer':"Seer", 'armsehak':"Seahawk", 'armsfig':"Tornado", 'armsh':"Skimmer", 'armshltx':"Experimental Gantry", 'armshock':"Vanguard", 'armsilo':"Retaliator", 'armsjam':"Escort", 'armsl':"Seahook", 'armsnipe':"Sharpshooter", 'armsolar':"Solar Collector", 'armsonar':"Sonar Station", 'armspid':"Spider", 'armsptk':"Recluse", 'armspy':"Infiltrator", 'armst':"Gremlin", 'armstump':"Stumpy", 'armsub':"Lurker", 'armsubk':"Piranha", 'armsy':"Shipyard", 'armtarg':"Targeting Facility", 'armthovr':"Bear", 'armthund':"Thunder", 'armtide':"Tidal Generator", 'armtl':"Harpoon", 'armtship':"Hulk", 'armuwadves':"Hardened Energy Storage", 'armuwadvms':"Hardened Metal Storage", 'armuwes':"Underwater Energy Storage", 'armuwfus':"Underwater Fusion Plant", 'armuwmex':"Underwater Metal Extractor", 'armuwmme':"Underwater Moho Mine", 'armuwmmm':"Underwater Moho Energy Converter", 'armuwms':"Underwater Metal Storage", 'armvader':"Invader", 'armveil':"Veil", 'armvp':"Vehicle Plant", 'armvulc':"Vulcan", 'armwar':"Warrior", 'armwin':"Wind Generator", 'armyork':"Phalanx", 'armzeus':"Zeus", 'aseadragon':"Epoch", 'asubpen':"Amphibious Complex", 'blade':"Blade", 'bladew':"Bladewing", 'cafus':"Advanced Fusion Reactor", 'cjuno':"Core Juno", 'cmgeo':"Moho Geothermal Powerplant", 'commando':"Commando", 'consul':"Consul", 'coraak':"Manticore", 'coraap':"Advanced Aircraft Plant", 'coraca':"Advanced Construction Aircraft", 'corack':"Advanced Construction Kbot", 'coracsub':"Advanced Construction Sub", 'coracv':"Advanced Construction Vehicle", 'coradvsol':"Advanced Solar Collector", 'corah':"Slinger", 'corak':"A.K.", 'coralab':"Advanced Kbot Lab", 'coramph':"Gimp", 'corap':"Aircraft Plant", 'corape':"Rapier", 'corarad':"Advanced Radar Tower", 'corarch':"Shredder", 'corason':"Advanced Sonar Station", 'corasp':"Air Repair Pad", 'corasy':"Advanced Shipyard", 'coratl':"Lamprey", 'coravp':"Advanced Vehicle Plant", 'corawac':"Vulture", 'corbats':"Warlord", 'corbhmth':"Behemoth", 'corblackhy':"Black Hydra", 'corbuzz':"Buzzsaw", 'corca':"Construction Aircraft", 'corcan':"Can", 'corcarry':"Hive", 'corch':"Construction Hovercraft", 'corck':"Construction Kbot", 'corcom':"Commander", 'corcrash':"Crasher", 'corcrus':"Executioner", 'corcrw':"Krow", 'corcs':"Construction Ship", 'corcsa':"Construction Seaplane", 'corcut':"Cutlass", 'corcv':"Construction Vehicle", 'cordecom':"Commander", 'cordl':"Jellyfish", 'cordoom':"Doomsday Machine", 'cordrag':"Dragon's Teeth", 'corenaa':"Cobra - NS", 'corerad':"Eradicator", 'corestor':"Energy Storage", 'coresupp':"Supporter", 'coreter':"Deleter", 'corexp':"Exploiter", 'coreyes':"Dragon's Eye", 'corfast':"Freaker", 'corfatf':"Floating Targeting Facility", 'corfav':"Weasel", 'corfdrag':"Shark's Teeth", 'corfhlt':"Thunderbolt", 'corfhp':"Floating Hovercraft Platform", 'corfink':"Fink", 'corflak':"Cobra", 'corfmd':"Fortitude", 'corfmine3':"1100 NS", 'corfmkr':"Floating Energy Converter", 'corfort':"Fortification Wall", 'corfrad':"Floating Radar Tower", 'corfrt':"Stinger", 'corfus':"Fusion Reactor", 'corgant':"Experimental Gantry", 'corgarp':"Garpike", 'corgate':"Overseer", 'corgator':"Instigator", 'corgeo':"Geothermal Powerplant", 'corgol':"Goliath", 'corgripn':"Stiletto", 'corhlt':"Gaat Gun", 'corhp':"Hovercraft Platform", 'corhrk':"Dominator", 'corhunt':"Hunter", 'corhurc':"Hurricane", 'corint':"Intimidator", 'corjamt':"Castro", 'corkarg':"Karganeth", 'corkrog':"Krogoth", 'corlab':"Kbot Lab", 'corlevlr':"Leveler", 'corllt':"LLT", 'cormabm':"Hedgehog", 'cormakr':"Energy Converter", 'cormart':"Pillager", 'cormaw':"Dragon's Maw", 'cormex':"Metal Extractor", 'cormexp':"Moho Exploiter", 'cormh':"Nixer", 'cormine1':"11", 'cormine2':"110", 'cormine3':"1100", 'cormist':"Slasher", 'cormls':"Pathfinder", 'cormlv':"Spoiler", 'cormmkr':"Moho Energy Converter", 'cormoho':"Moho Mine", 'cormort':"Morty", 'cormship':"Messenger", 'cormstor':"Metal Storage", 'cormuskrat':"Muskrat", 'cornanotc':"Nano Turret", 'cornecro':"Necro", 'corparrow':"Poison Arrow", 'corplat':"Seaplane Platform", 'corpt':"Searcher", 'corpun':"Punisher", 'corpyro':"Pyro", 'corrad':"Radar Tower", 'corraid':"Raider", 'correap':"Reaper", 'correcl':"Death Cavalry", 'corrl':"Pulverizer", 'corroach':"Roach", 'corroy':"Enforcer", 'corsb':"Maelstrom", 'corsd':"Nemesis", 'corseal':"Croc", 'corseap':"Typhoon", 'corsent':"Copperhead", 'corsfig':"Voodoo", 'corsh':"Scrubber", 'corshad':"Shadow", 'corshark':"Shark", 'corshroud':"Shroud", 'corsilo':"Silencer", 'corsjam':"Phantom", 'corsktl':"Skuttle", 'corsnap':"Snapper", 'corsolar':"Solar Collector", 'corsonar':"Sonar Station", 'corspec':"Spectre", 'corspy':"Parasite", 'corssub':"Leviathan", 'corstorm':"Storm", 'corsub':"Snake", 'corsumo':"Sumo", 'corsy':"Shipyard", 'cortarg':"Targeting Facility", 'cortermite':"Termite", 'corthovr':"Turtle", 'corthud':"Thud", 'cortide':"Tidal Generator", 'cortitan':"Titan", 'cortl':"Urchin", 'cortoast':"Toaster", 'cortron':"Catalyst", 'cortship':"Envoy", 'coruwadves':"Hardened Energy Storage", 'coruwadvms':"Hardened Metal Storage", 'coruwes':"Underwater Energy Storage", 'coruwfus':"Underwater Fusion Plant", 'coruwmex':"Underwater Metal Extractor", 'coruwmme':"Underwater Moho Mine", 'coruwmmm':"Underwater Moho Energy Converter", 'coruwms':"Underwater Metal Storage", 'corvalk':"Valkyrie", 'corvamp':"Vamp", 'corveng':"Avenger", 'corvipe':"Viper", 'corvoyr':"Voyeur", 'corvp':"Vehicle Plant", 'corvrad':"Informer", 'corvroc':"Diplomat", 'corwin':"Wind Generator", 'corwolv':"Wolverine", 'csubpen':"Amphibious Complex", 'decade':"Decade", 'gorg':"Juggernaut", 'hllt':"HLLT", 'intruder':"Intruder", 'krogtaar':"KrogTaar", 'madsam':"SAM", 'marauder':"Marauder", 'mercury':"Mercury", 'nsaclash':"Halberd", 'packo':"Pack0", 'screamer':"Screamer", 'shiva':"Shiva", 'tawf001':"Beamer", 'tawf009':"Serpent", 'tawf013':"Shellshocker", 'tawf114':"Banisher", 'trem':"Tremor"}
db=open('stats.csv','w')
onlydead=1
minrank = 5 # only gold star and higher
maxtime=30*60*60 # 1 hour is enough
maxxp=2
notdsd={}
dsd={}
i=0
days=[]
# plt.hist2d(range(10))
for filename in os.listdir(os.getcwd()):
if 'stats.txt' in filename:
i+=1
if i%100==0:
print 'file number',i
#break
f=open(filename,'r')
ln=f.readlines()
f.close()
ended=0
addedlines=0
nbteams=[]
isdsd=False
day=-1
for l in ln:
if '#' ==l[0]:
if 'replays' in l:
if 'DeltaSiegeDry' in l:
isdsd=True
try:
day=int(l.partition('replays/')[2].partition('_')[0])
if day not in days:
days.append(day)
except:
print filename,'cant parse date in ',l
pass
continue
if 'ended!' in l.lower():
ended=1
if onlydead==1:
break
continue
l=l.split(' ') # unitname, playernumber, XP, gameframe, rank
if len(l)!=5:
print 'bad line',l
continue
try:
unitname=l[0]
playernumber=int(l[1])
xp=float(l[2])
gf=float(l[3])
rank=int(l[4])
except:
print filename,'unable to parse line',l
pass
continue
if (ended==0 or (ended==1 and xp>0)) and gf< maxtime and rank>=minrank:
addedlines+=1
d=0
if isdsd:
d=dsd
else:
d=notdsd
if unitname not in d:
d[l[0]]={}
if day not in d[unitname]:
d[unitname][day]=[]
d[unitname][day].append((xp, gf))
days=sorted(days)
urls=[]
print 'Processed',i,'files, found',len(d),'unique units', 'in ',len(days), 'days'
db.write('name,id,avgXP,samplecount')
for k,v in d.iteritems():
if k not in names:
print 'Couldnt find',k,'in nametable, had',len(v),'items'
continue
exp=[]
time=[]
nonattack=1
avgexp=0
runavg=[]
maxtime=0
for day in days:
if day in v:
for e in v[day]: #collect time and exp
exp.append(e[0])
avgexp+=e[0]
time.append(e[1]/(30*60))
maxtime=max(maxtime,e[1]/(30*60))
if e[0]>0:
nonattack=0
avgexp/=len(exp)
if nonattack==1 or avgexp<0.0001:
print names[k] ,'is not an attack unit'
else:
for i in range(int(maxtime)): #collect avg's
curavg=0
cnt=0.001
for t in range(len(time)):
if time[t]>i-2 and time[t]<i+2:
curavg+=exp[t]
cnt+=1
runavg.append(curavg/cnt)
print names[k],'is an attack unit'
print '_average xp for '+k+' '+names[k]+' '+str(avgexp)
fig=plt.figure(1)
plt.clf()
greenexp=[]
greentime=[]
for e in range(len(exp)):
if exp[e]>2: #we mark sample with exp>2 with green color
exp[e]=2
greenexp.append(exp[e])
greentime.append(time[e])
e=0
while(e<len(exp)):
if exp[e]==0:
del exp[e]
del time[e]
#print e
else:
e+=1
# plt.plot(time,exp,marker='o',color='b',linestyle='None')
# plt.plot(greentime,greenexp,marker='o',color='g',linestyle='None')
plt.subplot(111, axisbg='#000080')
if len(exp)>0:
plt.hist2d(time, exp, bins=[4*60,200])#,norm=LogNorm())
plt.plot([0,60],[0.19,0.19],color='y')
plt.plot(range(len(runavg)),runavg,color='r')
plt.title(names[k]+'-'+k+' avg exp='+str(avgexp)+' samples='+str(len(time)))
db.write(names[k]+','+k+','+str(avgexp)+','+str(len(time))+'\n')
colorbar()
fig.savefig((names[k]+'-'+k+'.png'),dpi=144)
urls.append('[url=http://beherith.eat-peet.net/stuff/unitexp/'+names[k]+'-'+k+'.png]'+names[k]+'[/url]')
for k in urls:
print k
Re: Advice on gathering extended unit statistics?
You probably don't want to count every built dragon's teeth as a killed unit? Etc. Some filtering needs to be applied to those statistics.
There was a discussion before on this topic, not sure if it materialised to a widget or something...
There was a discussion before on this topic, not sure if it materialised to a widget or something...
Re: Advice on gathering extended unit statistics?
Jools, yes some filtering is applied to the data, as per the widget and the graphing script. And yes, the discussion of this topic did materialize into a widget. The widget is posted above :)
Re: Advice on gathering extended unit statistics?
Thanks, Behe! That's awesome.
I'll put these into use when I manage my hands on more fresh replays. :)
I'll put these into use when I manage my hands on more fresh replays. :)
Re: Advice on gathering extended unit statistics?
Nice work. I didn't see the callins the first time when I skimmed through the code but now I see them.
Re: Advice on gathering extended unit statistics?
Hey Beherith, is there some code missing from the python script for making graphs?
I've got the rest working (thank you!) and I thought this would be a good opportunity to learn plotting etc in python.
I've got the rest working (thank you!) and I thought this would be a good opportunity to learn plotting etc in python.
- Silentwings
- Posts: 3720
- Joined: 25 Oct 2008, 00:23
Re: Advice on gathering extended unit statistics?
The next version of BA will contain automated unit statistics collection (http://imolarpg.dyndns.org/trac/balatest/changeset/3723). Docs from the widget part:
A pre-emptive presentfor anyone trying to replace careful thought with statistics.
The stats collection is portable and could be used by any game. The stats table is basically human readable (although it will be a long file) and anyone wanting to process the stats will obviously need to learn/use lua themselves.luaui/widgets/stats_damage.lua wrote:This writes a file in /luaui/config for every user, containing statistics on which units were built, and various other stats, summarizing all (complete, non-replay) games seen by the user, of the most recent game version seen by the user.
The statistics are stored as a lua table which can be loaded in the normal way. the format of the stats table is self-explanatory: stats[game][mode][unitName] = { various statistics }. All statistics are mean averages, except for 'n', which is the number of samples seen of the given unit.
A pre-emptive presentfor anyone trying to replace careful thought with statistics.