mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2025-01-04 23:02:02 +01:00
798 lines
30 KiB
Python
Executable file
798 lines
30 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import argparse, datetime, json, os, random, shutil, signal, \
|
|
socket, struct, subprocess, sys, threading, time
|
|
|
|
g_ROOT_NAMES = ['Brynn', 'Ariela', 'Kati', 'Eric']
|
|
# These must correspond to what the linux app is looking for in roFromStr()
|
|
g_ROS = ['same', 'low_score_first', 'high_score_first', 'juggle',]
|
|
gDone = False
|
|
gGamesMade = 0
|
|
g_LOGFILE = None
|
|
|
|
def log(args, msg):
|
|
now = datetime.datetime.strftime(datetime.datetime.now(), '%X.%f')
|
|
msg = '{} {}'.format(now, msg)
|
|
if args.VERBOSE:
|
|
print(msg)
|
|
global g_LOGFILE
|
|
if g_LOGFILE:
|
|
print(msg, file=g_LOGFILE)
|
|
g_LOGFILE.flush()
|
|
|
|
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
|
|
|
|
def makeNames(nDevs):
|
|
names = g_ROOT_NAMES[:nDevs]
|
|
for ii in range(len(names), nDevs):
|
|
newName = '{}{:02}'.format(g_ROOT_NAMES[ii%len(g_ROOT_NAMES)], ii//len(g_ROOT_NAMES))
|
|
names += [newName]
|
|
return names
|
|
|
|
def chooseNames(nPlayers):
|
|
global g_NAMES
|
|
players = g_NAMES[:]
|
|
result = []
|
|
for ii in range(nPlayers):
|
|
indx = random.randint(0, len(players)-1)
|
|
result.append(players.pop(indx))
|
|
return result
|
|
|
|
class GameInfo():
|
|
def __init__(self, device, gid, rematchLevel):
|
|
self.device = device
|
|
self.gid = gid
|
|
self.state = {}
|
|
assert isinstance(rematchLevel, int)
|
|
self.rematchLevel = rematchLevel
|
|
|
|
def setGid(self, gid):
|
|
# set only once!
|
|
assert 8 == len(gid) and not self.gid
|
|
self.gid = gid
|
|
|
|
def gameOver(self):
|
|
return self.state.get('gameOver', False)
|
|
|
|
def getDevice(self): return self.device
|
|
|
|
class GuestGameInfo(GameInfo):
|
|
def __init__(self, device, gid, rematchLevel):
|
|
super().__init__(device, gid, rematchLevel)
|
|
|
|
class HostGameInfo(GameInfo):
|
|
def __init__(self, device, guestNames, **kwargs):
|
|
super().__init__(device, kwargs.get('gid'), kwargs.get('rematchLevel'))
|
|
self.guestNames = guestNames
|
|
self.needsInvite = kwargs.get('needsInvite', True)
|
|
self.orderedPlayers = None
|
|
global gGamesMade;
|
|
gGamesMade += 1
|
|
|
|
def haveOrder(self): return self.orderedPlayers is not None
|
|
|
|
def setOrder(self, orderedPlayers): self.orderedPlayers = orderedPlayers
|
|
|
|
def __str__(self):
|
|
return 'gid: {}, guests: {}'.format(self.gid, self.guestNames)
|
|
|
|
class GameStatus():
|
|
_statuses = None
|
|
_prevLines = []
|
|
_lastChange = datetime.datetime.now()
|
|
|
|
def __init__(self, gid):
|
|
self.gid = gid
|
|
self.players = []
|
|
self.allOver = True
|
|
self.hostPlayerName = None
|
|
self.hostName = None
|
|
|
|
def harvest(self, dev):
|
|
self.players.append(dev.host)
|
|
self.allOver = self.allOver and dev.gameOver(self.gid)
|
|
|
|
def sortPlayers(self):
|
|
game = Device.getHostGame(self.gid)
|
|
orderedPlayers = game.orderedPlayers
|
|
if orderedPlayers:
|
|
assert len(orderedPlayers) == len(self.players)
|
|
self.players = orderedPlayers
|
|
self.hostName = game.getDevice().host
|
|
|
|
# Build a gid->status map for each game, querying each device in
|
|
# the game for details
|
|
@staticmethod
|
|
def makeAll():
|
|
statuses = {}
|
|
for dev in Device.getAll():
|
|
for game in dev._allGames():
|
|
gid = game.gid
|
|
assert 8 == len(gid)
|
|
if not gid in statuses: statuses[gid] = GameStatus(gid)
|
|
statuses[gid].harvest(dev)
|
|
|
|
for status in statuses.values():
|
|
status.sortPlayers()
|
|
|
|
GameStatus._statuses = statuses
|
|
|
|
@staticmethod
|
|
def numLines():
|
|
maxPlayers = 0
|
|
for status in GameStatus._statuses.values():
|
|
nPlayers = len(status.players)
|
|
if nPlayers > maxPlayers: maxPlayers = nPlayers
|
|
return maxPlayers + 1
|
|
|
|
# For all games, print the proper line of status for that game in
|
|
# exactly 8 chars
|
|
@staticmethod
|
|
def line(indx):
|
|
results = []
|
|
for gid in sorted(GameStatus._statuses.keys()):
|
|
status = GameStatus._statuses[gid]
|
|
line = ''
|
|
if indx == 0:
|
|
line = gid
|
|
elif indx <= len(status.players) and not status.allOver:
|
|
player = status.players[indx-1]
|
|
hostMarker = status.hostName == player and '*' or ' '
|
|
initial = GameStatus._abbrev(player)
|
|
arg3 = -1
|
|
dev = Device._devs.get(player)
|
|
gameState = dev.gameFor(gid).state
|
|
if gameState:
|
|
if gameState.get('gameOver', False):
|
|
initial = initial.lower()
|
|
arg3 = gameState.get('nPending', 0)
|
|
else:
|
|
arg3 = gameState.get('nTiles')
|
|
line = '{}{:3}{: 3}'.format(hostMarker, initial, arg3)
|
|
results.append(line.center(len(gid)))
|
|
|
|
return ' '.join(results)
|
|
|
|
@staticmethod
|
|
def _abbrev(name):
|
|
for base in g_ROOT_NAMES:
|
|
if name.startswith(base):
|
|
return name[0] + name.strip(base)
|
|
|
|
class Device():
|
|
_devs = {}
|
|
_logdir = None
|
|
_nSteps = 0
|
|
|
|
@staticmethod
|
|
def setup(logdir):
|
|
Device._logdir = logdir
|
|
|
|
def __init__(self, args, host):
|
|
self.args = args
|
|
self.endTime = None
|
|
self.mqttDevID = None
|
|
self.smsNumber = args.WITH_SMS and '{}_phone'.format(host) or None
|
|
self.host = host
|
|
self.hostedGames = [] # array of HostGameInfo for each game I host
|
|
self.guestGames = []
|
|
self.script = '{}/{}.sh'.format(Device._logdir, host)
|
|
self.dbName = '{}/{}.db'.format(Device._logdir, host)
|
|
self.logfile = '{}/{}_log.txt'.format(Device._logdir, host)
|
|
self.cmdSocketName = '{}/{}.sock'.format(Device._logdir, host)
|
|
self._keyCur = 10000 * (1 + g_NAMES.index(host))
|
|
|
|
def init(self):
|
|
self._checkScript()
|
|
|
|
# called by thread proc
|
|
def _launchProc(self):
|
|
assert not self.endTime
|
|
self.endTime = datetime.datetime.now() + datetime.timedelta(seconds = 5)
|
|
args = [ self.script, '--close-stdin' ]
|
|
if not self.args.USE_GTK: args.append('--curses')
|
|
with open( self.logfile, 'a' ) as logfile:
|
|
subprocess.run(args, stdout = subprocess.DEVNULL,
|
|
stderr = logfile, universal_newlines = True)
|
|
self._log('_launchProc() (in thread): subprocess FINISHED')
|
|
os.unlink(self.cmdSocketName)
|
|
self.endTime = None
|
|
|
|
def launchIfNot(self):
|
|
if not self.endTime:
|
|
self.watcher = threading.Thread(target = Device.runnerStub, args=(self,))
|
|
self.watcher.isDaemon = True
|
|
self.watcher.start()
|
|
|
|
while not self.endTime or not os.path.exists(self.cmdSocketName):
|
|
time.sleep(0.2)
|
|
|
|
def moveOne(self):
|
|
moved = False
|
|
gids = [game.gid for game in self._allGames() if not self.gameOver(game.gid)]
|
|
random.shuffle(gids)
|
|
for gid in gids:
|
|
response = self._sendWaitReply('moveIf', gid=gid)
|
|
moved = response.get('success', False)
|
|
if moved: break
|
|
return moved
|
|
|
|
def _sendWaitReply(self, cmd, **kwargs):
|
|
self.launchIfNot()
|
|
|
|
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
# print('connecting to: {}'.format(self.cmdSocketName))
|
|
|
|
client.connect(self.cmdSocketName);
|
|
|
|
key = self._nextKey()
|
|
params = [{'cmd': cmd, 'key': key, 'args': {**kwargs}}]
|
|
payload = json.dumps(params).encode()
|
|
client.send(struct.pack('!h', len(payload)))
|
|
client.sendall(payload)
|
|
|
|
# # Receive a response from the server
|
|
# self._log('_sendWaitReply({}): calling recv()'.format(cmd))
|
|
reslen = struct.unpack('!h', client.recv(2))[0]
|
|
response = client.recv(reslen).decode()
|
|
# self._log('_sendWaitReply({}): recv => str: {}'.format(cmd, response))
|
|
response = json.loads(response)
|
|
self._log('_sendWaitReply({}, {}): recv => {}'.format(cmd, kwargs, response))
|
|
assert 1 == len(response)
|
|
response = response[0]
|
|
assert response.get('key', 0) == key
|
|
assert response.get('cmd') == cmd
|
|
response = response.get('response')
|
|
|
|
client.close()
|
|
return response
|
|
|
|
def _nextKey(self):
|
|
self._keyCur += 1
|
|
return self._keyCur
|
|
|
|
def setDevID(self):
|
|
response = self._sendWaitReply('getMQTTDevID')
|
|
mqttDevID = response and response.get('mqtt')
|
|
if mqttDevID:
|
|
self.mqttDevID = mqttDevID
|
|
else:
|
|
printError('no mqtt or no response')
|
|
|
|
def makeGames(self):
|
|
args = self.args
|
|
for remote in self.hostedGames:
|
|
nPlayers = 1 + len(remote.guestNames)
|
|
hostPosn = random.randint(0, nPlayers-1)
|
|
traySize = 0 == args.TRAY_SIZE and random.randint(7, 9) or args.TRAY_SIZE
|
|
|
|
response = self._sendWaitReply('makeGame', nPlayers=nPlayers, hostPosn=hostPosn,
|
|
dict=args.DICTS[0], boardSize=args.BOARD_SIZE,
|
|
traySize=traySize)
|
|
newGid = response.get('newGid')
|
|
if newGid:
|
|
remote.setGid(newGid)
|
|
|
|
# This is the heart of things. Do something as long as we have a
|
|
# game that needs to run.
|
|
def step(self):
|
|
# self._log('step() called for {}'.format(self))
|
|
stepped = False
|
|
for game in self.hostedGames:
|
|
if game.needsInvite:
|
|
self.invite(game)
|
|
stepped = True
|
|
break
|
|
|
|
if not stepped:
|
|
if not self.endTime:
|
|
self.launchIfNot()
|
|
elif datetime.datetime.now() > self.endTime:
|
|
self.rematchOrQuit()
|
|
elif self.moveOne():
|
|
pass
|
|
else:
|
|
# self._log('sleeping with {} to go'.format(self.endTime-now))
|
|
time.sleep(0.5)
|
|
stepped = True;
|
|
|
|
# I may be a guest or a host in this game. Rematch works either
|
|
# way. But how I figure out the other players differs.
|
|
def rematch(self, game):
|
|
gid = game.gid
|
|
rematchOrder = self.figureRematchOrder()
|
|
newGid = self._sendWaitReply('rematch', gid=gid, rematchOrder=rematchOrder) \
|
|
.get('newGid')
|
|
if newGid:
|
|
guests = Device.playersIn(gid)
|
|
guests.remove(self.host)
|
|
self._log('rematch: new host: {}; new guest[s]: {}, gid: {}'.format(self.host, guests, newGid))
|
|
|
|
rematchLevel = game.rematchLevel - 1
|
|
assert rematchLevel >= 0 # fired
|
|
self.hostedGames.append(HostGameInfo(self, guests, needsInvite=False, gid=newGid,
|
|
rematchLevel=rematchLevel))
|
|
for guest in guests:
|
|
Device.getForPlayer(guest).expectInvite(newGid, rematchLevel)
|
|
|
|
def invite(self, game):
|
|
failed = False
|
|
for ii in range(len(game.guestNames)):
|
|
guestDev = Device.getForPlayer(game.guestNames[ii])
|
|
|
|
addr = {}
|
|
if self.args.WITH_MQTT: addr['mqtt'] = guestDev.mqttDevID
|
|
if self.args.WITH_SMS: addr['sms'] = guestDev.smsNumber
|
|
response = self._sendWaitReply('invite', gid=game.gid,
|
|
channel=ii+1, addr=addr)
|
|
|
|
if response['success']:
|
|
guestDev.expectInvite(game.gid, game.rematchLevel)
|
|
else:
|
|
failed = True
|
|
if not failed: game.needsInvite = False
|
|
|
|
def expectInvite(self, gid, rematchLevel):
|
|
self.guestGames.append(GuestGameInfo(self, gid, rematchLevel))
|
|
self.launchIfNot()
|
|
|
|
def figureRematchOrder(self):
|
|
ro = self.args.REMATCH_ORDER
|
|
if not ro: ro = random.choice(g_ROS)
|
|
return ro
|
|
|
|
# Return true only if all games I host are finished on all games.
|
|
# But: what about games I don't host? For now, let's make it all
|
|
# games!
|
|
def finished(self):
|
|
allGames = self._allGames()
|
|
result = 0 < len(allGames)
|
|
for game in allGames:
|
|
if not result: break
|
|
peers = Device.devsWith(game.gid)
|
|
for dev in peers:
|
|
result = dev.gameOver(game.gid)
|
|
if not result: break
|
|
# if result: self._log('finished() => {}'.format(result))
|
|
return result
|
|
|
|
def gameOver(self, gid):
|
|
result = False
|
|
"""Is the game is over for *this* device"""
|
|
gameState = self.gameFor(gid).state
|
|
if gameState:
|
|
result = gameState.get('gameOver', False) and 0 == gameState.get('nPending', 1)
|
|
# if result: self._log('gameOver({}) => {}'.format(gid, result))
|
|
return result
|
|
|
|
# this device is stalled if none of its unfinshed games has
|
|
# changed state in some interval
|
|
def stalled(self):
|
|
return False
|
|
|
|
def _allGames(self):
|
|
return self.hostedGames + self.guestGames
|
|
|
|
def haveGame(self, gid):
|
|
withGid = [game for game in self._allGames() if gid == game.gid]
|
|
return 0 < len(withGid)
|
|
|
|
def gameFor(self, gid):
|
|
for game in self._allGames():
|
|
if gid == game.gid:
|
|
return game
|
|
|
|
def rematchOrQuit(self):
|
|
if self.endTime:
|
|
gids = [game.gid for game in self._allGames() if not self.gameOver(game.gid)]
|
|
|
|
orders = []
|
|
for gid in gids:
|
|
game = Device.getHostGame(gid)
|
|
if game and not game.haveOrder():
|
|
orders.append(gid)
|
|
|
|
response = self._sendWaitReply('getStates', gids=gids, orders=orders)
|
|
|
|
for order in response.get('orders', []):
|
|
gid = order.get('gid')
|
|
game = Device.getHostGame(gid)
|
|
assert isinstance(game, HostGameInfo) # firing
|
|
game.setOrder(order.get('players'))
|
|
|
|
anyRematched = False
|
|
for obj in response.get('states', []):
|
|
game = self.gameFor(obj.get('gid'))
|
|
game.state = obj
|
|
|
|
if game.gameOver() and 0 < game.rematchLevel:
|
|
self.rematch(game)
|
|
game.rematchLevel = 0 # so won't be used again
|
|
anyRematched = True
|
|
|
|
if not anyRematched:
|
|
response = self._sendWaitReply('quit')
|
|
self.watcher.join()
|
|
self.watcher = None
|
|
assert not self.endTime
|
|
|
|
def quit(self):
|
|
if self.endTime:
|
|
allGames = self._allGames()
|
|
gids = [game.gid for game in allGames if not self.gameOver(game.gid)]
|
|
response = self._sendWaitReply('quit', gids=gids)
|
|
|
|
for obj in response.get('states', []):
|
|
self.gameFor(obj.get('gid')).state = obj
|
|
|
|
# wait for the thing to actually die
|
|
self.watcher.join()
|
|
self.watcher = None
|
|
assert not self.endTime
|
|
|
|
def _checkScript(self):
|
|
if not os.path.exists(self.script):
|
|
scriptArgs = ['exec'] # without exec means terminate() won't work
|
|
if self.args.VALGRIND:
|
|
scriptArgs += ['valgrind']
|
|
# args += ['--leak-check=full']
|
|
# args += ['--track-origins=yes']
|
|
scriptArgs.append(self.args.APP_NEW) # + [str(p) for p in self.params]
|
|
|
|
scriptArgs += '--db', self.dbName, '--skip-confirm'
|
|
if self.args.SEND_CHAT:
|
|
scriptArgs += '--send-chat', self.args.SEND_CHAT
|
|
scriptArgs += '--localName', self.host
|
|
scriptArgs += '--cmd-socket-name', self.cmdSocketName
|
|
|
|
if self.args.WITH_MQTT:
|
|
scriptArgs += [ '--mqtt-port', self.args.MQTT_PORT, '--mqtt-host', self.args.MQTT_HOST ]
|
|
|
|
if self.args.WITH_SMS:
|
|
scriptArgs += [ '--sms-number', self.smsNumber ]
|
|
|
|
scriptArgs += ['--board-size', '15', '--sort-tiles']
|
|
|
|
# useDupeMode = random.randint(0, 100) < self.args.DUP_PCT
|
|
# if not useDupeMode: scriptArgs += ['--trade-pct', self.args.TRADE_PCT]
|
|
|
|
# if self.devID: args.extend( ' '.split(self.devID))
|
|
scriptArgs += [ '$*' ]
|
|
|
|
with open( self.script, 'w' ) as fil:
|
|
fil.write( "#!/bin/sh\n" )
|
|
fil.write( ' '.join([str(arg) for arg in scriptArgs]) + '\n' )
|
|
os.chmod(self.script, 0o755)
|
|
|
|
@staticmethod
|
|
def printStatus(statusSteps):
|
|
Device._nSteps += 1
|
|
print('.', end='', flush=True)
|
|
if 0 == Device._nSteps % statusSteps:
|
|
print()
|
|
GameStatus.makeAll()
|
|
|
|
lines = []
|
|
for line in range(GameStatus.numLines()):
|
|
txt = GameStatus.line(line)
|
|
print(txt)
|
|
lines.append(txt)
|
|
|
|
now = datetime.datetime.now()
|
|
if not lines == GameStatus._prevLines:
|
|
GameStatus._prevLines = lines
|
|
GameStatus._lastChange = now
|
|
else:
|
|
print('no change in {}'.format(now - GameStatus._lastChange))
|
|
|
|
@staticmethod
|
|
def deviceFor(args, host):
|
|
dev = Device._devs.get(host)
|
|
if not dev:
|
|
dev = Device(args, host)
|
|
Device._devs[host] = dev
|
|
return dev
|
|
|
|
@staticmethod
|
|
def playersIn(gid):
|
|
return [dev.host for dev in Device.devsWith(gid)]
|
|
|
|
@staticmethod
|
|
# return all devices (up to 4 of them) that are host or guest in a
|
|
# game with <gid>"""
|
|
def devsWith(gid):
|
|
result = [dev for dev in Device._devs.values() if dev.haveGame(gid)]
|
|
return result
|
|
|
|
@staticmethod
|
|
def getAll():
|
|
return [dev for dev in Device._devs.values()]
|
|
|
|
@staticmethod
|
|
def getForPlayer(player):
|
|
result = None
|
|
for dev in Device.getAll():
|
|
if dev.host == player:
|
|
result = dev
|
|
break;
|
|
assert result
|
|
return result
|
|
|
|
@staticmethod
|
|
def getHostGame(gid):
|
|
devs = Device.devsWith(gid)
|
|
for dev in devs:
|
|
game = dev.gameFor(gid)
|
|
if isinstance(game, HostGameInfo):
|
|
return game
|
|
assert False
|
|
|
|
def addGameWith(self, guests):
|
|
self.hostedGames.append(HostGameInfo(self, guests, needsInvite=True,
|
|
rematchLevel=self.args.REMATCH_LEVEL))
|
|
for guest in guests:
|
|
Device.deviceFor(self.args, guest) # in case this device never hosts
|
|
|
|
def _log(self, msg):
|
|
log(self.args, '{}: {}'.format(self.host, msg))
|
|
|
|
def __str__(self):
|
|
result = 'host: {}, devID: {}, with {} games: ' \
|
|
.format(self.host, self.mqttDevID,
|
|
len(self.hostedGames)+len(self.guestGames))
|
|
result += '{' + ', '.join(['{}'.format(game) for game in self._allGames()]) + '}'
|
|
result += ' running={}'.format(self.endTime is not None)
|
|
return result
|
|
|
|
@staticmethod
|
|
def runnerStub(self):
|
|
self._launchProc()
|
|
|
|
def openOnExit(args):
|
|
devs = Device.getAll()
|
|
for dev in devs:
|
|
appargs = [args.APP_NEW, '--db', dev.dbName]
|
|
if args.WITH_MQTT:
|
|
appargs += [ '--mqtt-port', args.MQTT_PORT, '--mqtt-host', args.MQTT_HOST ]
|
|
subprocess.Popen([str(arg) for arg in appargs], stdout = subprocess.DEVNULL,
|
|
stderr = subprocess.DEVNULL, universal_newlines = True)
|
|
|
|
def mainLoop(args, devs):
|
|
startCount = len(devs)
|
|
|
|
startTime = datetime.datetime.now()
|
|
nextStallCheck = startTime + datetime.timedelta(seconds = 20)
|
|
|
|
while 0 < len(devs):
|
|
if gDone:
|
|
print('gDone set; exiting loop')
|
|
break
|
|
dev = random.choice(devs)
|
|
dev.step()
|
|
if dev.finished():
|
|
dev.quit()
|
|
devs.remove(dev)
|
|
log(args, 'removed dev for {}; {} devs left'.format(dev.host, len(devs)))
|
|
|
|
now = datetime.datetime.now()
|
|
if devs and now > nextStallCheck:
|
|
nextStallCheck = now + datetime.timedelta(seconds = 10)
|
|
allStalled = True
|
|
for dev in devs:
|
|
if not dev.stalled():
|
|
allStalled = False
|
|
break
|
|
if allStalled:
|
|
log(args, 'exiting mainLoop with {} left (of {}) because all stalled' \
|
|
.format(len(devs), startCount))
|
|
break
|
|
|
|
if False and endTime < datetime.datetime.now():
|
|
log(args, 'exiting mainLoop with {} left (of {}) because out of time' \
|
|
.format(len(devs), startCount))
|
|
break
|
|
|
|
if not args.VERBOSE:
|
|
Device.printStatus(args.STATUS_STEPS)
|
|
|
|
for dev in devs:
|
|
print('killing {}'.format(dev.host))
|
|
dev.quit()
|
|
|
|
# We will build one Device for each player in the set of games, and
|
|
# prime each with enough information that when we start running them
|
|
# they can invite each other.
|
|
def build_devs(args):
|
|
global g_NAMES
|
|
g_NAMES = makeNames(args.NDEVS)
|
|
for ii in range(args.NGAMES):
|
|
nPlayers = pick_ndevs(args)
|
|
players = chooseNames(nPlayers)
|
|
host = players[0]
|
|
guests = players[1:]
|
|
|
|
Device.deviceFor(args, host).addGameWith(guests)
|
|
|
|
return Device.getAll()
|
|
|
|
def mkParser():
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument('--status-steps', dest = 'STATUS_STEPS', type = int, default = 20,
|
|
help = 'how many steps between status dumps (matters only if not --debug)')
|
|
|
|
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('--num-devs', dest = 'NDEVS', type = int, default = len(g_ROOT_NAMES), help = 'number of devices')
|
|
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 = 0)
|
|
# # 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 = 'TRAY_SIZE', 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-level', dest = 'REMATCH_LEVEL', type = int, default = 0,
|
|
help = 'rematch games down to this ancestry/depth')
|
|
parser.add_argument('--rematch-order', dest = 'REMATCH_ORDER', type = str, default = None,
|
|
help = 'order rematched games one of these ways: {}'.format(g_ROS))
|
|
|
|
# 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')
|
|
|
|
parser.add_argument('--debug', dest='VERBOSE', default=False, action='store_true',
|
|
help='log stuff')
|
|
parser.add_argument('--open-on-exit', dest = 'OPEN_ON_EXIT', default = False,
|
|
action = 'store_true', help='Open devs in gtk app when finished')
|
|
|
|
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')
|
|
|
|
def termHandler(signum, frame):
|
|
global gDone
|
|
print('termHandler() called')
|
|
gDone = True
|
|
|
|
def printError(msg): print( 'ERROR: {}'.format(msg))
|
|
|
|
def initLogs():
|
|
scriptName = os.path.splitext(os.path.basename(sys.argv[0]))[0]
|
|
logdir = scriptName + '_logs'
|
|
if os.path.exists(logdir):
|
|
shutil.rmtree(logdir)
|
|
# shutil.move(logdir, '/tmp/' + logdir + '_' + str(random.randint(0, 100000)))
|
|
os.mkdir(logdir)
|
|
|
|
logfilepath = '{}/{}_log.txt'.format(logdir, scriptName)
|
|
global g_LOGFILE
|
|
g_LOGFILE = open(logfilepath, 'w')
|
|
|
|
return logdir
|
|
|
|
def main():
|
|
startTime = datetime.datetime.now()
|
|
signal.signal(signal.SIGINT, termHandler)
|
|
|
|
logdir = initLogs()
|
|
|
|
args = parseArgs()
|
|
|
|
if args.SEED: random.seed(args.SEED)
|
|
# Hack: old files confuse things. Remove is simple fix good for now
|
|
if args.WITH_SMS:
|
|
try: rmtree('/tmp/xw_sms')
|
|
except: None
|
|
|
|
Device.setup(logdir) # deletes old logdif
|
|
devs = build_devs(args)
|
|
for dev in devs:
|
|
dev.init()
|
|
dev.launchIfNot()
|
|
time.sleep(1)
|
|
for dev in devs:
|
|
if args.WITH_MQTT: dev.setDevID()
|
|
for dev in devs:
|
|
dev.makeGames()
|
|
dev.quit()
|
|
mainLoop(args, devs)
|
|
|
|
elapsed = datetime.datetime.now() - startTime
|
|
print('played {} games in {}'.format(gGamesMade, elapsed))
|
|
|
|
if g_LOGFILE: g_LOGFILE.close()
|
|
|
|
if args.OPEN_ON_EXIT: openOnExit(args)
|
|
|
|
##############################################################################
|
|
if __name__ == '__main__':
|
|
main()
|