def step(d): startTime = time.perf_counter() d.state['gameStep'] += 1 d.state['serverSteps'] += 1 # for each bot that is alive, copy health to so we know what it was at the start of the step. aliveBots = {} for src, bot in d.bots.items(): if bot['health'] != 0: aliveBots[src] = bot['health'] # for all bots that are alive for src, bot in d.bots.items(): if src in aliveBots: # change speed if needed if bot['currentSpeed'] > bot['requestedSpeed']: bot['currentSpeed'] -= d.getClassValue('botAccRate', bot['class']) if bot['currentSpeed'] < bot['requestedSpeed']: bot['currentSpeed'] = bot['requestedSpeed'] elif bot['currentSpeed'] < bot['requestedSpeed']: bot['currentSpeed'] += d.getClassValue('botAccRate', bot['class']) if bot['currentSpeed'] > bot['requestedSpeed']: bot['currentSpeed'] = bot['requestedSpeed'] # change direction if needed if bot['currentDirection'] != bot['requestedDirection']: if bot['currentDirection'] != bot['requestedDirection'] and bot['currentSpeed'] == 0: # turn instanly if bot is not moving bot['currentDirection'] = bot['requestedDirection'] else: # how much can we turn at the speed we are going? turnRate = d.getClassValue('botMinTurnRate', bot['class']) \ + (d.getClassValue('botMaxTurnRate', bot['class']) - d.getClassValue('botMinTurnRate', bot['class'])) \ * (1 - bot['currentSpeed'] / 100) # if turn is negative and does not pass over 0 radians if bot['currentDirection'] > bot['requestedDirection'] and \ bot['currentDirection'] - bot['requestedDirection'] <= math.pi: bot['currentDirection'] -= turnRate if bot['currentDirection'] <= bot['requestedDirection']: bot['currentDirection'] = bot['requestedDirection'] # if turn is negative and passes over 0 radians, so we may need to normalize angle elif bot['requestedDirection'] > bot['currentDirection'] and \ bot['requestedDirection'] - bot['currentDirection'] >= math.pi: bot['currentDirection'] = nbmath.normalizeAngle(bot['currentDirection'] - turnRate) if bot['currentDirection'] <= bot['requestedDirection'] and bot['currentDirection'] >= bot['requestedDirection'] - math.pi: bot['currentDirection'] = bot['requestedDirection'] # if turn is positive and does not pass over 0 radians elif bot['requestedDirection'] > bot['currentDirection'] and \ bot['requestedDirection'] - bot['currentDirection'] <= math.pi: bot['currentDirection'] += turnRate if bot['requestedDirection'] <= bot['currentDirection']: bot['currentDirection'] = bot['requestedDirection'] # if turn is positive and passes over 0 radians elif bot['currentDirection'] > bot['requestedDirection'] and \ bot['currentDirection'] - bot['requestedDirection'] >= math.pi: bot['currentDirection'] = nbmath.normalizeAngle(bot['currentDirection'] + turnRate) if bot['currentDirection'] >= bot['requestedDirection'] and bot['currentDirection'] <= bot['requestedDirection'] + math.pi: bot['currentDirection'] = bot['requestedDirection'] # move bot if bot['currentSpeed'] != 0: bot['x'], bot['y'] = nbmath.project(bot['x'], bot['y'], bot['currentDirection'], bot['currentSpeed'] / 100.0 * d.getClassValue('botMaxSpeed', bot['class'])) # set starting hitSeverity to 0 for all robots. hitSeverity == 0 means robot did not # hit anything this step. for src, bot in d.bots.items(): bot['hitSeverity'] = 0.0 # do until we get one clean pass where no bot hitting wall, obstacle or other bot. foundOverlap = True while foundOverlap: foundOverlap = False # detect if bots hit walls. if they, did move them so they are just barely not touching, for src, bot in d.bots.items(): hitSeverity = 0 if bot['x'] - d.conf['botRadius'] < 0: # hit left side bot['x'] = d.conf['botRadius'] + 1 hitSeverity = getHitSeverity(d, bot, math.pi) if bot['x'] + d.conf['botRadius'] > d.conf['arenaSize']: # hit right side bot['x'] = d.conf['arenaSize'] - d.conf['botRadius'] - 1 hitSeverity = getHitSeverity(d, bot, 0) if bot['y'] - d.conf['botRadius'] < 0: # hit bottom side bot['y'] = d.conf['botRadius'] + 1 hitSeverity = getHitSeverity(d, bot, math.pi * 3 / 2) if bot['y'] + d.conf['botRadius'] > d.conf['arenaSize']: # hit top side bot['y'] = d.conf['arenaSize'] - d.conf['botRadius'] - 1 hitSeverity = getHitSeverity(d, bot, math.pi/2) if hitSeverity: foundOverlap = True bot['hitSeverity'] = max(bot['hitSeverity'], hitSeverity) # detect if bots hit obstacles, if the did move them so they are just barely not touching, overlap = findOverlapingBotsAndObstacles(d, d.bots) while overlap: foundOverlap = True b = d.bots[overlap[0]] o = overlap[1] # find angle to move bot directly away from obstacle a = nbmath.angle(o['x'], o['y'], b['x'], b['y']) # find min distance to move bot so it don't touch (plus 0.5 for safety). distance = d.conf['botRadius'] + o['radius'] + 0.5 - nbmath.distance(o['x'], o['y'], b['x'], b['y']) # move bot b['x'], b['y'] = nbmath.project(b['x'], b['y'], a, distance) # record damage hitSeverity = getHitSeverity(d, b, a + math.pi) b['hitSeverity'] = max(b['hitSeverity'], hitSeverity) # check for more bots overlapping overlap = findOverlapingBotsAndObstacles(d, d.bots) # detect if bots hit other bots, if the did move them so they are just barely not touching, overlap = findOverlapingBots(d, d.bots) while overlap: foundOverlap = True b1 = d.bots[overlap[0]] b2 = d.bots[overlap[1]] # find angle to move bot directly away from each other a = nbmath.angle(b1['x'], b1['y'], b2['x'], b2['y']) # find min distance to move each bot so they don't touch (plus 0.5 for saftly). between = nbmath.distance(b1['x'], b1['y'], b2['x'], b2['y']) distance = between / 2 - (between - d.conf['botRadius']) + 0.5 # move bots b1['x'], b1['y'] = nbmath.project(b1['x'], b1['y'], a + math.pi, distance) b2['x'], b2['y'] = nbmath.project(b2['x'], b2['y'], a, distance) # record damage hitSeverity = getHitSeverity(d, b1, a, b2) b1['hitSeverity'] = max(b1['hitSeverity'], hitSeverity) b2['hitSeverity'] = max(b2['hitSeverity'], hitSeverity) # check for more bots overlapping overlap = findOverlapingBots(d, d.bots) # give damage (only once this step) to bots that hit things. Also stop them. for src, bot in d.bots.items(): if bot['hitSeverity']: if d.conf['simpleCollisions']: bot['hitSeverity'] = 1 bot['health'] = max(0, bot['health'] - bot['hitSeverity'] * d.conf['hitDamage'] * d.getClassValue('botArmor', bot['class'])) bot['currentSpeed'] = 0 bot['requestedSpeed'] = 0 del bot['hitSeverity'] # for all shells for src in list(d.shells.keys()): shell = d.shells[src] # remember shells start point before moving oldx = shell['x'] oldy = shell['y'] # move shell distance = min(d.getClassValue('shellSpeed', d.bots[src]['class']), shell['distanceRemaining']) shell['x'], shell['y'] = nbmath.project(shell['x'], shell['y'], shell['direction'], distance) shell['distanceRemaining'] -= distance # did shell hit an obstacle? shellHitObstacle = False for o in d.conf['obstacles']: if nbmath.intersectLineCircle(oldx, oldy, shell['x'], shell['y'], o['x'], o['y'], o['radius']): shellHitObstacle = True # if did not hit an obstacle and shell's explosion would touch inside of arena if not shellHitObstacle and \ (shell['x'] > d.getClassValue('explRadius', d.bots[src]['class']) * -1 and shell['x'] < d.conf['arenaSize'] + d.getClassValue('explRadius', d.bots[src]['class']) and shell['y'] > d.getClassValue('explRadius', d.bots[src]['class']) * -1 and shell['y'] < d.conf['arenaSize'] + d.getClassValue('explRadius', d.bots[src]['class'])): # if shell has reached it destination then explode. if shell['distanceRemaining'] <= 0: # apply damage to bots. for k, bot in d.bots.items(): if bot['health'] > 0: distance = nbmath.distance(bot['x'], bot['y'], shell['x'], shell['y']) if distance < d.getClassValue('explRadius', d.bots[src]['class']): damage = d.getClassValue('explDamage', d.bots[src]['class']) * (1 - distance / d.getClassValue('explRadius', d.bots[src]['class'])) bot['health'] = max(0, bot['health'] - (damage * d.getClassValue('botArmor', bot['class']))) # allow recording of inflicting damage that is greater than health of hit robot. # also record damage to oneself. d.bots[src]['shellDamage'] += damage # store the explosion so viewers can display it. we can't use src as index because it is possible for two explosions # from same bot to exist (but not likly). d.explosions[d.state['explIndex']] = { 'x': shell['x'], 'y': shell['y'], 'stepsAgo': 0, 'src': src # this is needed by viewer to color this explosion based on the bot who fired it. } d.state['explIndex'] += 1 if d.state['explIndex'] > 65000: d.state['explIndex'] = 0 # this shell exploed so remove it del d.shells[src] else: # shell hit obstacle or left arena so remove it without exploding del d.shells[src] # Remove old explosions and add 1 to other explosions stepsAgo. # Note, We only keep these around so the viewer can do a nice animation # over a number of steps before they are removed. for key in list(d.explosions.keys()): expl = d.explosions[key] if expl['stepsAgo'] == d.conf['keepExplosionSteps']: del d.explosions[key] else: expl['stepsAgo'] += 1 # find how many points bots that died this step will get. (Based on how many bots have died previouly) if len(aliveBots) == d.conf['botsInGame']: points = 0 # first to die elif len(aliveBots) > d.conf['botsInGame'] / 2: points = 2 # died in first half else: points = 5 # died in second half # Kill all bots if we have reached the max steps and there is still more than one bot alive. if d.state['gameStep'] == d.conf['stepMax'] and len(aliveBots) != 1: log("Game reached stepMax with more than one bot alive. Killing all bots.") for src in aliveBots: d.bots[src]['health'] = 0 # Assign points to bots that died this turn for src in list(aliveBots.keys()): if d.bots[src]['health'] == 0: d.bots[src]['points'] += points del aliveBots[src] # If only one bot is left then end game. if len(aliveBots) == 1: src = list(aliveBots.keys())[0] d.bots[src]['winHealth'] += d.bots[src]['health'] d.bots[src]['winCount'] += 1 d.bots[src]['health'] = 0 d.bots[src]['points'] += 10 # last robot (winner) del aliveBots[src] d.state['stepTime'] += time.perf_counter() - startTime
def checkForUpdates(d): msg = {"type": "Error", "result": "We never got any new data from server."} try: # keep getting messages until we get the last one and then an exception is thrown. while True: msg, ip, port = d.viewerSocket.recvMessage() d.replayData.append(msg) while len(d.replayData) > d.replaySaveSteps / d.replaySaveEveryNth: d.replayData.pop(0) except nbipc.NetBotSocketException as e: # if message type is Error and we have not got good data for 100 steps then quit if msg['type'] == 'Error' and d.lastViewData + d.conf['stepSec'] * 100 < time.time(): # We didn't get anything from the buffer or it was an invalid message. d.canvas.itemconfigure(d.bigMsg, text="Server stopped sending data.") except Exception as e: log(str(e), "ERROR") quit() if msg['type'] == 'viewData': # turn replay on or off if space bar pressed if d.toggleReplaying: if d.isReplaying: d.playingData = [] d.toggleReplaying = False d.isReplaying = False else: d.playingData = [] d.playingData.extend(d.replayData) d.toggleReplaying = False d.isReplaying = True # play back and then remove data if d.isReplaying: if len(d.playingData) > 0: msg = d.playingData[0] d.playingData.pop(0) # replay is over else: d.isReplaying = False # draw red border on arena and red instant replay widget if d.isReplaying: d.canvas.config(highlightbackground='#FF0000') if d.replayWidget is None: d.replayWidget = t.Message(d.frame, width=200, justify='center') d.replayWidget.config(highlightbackground='#FF0000') d.replayWidget.config(highlightthickness=d.borderSize) d.replayWidget.pack(fill=t.X) d.replayWidget.config(text="Instant Replay!") else: d.canvas.config(highlightbackground='#000') if d.replayWidget is not None: d.replayWidget.destroy() d.replayWidget = None # if gameNumber == 0 then post message if msg['state']['gameNumber'] == 0: leftToJoin = d.conf['botsInGame'] - len(msg['bots']) if leftToJoin == 1: s = "" else: s = "s" d.canvas.itemconfigure(d.bigMsg, text="Waiting for " + str(leftToJoin) + " robot" + s + " to join.") else: d.canvas.itemconfigure(d.bigMsg, text="") for src, bot in msg['bots'].items(): # ensure all bots on server have widgets if not src in d.botStatusWidgets: # pick color for this bot c = d.colors.pop() # create bot status widget d.botStatusWidgets[src] = t.Message(d.frame, width=200, justify='center') d.botStatusWidgets[src].config(highlightbackground=c) d.botStatusWidgets[src].config(highlightthickness=d.borderSize) d.botStatusWidgets[src].pack(fill=t.X) # create bot widgets d.botScan[src] = d.canvas.create_arc(0, 0, 50, 50, start=0, extent=0, style='arc', width=4, outline='#bbb') d.botTrackLeft[src] = d.canvas.create_line(0, 0, 50, 50, width= d.conf['botRadius'] * (10 / 24.0), fill='grey') d.botTrackRight[src] = d.canvas.create_line(0, 0, 50, 50, width= d.conf['botRadius'] * (10 / 24.0), fill='grey') d.botWidgets[src] = d.canvas.create_oval(0, 0, 0, 0, fill=c) d.botCanon[src] = d.canvas.create_line(0, 0, 50, 50, width= d.conf['botRadius'] * (1/3.0), fill=c) d.botRequestedDirection[src] = d.canvas.create_line(0, 0, 50, 50, width= d.conf['botRadius'] * (5 / 24.0), arrow=t.LAST, fill=colorVariant(c,-100)) d.botCurrentDirection[src] = d.canvas.create_line(0, 0, 50, 50, width= d.conf['botRadius'] * (5 / 24.0), arrow=t.LAST, fill=colorVariant(c,100)) # update text for each bot d.botStatusWidgets[src].config(text=bot['name'] + "\n" + "__________________________________" + "\nPoints: " + str(bot['points']) + "\nCanon Fired: " + str(bot['firedCount']) + "\nShell Damage Inflicted: " + '%.1f' % (bot['shellDamage']) + "\n" + "__________________________________" + "\nHealth: " + '%.1f' % (bot['health']) + "%" " Speed: " + '%.1f' % (bot['currentSpeed']) + "%") # update location of bot widgets or hide if health == 0 if bot['health'] == 0: d.canvas.itemconfigure(d.botWidgets[src], state='hidden') d.canvas.itemconfigure(d.botRequestedDirection[src], state='hidden') d.canvas.itemconfigure(d.botCurrentDirection[src], state='hidden') d.canvas.itemconfigure(d.botTrackLeft[src], state='hidden') d.canvas.itemconfigure(d.botTrackRight[src], state='hidden') d.canvas.itemconfigure(d.botScan[src], state='hidden') d.canvas.itemconfigure(d.botCanon[src], state='hidden') else: centerX = bot['x'] * d.scale + d.borderSize centerY = d.conf['arenaSize'] - bot['y'] * d.scale + d.borderSize d.canvas.coords(d.botWidgets[src], centerX - d.conf['botRadius'], centerY - d.conf['botRadius'], centerX + d.conf['botRadius'], centerY + d.conf['botRadius']) d.canvas.coords(d.botRequestedDirection[src], centerX + d.conf['botRadius'] * bot['requestedSpeed']/100 * (19.0 / 24.0) * math.cos(-bot['requestedDirection']), # 19 centerY + d.conf['botRadius'] * bot['requestedSpeed']/100 * (19.0 / 24.0) * math.sin( -bot['requestedDirection']), d.conf['botRadius'] * bot['requestedSpeed']/100 * math.cos(-bot['requestedDirection']) + centerX, # 24 d.conf['botRadius'] * bot['requestedSpeed']/100 * math.sin(-bot['requestedDirection']) + centerY) d.canvas.coords(d.botCurrentDirection[src], centerX + d.conf['botRadius'] * bot['currentSpeed']/100 * (19.0 / 24.0) * math.cos(-bot['currentDirection']), # 19 centerY + d.conf['botRadius'] * bot['currentSpeed']/100 * (19.0 / 24.0) * math.sin( -bot['currentDirection']), d.conf['botRadius'] * bot['currentSpeed']/100 * math.cos(-bot['currentDirection']) + centerX, # 24 d.conf['botRadius'] * bot['currentSpeed']/100 * math.sin(-bot['currentDirection']) + centerY) d.canvas.coords(d.botTrackLeft[src], centerX + d.conf['botRadius'] * (30.0 / 24.0) * math.cos(-bot['currentDirection'] - math.pi / 4), centerY + d.conf['botRadius'] * (30.0 / 24.0) * math.sin(-bot['currentDirection'] - math.pi / 4), d.conf['botRadius'] * (30.0 / 24.0) * math.cos(-bot['currentDirection'] - (3 * math.pi) / 4) + centerX, d.conf['botRadius'] * (30.0 / 24.0) * math.sin(-bot['currentDirection'] - (3 * math.pi) / 4) + centerY) d.canvas.coords(d.botTrackRight[src], centerX + d.conf['botRadius'] * (30.0 / 24.0) * math.cos(-bot['currentDirection'] - (5 * math.pi) / 4), centerY + d.conf['botRadius'] * (30.0 / 24.0) * math.sin(-bot['currentDirection'] - (5 * math.pi) / 4), d.conf['botRadius'] * (30.0 / 24.0) * math.cos(-bot['currentDirection'] - (7 * math.pi) / 4) + centerX, d.conf['botRadius'] * (30.0 / 24.0) * math.sin(-bot['currentDirection'] - (7 * math.pi) / 4) + centerY) x2, y2 = nbmath.project(centerX, 0, bot['last']['fireCanonRequest']['direction'], d.conf['botRadius'] * 1.35) y2 = centerY - y2 d.canvas.coords(d.botCanon[src], centerX, centerY, x2, y2) d.canvas.coords(d.botScan[src], centerX - d.conf['botRadius'] * 1.5, centerY - d.conf['botRadius'] * 1.5, centerX + d.conf['botRadius'] * 1.5, centerY + d.conf['botRadius'] * 1.5) d.canvas.itemconfigure(d.botScan[src], start=math.degrees(bot['last']['scanRequest']['startRadians'])) extent = bot['last']['scanRequest']['endRadians'] - bot['last']['scanRequest']['startRadians'] if extent < 0: extent += math.pi*2 d.canvas.itemconfigure(d.botScan[src], extent=math.degrees(extent)) d.canvas.itemconfigure(d.botRequestedDirection[src], state='normal') d.canvas.itemconfigure(d.botCurrentDirection[src], state='normal') d.canvas.itemconfigure(d.botWidgets[src], state='normal') d.canvas.itemconfigure(d.botTrackLeft[src], state='normal') d.canvas.itemconfigure(d.botTrackRight[src], state='normal') d.canvas.itemconfigure(d.botScan[src], state='normal') d.canvas.itemconfigure(d.botCanon[src], state='normal') # remove shell widgets veiwer has but are not on server. for src in list(d.shellWidgets.keys()): if not src in msg['shells']: d.canvas.delete(d.shellWidgets[src][1]) d.canvas.delete(d.shellWidgets[src][0]) del d.shellWidgets[src] # add shell widgets server has that viewer doesn't for src in msg['shells']: if not src in d.shellWidgets: c = d.canvas.itemcget(d.botWidgets[src], 'fill') d.shellWidgets[src] = [ d.canvas.create_line(0, 0, 0, 0, width=2, arrow=t.LAST, fill=c), d.canvas.create_line(0, 0, 0, 0, width=2, fill=c) ] # update location of shell widgets for src in d.shellWidgets: centerX = msg['shells'][src]['x'] * d.scale + d.borderSize centerY = d.conf['arenaSize'] - msg['shells'][src]['y'] * d.scale + d.borderSize shellDir = msg['shells'][src]['direction'] shell_item_1 = d.shellWidgets[src][0] d.canvas.coords(shell_item_1, centerX, centerY, d.scale * 1 * math.cos(-shellDir) + centerX, d.scale * 1 * math.sin(-shellDir) + centerY) shell_item_2 = d.shellWidgets[src][1] d.canvas.coords(shell_item_2, centerX, centerY, d.scale * 10 * math.cos(-shellDir) + centerX, d.scale * 10 * math.sin(-shellDir) + centerY) # remove explosion widgets viewer has but are not on server. for k in list(d.explWidgets.keys()): if not k in msg['explosions']: d.canvas.delete(d.explWidgets[k]) del d.explWidgets[k] # reduce existing explosion size by 30% and turn off fill for k in d.explWidgets: bbox = d.canvas.bbox(d.explWidgets[k]) d.canvas.coords(d.explWidgets[k], bbox[0] + (bbox[2] - bbox[0]) * 0.85, bbox[1] + (bbox[3] - bbox[1]) * 0.85, bbox[2] - (bbox[2] - bbox[0]) * 0.85, bbox[3] - (bbox[3] - bbox[1]) * 0.85) d.canvas.itemconfig(d.explWidgets[k], fill='') # add explosion widgets server has that viewer doesn't for k, expl in msg['explosions'].items(): if not k in d.explWidgets: c = d.canvas.itemcget(d.botWidgets[expl['src']], 'fill') centerX = expl['x'] * d.scale + d.borderSize centerY = d.conf['arenaSize'] - expl['y'] * d.scale + d.borderSize explRadius = SrvData.getClassValue(d, 'explRadius', msg['bots'][expl['src']]['class']) d.explWidgets[k] = d.canvas.create_oval(centerX - explRadius, centerY - explRadius, centerX + explRadius, centerY + explRadius, fill=c, width=3, outline=c) # update game status widget d.statusWidget.config(text=d.conf['serverName'] + "\n\nGame: " + str(msg['state']['gameNumber']) + " / " + str(d.conf['gamesToPlay']) + "\nStep: " + str(msg['state']['gameStep']) + " / " + str(d.conf['stepMax'])) # record the last time we got good view data from server. d.lastViewData = time.time() # server needs 1 every 10 seconds to keep us alive. Send every 2 secs to be sure. if time.time() > d.nextKeepAlive: d.viewerSocket.sendMessage({'type': 'viewKeepAlive'}, d.srvIP, d.srvPort) d.nextKeepAlive += 1 # wait during instant replay if d.isReplaying: # Wait two steps before updating screen. wakeat = int(d.replayStepSec * d.replaySaveEveryNth * 1000) # normal wait else: # Wait two steps before updating screen. wakeat = int(d.conf['stepSec'] * 1000) d.window.after(wakeat, checkForUpdates, d)