mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2024-12-30 10:26:58 +01:00
698 lines
28 KiB
Python
Executable file
698 lines
28 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import glob, json, os, re, requests, shutil, socket, sys, threading
|
|
import argparse, datetime, random, signal, subprocess, time
|
|
from shutil import rmtree
|
|
|
|
g_UDP_PCT_START = 100
|
|
gDeadLaunches = 0
|
|
g_DROP_N=0
|
|
g_NAMES = [None, 'Brynn', 'Ariela', 'Kati', 'Eric']
|
|
|
|
def pick_ndevs(args):
|
|
RNUM = random.randint(0, 99)
|
|
if RNUM > 95 and args.MAXDEVS >= 4:
|
|
NDEVS = 4
|
|
elif RNUM > 90 and args.MAXDEVS >= 3:
|
|
NDEVS = 3
|
|
else:
|
|
NDEVS = 2
|
|
if NDEVS < args.MINDEVS:
|
|
NDEVS = args.MINDEVS
|
|
return NDEVS
|
|
|
|
# # Given a device count, figure out how many local players per device.
|
|
# # "1 1" would be a two-device game with 1 each. "1 2 1" a
|
|
# # three-device game with four players total
|
|
def figure_locals(args, NDEVS):
|
|
NPLAYERS = pick_ndevs(args)
|
|
if NPLAYERS < NDEVS: NPLAYERS = NDEVS
|
|
|
|
EXTRAS = 0
|
|
if not args.ONEPER:
|
|
EXTRAS = NPLAYERS - NDEVS
|
|
|
|
LOCALS = []
|
|
for IGNORE in range(NDEVS):
|
|
COUNT = 1
|
|
if EXTRAS > 0:
|
|
EXTRA = random.randint(0, EXTRAS)
|
|
if EXTRA > 0:
|
|
COUNT += EXTRA
|
|
EXTRAS -= EXTRA
|
|
LOCALS.append(COUNT)
|
|
assert 0 < sum(LOCALS) <= 4
|
|
return LOCALS
|
|
|
|
def player_params(args, NLOCALS, NPLAYERS, NAME_INDX):
|
|
assert 0 < NPLAYERS <= 4
|
|
PARAMS = []
|
|
if 1 == NAME_INDX:
|
|
NREMOTES = NPLAYERS - NLOCALS
|
|
while NLOCALS > 0 or NREMOTES > 0:
|
|
if 0 == random.randint(0, 2) and 0 < NLOCALS:
|
|
PARAMS += ['--robot', g_NAMES[NAME_INDX]]
|
|
if not args.IQS_SAME:
|
|
PARAMS += ['--robot-iq', str(random.randint(1,100))]
|
|
NLOCALS -= 1
|
|
NAME_INDX += 1
|
|
elif 0 < NREMOTES:
|
|
PARAMS += ['--remote-player']
|
|
NREMOTES -= 1
|
|
else:
|
|
PARAMS += ['--robot', g_NAMES[NAME_INDX]]
|
|
return PARAMS
|
|
|
|
def logReaderStub(dev): dev.logReaderMain()
|
|
|
|
def statusReaderStub(dev): dev.statusReaderMain()
|
|
|
|
class Device():
|
|
sHasLDevIDMap = {}
|
|
sTilesLeftTrayPat = re.compile('.*player \d+ now has (\d+) tiles') # not used
|
|
sMQTTDevIDPat = re.compile('.*getMQTTDevID.*: generated id: ([\d[A-F]+).*')
|
|
sConnPat = re.compile('.*linux_util_informMissing\(isServer.*nMissing=0\).*')
|
|
|
|
sScoresDup = []
|
|
sScoresReg = []
|
|
|
|
def __init__(self, args, game, indx, params, peers, order,
|
|
db, log, script, nInGame, inDupMode):
|
|
self.game = game
|
|
self.indx = indx
|
|
self.args = args
|
|
self.pid = 0
|
|
self.gamesOver = False
|
|
self.nGames = 1
|
|
self.params = params
|
|
self.order = order
|
|
self.db = db
|
|
self.logPath = log
|
|
self.script = script
|
|
self.nInGame = nInGame
|
|
self.inDupMode = inDupMode
|
|
# runtime stuff; init now
|
|
self.app = args.APP_OLD
|
|
self.proc = None
|
|
self.peers = peers
|
|
self.devID = ''
|
|
self.launchCount = 0
|
|
self.allDone = False # when true, can be killed
|
|
self.nTilesLeftPool = None
|
|
self.nTilesLeftTray = None
|
|
self.inviteeDevID = None
|
|
self.inviteeDevIDs = [] # only servers use this
|
|
self.inviteeMQTTDevID = None
|
|
self.inviteeMQTTDevIDs = []
|
|
self.connected = False
|
|
self.locked = False
|
|
self.msgCount = -1
|
|
self.statusSocketPath = self.logPath.replace('.txt', '.sock')
|
|
|
|
self.setApp(args.START_PCT)
|
|
|
|
with open(self.logPath, "w") as log:
|
|
log.write('New cmdline: ' + self.app + ' ' + (' '.join([str(p) for p in self.params])))
|
|
log.write(os.linesep)
|
|
|
|
def setApp(self, pct):
|
|
if self.app == self.args.APP_OLD and not self.app == self.args.APP_NEW:
|
|
if os.path.exists(self.script) and pct > random.randint(0, 99):
|
|
print('launch(): upgrading {} from {} to {}' \
|
|
.format(self.devName(), self.app, self.args.APP_NEW))
|
|
self.app = self.args.APP_NEW
|
|
# nuke script to force regeneration
|
|
os.unlink(self.script)
|
|
|
|
def devName(self):
|
|
return 'dev_' + str(self.indx)
|
|
|
|
def statusReaderMain(self):
|
|
statusSocket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
statusSocket.bind(self.statusSocketPath)
|
|
self.statusData = statusSocket.recv(1024)
|
|
statusSocket.close()
|
|
|
|
def logReaderMain(self):
|
|
assert self and self.proc
|
|
# print('logReaderMain called; opening:', self.logPath)
|
|
stdout, stderr = self.proc.communicate()
|
|
nLines = 0
|
|
with open(self.logPath, 'a') as log:
|
|
for line in stderr.splitlines():
|
|
nLines += 1
|
|
log.write(line + os.linesep)
|
|
|
|
self.locked = True
|
|
|
|
# Check every line for tiles left in tray
|
|
match = Device.sTilesLeftTrayPat.match(line)
|
|
if match: self.nTilesLeftTray = int(match.group(1))
|
|
|
|
if self.args.WITH_MQTT and not self.inviteeMQTTDevID:
|
|
match = Device.sMQTTDevIDPat.match(line)
|
|
if match:
|
|
self.inviteeMQTTDevID = int(match.group(1), 16)
|
|
# print('read mqtt devid: {:16X}'.format(self.inviteeMQTTDevID))
|
|
|
|
if not self.connected:
|
|
match = Device.sConnPat.match(line)
|
|
if match: self.connected = True
|
|
|
|
self.locked = False
|
|
|
|
# print('logReaderMain done, wrote lines:', nLines, 'to', self.logPath);
|
|
|
|
def checkScript(self):
|
|
if not os.path.exists(self.script):
|
|
args = ['exec'] # without exec means terminate() won't work
|
|
if self.args.VALGRIND:
|
|
args += ['valgrind']
|
|
# args += ['--leak-check=full']
|
|
# args += ['--track-origins=yes']
|
|
args += [self.app] + [str(p) for p in self.params]
|
|
if self.devID: args.extend( ' '.split(self.devID))
|
|
args += [ '$*' ]
|
|
with open( self.script, 'w' ) as fil:
|
|
fil.write( "#!/bin/sh\n" )
|
|
fil.write( ' '.join(args) + '\n' )
|
|
os.chmod(self.script, 0o755)
|
|
|
|
def launch(self, canRematch):
|
|
self.setApp(self.args.UPGRADE_PCT)
|
|
self.checkScript()
|
|
self.launchCount += 1
|
|
args = [ self.script, '--close-stdin' ]
|
|
if canRematch:
|
|
args.append('--rematch-when-done')
|
|
|
|
if self.statusSocketPath:
|
|
args += ['--status-socket-name', self.statusSocketPath]
|
|
self.statusReader = threading.Thread(target = statusReaderStub, args=(self,))
|
|
self.statusReader.isDaemon = True
|
|
self.statusReader.start()
|
|
|
|
# If I'm an unconnected server and I know a client's relayid,
|
|
# append it so invitation can happen. When more than one
|
|
# device will be invited, the invitations must always go in
|
|
# the same order so channels will be assigned consistently. So
|
|
# keep them in an array as they're encountered, and use in
|
|
# that order
|
|
if self.args.WITH_MQTT:
|
|
if self.order == 1 and not self.connected:
|
|
for peer in self.peers:
|
|
if peer.inviteeMQTTDevID and not peer == self:
|
|
if not peer.inviteeMQTTDevID in self.inviteeMQTTDevIDs:
|
|
self.inviteeMQTTDevIDs.append(peer.inviteeMQTTDevID)
|
|
if self.inviteeMQTTDevIDs:
|
|
args += [ '--force-invite' ]
|
|
for idid in self.inviteeMQTTDevIDs:
|
|
asHexStr = '{:016X}'.format(idid)
|
|
args += ['--invitee-mqtt-devid', asHexStr]
|
|
|
|
self.proc = subprocess.Popen(args, stdout = subprocess.DEVNULL,
|
|
stderr = subprocess.PIPE, universal_newlines = True)
|
|
self.pid = self.proc.pid
|
|
self.minEnd = datetime.datetime.now() + datetime.timedelta(seconds = self.args.MINRUN)
|
|
|
|
# Now start a thread to read stdio
|
|
self.reader = threading.Thread(target = logReaderStub, args=(self,))
|
|
self.reader.isDaemon = True
|
|
self.reader.start()
|
|
|
|
def running(self):
|
|
return self.proc and not self.proc.poll()
|
|
|
|
def minTimeExpired(self):
|
|
assert self.proc
|
|
return self.minEnd < datetime.datetime.now()
|
|
|
|
def kill(self):
|
|
if self.proc.poll() is None:
|
|
self.proc.terminate()
|
|
self.proc.wait()
|
|
assert self.proc.poll() is not None
|
|
|
|
self.reader.join()
|
|
self.reader = None
|
|
|
|
if self.statusSocketPath:
|
|
self.statusReader.join(timeout=2)
|
|
if self.statusReader.is_alive():
|
|
print('join() for {} failed; everything ok?'.format(self.statusSocketPath))
|
|
os.unlink(self.statusSocketPath)
|
|
else:
|
|
print('NOT killing')
|
|
self.proc = None
|
|
self.check_games_over()
|
|
|
|
def handleAllDone(self):
|
|
global gDeadLaunches
|
|
if self.allDone:
|
|
self.moveFiles()
|
|
gDeadLaunches += self.launchCount
|
|
return self.allDone
|
|
|
|
def moveFiles(self):
|
|
assert not self.running()
|
|
for fil in [ self.logPath, self.db, self.script ]:
|
|
shutil.move(fil, self.args.LOGDIR + '/done')
|
|
|
|
def getTilesCount(self):
|
|
assert not self.locked
|
|
return {'index': self.indx,
|
|
'nTilesLeftPool': self.nTilesLeftPool,
|
|
'nTilesLeftTray': self.nTilesLeftTray,
|
|
'launchCount': self.launchCount,
|
|
'nGames': self.nGames,
|
|
'game': self.game,
|
|
}
|
|
|
|
def update_ldevid(self):
|
|
if not self.app in Device.sHasLDevIDMap:
|
|
hasLDevID = False
|
|
proc = subprocess.Popen([self.app, '--help'], stderr=subprocess.PIPE)
|
|
# output, err, = proc.communicate()
|
|
for line in proc.stderr.readlines():
|
|
if b'--ldevid' in line:
|
|
hasLDevID = True
|
|
break
|
|
# print('found --ldevid:', hasLDevID);
|
|
Device.sHasLDevIDMap[self.app] = hasLDevID
|
|
|
|
if Device.sHasLDevIDMap[self.app]:
|
|
RNUM = random.randint(0, 99)
|
|
if not self.devID:
|
|
if RNUM < 30:
|
|
self.devID = '--ldevid LINUX_TEST_%.5d_' % (self.indx)
|
|
elif RNUM < 10:
|
|
self.devID += 'x'
|
|
|
|
def check_games_over(self):
|
|
data = json.loads(self.statusData)
|
|
self.gamesOver = data.get('allDone', False)
|
|
self.nTilesLeftPool = data.get('nTiles')
|
|
self.nGames = data.get('nGames', 0)
|
|
|
|
if self.gamesOver and not self.allDone:
|
|
allDone = True
|
|
for dev in self.peers:
|
|
if dev == self: continue
|
|
if not dev.gamesOver:
|
|
allDone = False
|
|
break
|
|
|
|
if allDone:
|
|
for dev in self.peers:
|
|
assert self.game == dev.game
|
|
dev.allDone = True
|
|
|
|
def makeSMSPhoneNo( game, dev ):
|
|
return '{:03d}{:03d}'.format( game, dev )
|
|
|
|
def build_cmds(args):
|
|
devs = []
|
|
COUNTER = 0
|
|
|
|
for GAME in range(1, args.NGAMES + 1):
|
|
peers = set()
|
|
NDEVS = pick_ndevs(args)
|
|
LOCALS = figure_locals(args, NDEVS) # as array
|
|
NPLAYERS = sum(LOCALS)
|
|
assert(len(LOCALS) == NDEVS)
|
|
DICT = args.DICTS[GAME % len(args.DICTS)]
|
|
# make one in three games public
|
|
useDupeMode = random.randint(0, 100) < args.DUP_PCT
|
|
if args.PHONIES == -1: phonies = GAME % 3
|
|
else: phonies = args.PHONIES
|
|
DEV = 0
|
|
for NLOCALS in LOCALS:
|
|
DEV += 1
|
|
DB = '{}/{:02d}_{:02d}_DB.sql3'.format(args.LOGDIR, GAME, DEV)
|
|
LOG = '{}/{:02d}_{:02d}_LOG.txt'.format(args.LOGDIR, GAME, DEV)
|
|
SCRIPT = '{}/start_{:02d}_{:02d}.sh'.format(args.LOGDIR, GAME, DEV)
|
|
|
|
PARAMS = player_params(args, NLOCALS, NPLAYERS, DEV)
|
|
if not args.USE_GTK: PARAMS += ['--curses']
|
|
PARAMS += ['--board-size', '15', '--sort-tiles']
|
|
if not useDupeMode: PARAMS += ['--trade-pct', args.TRADE_PCT]
|
|
|
|
# We SHOULD support having both SMS and relay working...
|
|
if args.WITH_SMS:
|
|
PARAMS += [ '--sms-number', makeSMSPhoneNo(GAME, DEV) ]
|
|
if args.SMS_FAIL_PCT > 0:
|
|
PARAMS += [ '--sms-fail-pct', args.SMS_FAIL_PCT ]
|
|
if DEV == 1:
|
|
PARAMS += [ '--force-invite' ]
|
|
for dev in range(2, NDEVS + 1):
|
|
PARAMS += [ '--invitee-sms-number', makeSMSPhoneNo(GAME, dev) ]
|
|
|
|
PARAMS += [ '--mqtt-port', args.MQTT_PORT, '--mqtt-host', args.MQTT_HOST ]
|
|
if args.WITH_MQTT:
|
|
PARAMS += [ '--with-mqtt' ]
|
|
if DEV == 1:
|
|
PARAMS += [ '--force-invite' ]
|
|
else:
|
|
PARAMS += [ '--without-mqtt' ]
|
|
|
|
if args.UNDO_PCT > 0:
|
|
PARAMS += ['--undo-pct', args.UNDO_PCT]
|
|
PARAMS += [ '--game-dict', DICT]
|
|
# Removing --slow-robot for now. With it on (and
|
|
# successfully passed through to the curses client, which
|
|
# hasn't always been happening), 20% of games
|
|
# stall. PENDING...
|
|
# PARAMS += ['--slow-robot', '1:3']
|
|
PARAMS += ['--skip-confirm']
|
|
PARAMS += ['--db', DB]
|
|
|
|
PARAMS += ['--drop-nth-packet', g_DROP_N]
|
|
PARAMS += ['--split-packets', '2']
|
|
if args.SEND_CHAT:
|
|
PARAMS += ['--send-chat', args.SEND_CHAT]
|
|
|
|
if args.DUP_PACKETS:
|
|
PARAMS += ['--dup-packets']
|
|
# PARAMS += ['--my-port', '1024']
|
|
# PARAMS += ['--savefail-pct', 10]
|
|
|
|
# With the --seed param passed, games with more than 2
|
|
# devices don't get going. No idea why. This param is NOT
|
|
# passed in the old bash version of this script, so fixing
|
|
# it isn't a priority.
|
|
# PARAMS += ['--seed', args.SEED]
|
|
|
|
if DEV == 1:
|
|
PARAMS += ['--force-game']
|
|
PARAMS += ['--server', '--phonies', phonies ]
|
|
if 0 == args.TRAYSIZE: traySize = random.randint(7, 9)
|
|
else: traySize = args.TRAYSIZE
|
|
PARAMS += ['--tray-size', traySize] # randint() is *inclusive*
|
|
# IFF there are any non-1 player counts, tell inviter which
|
|
if sum(LOCALS) > NDEVS:
|
|
PARAMS += ['--invitee-counts', ":".join(str(n) for n in LOCALS[1:])]
|
|
else:
|
|
PARAMS += ['--force-channel', DEV - 1]
|
|
if args.PHONY_PCT and phonies == 2: PARAMS += [ '--make-phony-pct', args.PHONY_PCT ]
|
|
|
|
if useDupeMode: PARAMS += ['--duplicate-mode']
|
|
|
|
PARAMS += ['--board-size', args.BOARD_SIZE]
|
|
|
|
# print('PARAMS:', PARAMS)
|
|
|
|
dev = Device( args, GAME, COUNTER, PARAMS, peers,
|
|
DEV, DB, LOG, SCRIPT, len(LOCALS), useDupeMode )
|
|
peers.add(dev)
|
|
dev.update_ldevid()
|
|
devs.append(dev)
|
|
|
|
COUNTER += 1
|
|
return devs
|
|
|
|
def summarizeTileCounts(devs, endTime, state, changeSecs, endRematchTime):
|
|
global gDeadLaunches
|
|
shouldGoOn = True
|
|
now = datetime.datetime.now()
|
|
data = [dev.getTilesCount() for dev in devs]
|
|
dupModeFlags = [dev.inDupMode for dev in devs]
|
|
nDevs = len(data)
|
|
totalTilesStd = 0
|
|
totalTilesDup = 0
|
|
colWidth = max(2, len(str(nDevs)))
|
|
headWidth = 0
|
|
fmtData = [{'head' : 'dev', },
|
|
{'head' : 'launches', },
|
|
{'head' : 'nGames', },
|
|
{'head' : 'tls left', },
|
|
]
|
|
for datum in fmtData:
|
|
headWidth = max(headWidth, len(datum['head']))
|
|
datum['data'] = []
|
|
|
|
# Group devices by game
|
|
games = []
|
|
joinChars = []
|
|
prev = -1
|
|
for datum, inDupMode in zip(data, dupModeFlags):
|
|
gameNo = datum['game']
|
|
if gameNo != prev:
|
|
games.append([])
|
|
if inDupMode: joinChars.append('.')
|
|
else: joinChars.append('+')
|
|
prev = gameNo
|
|
games[-1].append('{:0{width}d}'.format(datum['index'], width=colWidth))
|
|
|
|
fmtData[0]['data'] = []
|
|
for game, joinChar in zip(games, joinChars):
|
|
fmtData[0]['data'].append( joinChar.join(game) )
|
|
|
|
nLaunches = gDeadLaunches
|
|
for datum, inDupMode in zip(data, dupModeFlags):
|
|
launchCount = datum['launchCount']
|
|
nLaunches += launchCount
|
|
fmtData[1]['data'].append('{:{width}d}'.format(launchCount, width=colWidth))
|
|
|
|
nGames = datum['nGames']
|
|
fmtData[2]['data'].append('{:{width}d}'.format(nGames, width=colWidth))
|
|
|
|
# Format tiles left. It's the number in the bag/pool until
|
|
# that drops to 0, then the number in the tray preceeded by
|
|
# '+'. Only the pool number is included in the totalTiles sum.
|
|
nTilesPool = datum['nTilesLeftPool']
|
|
nTilesTray = datum['nTilesLeftTray']
|
|
if nTilesPool is None and nTilesTray is None:
|
|
txt = ('-' * colWidth)
|
|
elif int(nTilesPool) == 0 and not nTilesTray is None:
|
|
txt = '{:+{width}d}'.format(nTilesTray, width=colWidth-1)
|
|
else:
|
|
txt = '{:{width}d}'.format(nTilesPool, width=colWidth)
|
|
if inDupMode: totalTilesDup += int(nTilesPool)
|
|
else: totalTilesStd += int(nTilesPool)
|
|
fmtData[3]['data'].append(txt)
|
|
|
|
print('')
|
|
if totalTilesDup: dupDetails = ' (std: {}, dup: {})'.format(totalTilesStd, totalTilesDup)
|
|
else: dupDetails = ''
|
|
# here
|
|
rematchTime = 'Rematch: '
|
|
if endRematchTime < now:
|
|
rematchTime += 'off'
|
|
else:
|
|
secs = int((endRematchTime - now).total_seconds())
|
|
rematchTime += '{} secs left'.format(secs)
|
|
|
|
fmt = 'devs left: {nDevs}; bag tiles left: {total}{details}; total launches: ' \
|
|
+ '{nLaunches}; {now}/{endTime}; {rematchTime}'
|
|
print( fmt.format(nDevs=nDevs, total=totalTilesStd + totalTilesDup, details=dupDetails, \
|
|
nLaunches=nLaunches, now=datetime.datetime.now(), endTime=endTime,
|
|
rematchTime=rematchTime))
|
|
fmt = '{head:>%d} {data}' % headWidth
|
|
for datum in fmtData: datum['data'] = ' '.join(datum['data'])
|
|
for datum in fmtData:
|
|
print(fmt.format(**datum))
|
|
|
|
# Now let's see if things are stuck: if the tile string hasn't
|
|
# changed in two minutes bail. Note that the count of tiles left
|
|
# isn't enough because it's zero for a long time as devices are
|
|
# using up what's left in their trays and getting killed.
|
|
now = datetime.datetime.now()
|
|
tilesStr = fmtData[2]['data']
|
|
if not 'tilesStr' in state or state['tilesStr'] != tilesStr:
|
|
state['lastChange'] = now
|
|
state['tilesStr'] = tilesStr
|
|
|
|
return now - state['lastChange'] < datetime.timedelta(seconds = changeSecs)
|
|
|
|
def countCores(args):
|
|
count = 0
|
|
if args.CORE_PAT:
|
|
count = len( glob.glob(args.CORE_PAT) )
|
|
return count
|
|
|
|
gDone = False
|
|
|
|
def run_cmds(args, devs, startTime):
|
|
nCores = countCores(args)
|
|
endTime = startTime + datetime.timedelta(minutes = args.TIMEOUT_MINS)
|
|
endRematchTime = startTime + datetime.timedelta(seconds = args.REMATCH_SECS)
|
|
printState = {}
|
|
lastPrint = datetime.datetime.now()
|
|
|
|
while len(devs) > 0 and not gDone:
|
|
if countCores(args) > nCores:
|
|
print('core file count increased; exiting')
|
|
break
|
|
now = datetime.datetime.now()
|
|
if now > endTime:
|
|
print('outta time; outta here')
|
|
break
|
|
|
|
# print stats every 5 seconds
|
|
if now - lastPrint > datetime.timedelta(seconds = 5):
|
|
lastPrint = now
|
|
if not summarizeTileCounts(devs, endTime, printState,
|
|
args.NO_CHANGE_SECS, endRematchTime):
|
|
print('no change in too long; exiting')
|
|
break
|
|
|
|
dev = random.choice(devs)
|
|
if not dev.running():
|
|
if dev.handleAllDone():
|
|
devs.remove(dev)
|
|
else:
|
|
canRematch = now < endRematchTime
|
|
dev.launch(canRematch)
|
|
elif dev.minTimeExpired():
|
|
dev.kill()
|
|
if dev.handleAllDone():
|
|
devs.remove(dev)
|
|
else:
|
|
time.sleep(1.0)
|
|
print('.', end='', flush=True)
|
|
|
|
# if we get here via a break, kill any remaining games
|
|
if devs:
|
|
print('stopping {} remaining games'.format(len(devs)))
|
|
for dev in devs:
|
|
if dev.running(): dev.kill()
|
|
|
|
def log_scores( devs ):
|
|
if len(Device.sScoresReg) > 0:
|
|
print( "average score for regular games:",
|
|
sum(Device.sScoresReg) // len(Device.sScoresReg) )
|
|
if len(Device.sScoresDup) > 0:
|
|
print( "average score for dup games:",
|
|
sum(Device.sScoresDup) // len(Device.sScoresDup) )
|
|
|
|
def mkParser():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--send-chat', dest = 'SEND_CHAT', type = str, default = None,
|
|
help = 'the message to send')
|
|
|
|
parser.add_argument('--app-new', dest = 'APP_NEW', default = './obj_linux_memdbg/xwords',
|
|
help = 'the app we\'ll use')
|
|
parser.add_argument('--app-old', dest = 'APP_OLD', default = './obj_linux_memdbg/xwords',
|
|
help = 'the app we\'ll upgrade from')
|
|
parser.add_argument('--start-pct', dest = 'START_PCT', default = 50, type = int,
|
|
help = 'odds of starting with the new app, 0 <= n < 100')
|
|
parser.add_argument('--upgrade-pct', dest = 'UPGRADE_PCT', default = 20, type = int,
|
|
help = 'odds of upgrading at any launch, 0 <= n < 100')
|
|
|
|
parser.add_argument('--num-games', dest = 'NGAMES', type = int, default = 1, help = 'number of games')
|
|
parser.add_argument('--timeout-mins', dest = 'TIMEOUT_MINS', default = 10000, type = int,
|
|
help = 'minutes after which to timeout')
|
|
parser.add_argument('--nochange-secs', dest = 'NO_CHANGE_SECS', default = 30, type = int,
|
|
help = 'seconds without change after which to timeout')
|
|
parser.add_argument('--log-root', dest='LOGROOT', default = '.', help = 'where logfiles go')
|
|
parser.add_argument('--dup-packets', dest = 'DUP_PACKETS', default = False, action = 'store_true',
|
|
help = 'send all packet twice')
|
|
parser.add_argument('--phonies', dest = 'PHONIES', default = -1, type = int,
|
|
help = '0 (ignore), 1 (warn)) or 2 (lose turn); default is pick at random')
|
|
parser.add_argument('--make-phony-pct', dest = 'PHONY_PCT', default = 20, type = int,
|
|
help = 'how often a robot should play a phony (only applies when --phonies==2')
|
|
parser.add_argument('--use-gtk', dest = 'USE_GTK', default = False, action = 'store_true',
|
|
help = 'run games using gtk instead of ncurses')
|
|
|
|
parser.add_argument('--dup-pct', dest = 'DUP_PCT', default = 0, type = int,
|
|
help = 'this fraction played in duplicate mode')
|
|
|
|
# #
|
|
# # echo " [--clean-start] \\" >&2
|
|
parser.add_argument('--game-dict', dest = 'DICTS', action = 'append', default = [])
|
|
# # echo " [--help] \\" >&2
|
|
# # echo " [--max-devs <int>] \\" >&2
|
|
parser.add_argument('--min-devs', dest = 'MINDEVS', type = int, default = 2,
|
|
help = 'No game will have fewer devices than this')
|
|
parser.add_argument('--max-devs', dest = 'MAXDEVS', type = int, default = 4,
|
|
help = 'No game will have more devices than this')
|
|
|
|
parser.add_argument('--robots-all-same-iq', dest = 'IQS_SAME', default = False,
|
|
action = 'store_true', help = 'give all robots the same IQ')
|
|
|
|
parser.add_argument('--min-run', dest = 'MINRUN', type = int, default = 2,
|
|
help = 'Keep each run alive at least this many seconds')
|
|
# # echo " [--new-app <path/to/app] \\" >&2
|
|
# # echo " [--new-app-args [arg*]] # passed only to new app \\" >&2
|
|
# # echo " [--num-rooms <int>] \\" >&2
|
|
# # echo " [--old-app <path/to/app]* \\" >&2
|
|
parser.add_argument('--one-per', dest = 'ONEPER', default = False,
|
|
action = 'store_true', help = 'force one player per device')
|
|
parser.add_argument('--resign-pct', dest = 'RESIGN_PCT', default = 0, type = int, \
|
|
help = 'Odds of resigning [0..100]')
|
|
parser.add_argument('--seed', type = int, dest = 'SEED',
|
|
default = random.randint(1, 1000000000))
|
|
# # echo " [--send-chat <interval-in-seconds> \\" >&2
|
|
# # echo " [--udp-incr <pct>] \\" >&2
|
|
# # echo " [--udp-start <pct>] # default: $UDP_PCT_START \\" >&2
|
|
# # echo " [--undo-pct <int>] \\" >&2
|
|
|
|
parser.add_argument('--undo-pct', dest = 'UNDO_PCT', default = 0, type = int)
|
|
parser.add_argument('--trade-pct', dest = 'TRADE_PCT', default = 10, type = int)
|
|
|
|
parser.add_argument('--with-sms', dest = 'WITH_SMS', action = 'store_true')
|
|
parser.add_argument('--without-sms', dest = 'WITH_SMS', default = False, action = 'store_false')
|
|
parser.add_argument('--sms-fail-pct', dest = 'SMS_FAIL_PCT', default = 0, type = int)
|
|
|
|
parser.add_argument('--with-mqtt', dest = 'WITH_MQTT', default = True, action = 'store_true')
|
|
parser.add_argument('--without-mqtt', dest = 'WITH_MQTT', action = 'store_false')
|
|
parser.add_argument('--mqtt-port', dest = 'MQTT_PORT', default = 1883 )
|
|
parser.add_argument('--mqtt-host', dest = 'MQTT_HOST', default = 'localhost' )
|
|
|
|
parser.add_argument('--force-tray', dest = 'TRAYSIZE', default = 0, type = int,
|
|
help = 'Always this many tiles per tray')
|
|
|
|
parser.add_argument('--board-size', dest = 'BOARD_SIZE', type = int, default = 15,
|
|
help = 'Use <n>x<n> size board')
|
|
parser.add_argument('--rematch-limit-secs', dest = 'REMATCH_SECS', type = int, default = 0,
|
|
help = 'rematch games that end within this many seconds of script launch')
|
|
|
|
envpat = 'DISCON_COREPAT'
|
|
parser.add_argument('--core-pat', dest = 'CORE_PAT', default = os.environ.get(envpat),
|
|
help = "pattern for core files that should stop the script " \
|
|
+ "(default from env {})".format(envpat) )
|
|
|
|
parser.add_argument('--with-valgrind', dest = 'VALGRIND', default = False,
|
|
action = 'store_true')
|
|
|
|
return parser
|
|
|
|
def parseArgs():
|
|
args = mkParser().parse_args()
|
|
assignDefaults(args)
|
|
print(args)
|
|
return args
|
|
# print(options)
|
|
|
|
def assignDefaults(args):
|
|
if len(args.DICTS) == 0: args.DICTS.append('CollegeEng_2to8.xwd')
|
|
args.LOGDIR = os.path.splitext(os.path.basename(sys.argv[0]))[0] + '_logs'
|
|
# Move an existing logdir aside
|
|
if os.path.exists(args.LOGDIR):
|
|
shutil.move(args.LOGDIR, '/tmp/' + args.LOGDIR + '_' + str(random.randint(0, 100000)))
|
|
for d in ['', 'done', 'dead',]:
|
|
os.mkdir(args.LOGDIR + '/' + d)
|
|
|
|
def termHandler(signum, frame):
|
|
global gDone
|
|
print('termHandler() called')
|
|
gDone = True
|
|
|
|
def main():
|
|
startTime = datetime.datetime.now()
|
|
signal.signal(signal.SIGINT, termHandler)
|
|
|
|
args = parseArgs()
|
|
# Hack: old files confuse things. Remove is simple fix good for now
|
|
if args.WITH_SMS:
|
|
try: rmtree('/tmp/xw_sms')
|
|
except: None
|
|
devs = build_cmds(args)
|
|
nDevs = len(devs)
|
|
run_cmds(args, devs, startTime)
|
|
print('{} finished; took {} for {} devices'.format(sys.argv[0], datetime.datetime.now() - startTime, nDevs))
|
|
log_scores( devs )
|
|
|
|
##############################################################################
|
|
if __name__ == '__main__':
|
|
main()
|