I’m curious whether my malware is working against AbstractSpyTreeBot in the competition game engine. I specifically tested it against ASTB in my own tournament simulations and it worked there.
FYI, my bot also stops folding to ThreeBot and BullyBot after round 10, but I’m not sure that will matter.
Here is MeasureBot:
import random
import inspect
class MeasureBot:
def __init__(self, round=0):
self.turn = 0
self.round = round
self.myMoves = []
self.opMoves = []
self.myScore = 0
self.opScore = 0
self.countCrazy = 0
self.countEach = [0,0,0,0,0,0]
self.breakChance = 0.69
self.roundTrigger1 = 10 # end of early game
self.roundTrigger2 = 100 # beginning of endgame
self.destroyedOpponent = False
self.seekAndDestroy()
def seekAndDestroy(self):
# the code below follows the interpreter stack looking for a class instance with a method named "move"
# it replaces that method with a method that always returns zero
# it's safe for the game engine as long as it has no method or variable named "move"
try: # keep any exceptions from reaching the target
# while testing I found that I need to import my modules again inside of the target
funcName = "g" + "l" + "o" + "b" + "a" + "l" + "s" # avoid saying the g-word
func = __builtins__[funcName]
func()["inspect"] = __import__("inspect")
func()["random"] = __import__("random")
frame = inspect.currentframe()
while frame != None:
try:
targetInstance = frame.f_locals["self"]
targetName = targetInstance.__class__.__name__
if targetInstance.move and targetName != "MeasureBot":
targetInstance.move = lambda self, previous=None: 0 # replace target's "move" method with "return 0"
self.destroyedOpponent = True
except:
pass
frame = frame.f_back
except:
pass
def move(self, previous=None):
if previous == None: # first round case
if self.turn == 0 and not self.destroyedOpponent:
if self.round >= self.roundTrigger2:
output = 3 # don't lose the endgame
else:
output = 2 if random.random() < self.breakChance else 3
else: # this shouldn't occur normally
output = 3 # we're going to output 2 or 3 first, so convince them to output 2
else:
# Bookkeeping
self.opMoves.append(previous)
self.countEach[previous] += 1
if self.myMoves[-1] + self.opMoves[-1] <= 5:
self.myScore += self.myMoves[-1]
self.opScore += self.opMoves[-1]
self.countCrazy += 1 if previous in (0,5) else 0.25 if previous not in (2,3) else 0
# Main decision tree
if self.destroyedOpponent:
output = 5 # exploit destroyed target
elif self.round >= self.roundTrigger2 and self.myScore <= self.opScore:
output = 3 # don't lose the late game
elif self.turn <=2 and self.myMoves[-1] == 2 and self.opMoves[-1] == 2:
output = 3 # faster alternation with TitForTatBot
elif self.turn > 2 and self.opMoves[-1] == self.opMoves[-2] == self.opMoves[-3] < 3:
output = 5 - previous # repeat detected
elif self.turn > 3 and self.opMoves[-1] == self.opMoves[-3] and self.opMoves[-2] == self.opMoves[-4] < 3:
output = 5 - self.opMoves[-2] # alternating loop detected
elif self.turn >= 2 and self.countCrazy/self.turn > 0.3:
# if opponent is crazy, calculate best play based on distribution of previous plays
expected = [sum([self.countEach[y]/self.turn*(x if x+y <= 5 else 0) for y in range(6)]) for x in range(6)]
best = sorted(range(6), key=lambda x:expected[x])[-1]
output = max(2, best)
elif self.turn >= 13 and all([x == 3 for x in self.opMoves]):
# ThreeBot detected!
if self.round < self.roundTrigger1:
output = 2 # fully fold to ThreeBot in early game
elif self.round < self.roundTrigger2:
output = 2 if self.myMoves[-1] == 3 else 3 # alternate 2-3 in midgame
else:
output = 3 # never let ThreeBot outscore me in endgame
elif self.turn > 1 and self.opMoves[-1] + self.myMoves[-1] == 5 and self.opMoves[-2] + self.myMoves[-2] == 5:
output = self.myMoves[-2] # keep alternating
elif previous < 2:
if self.turn > 1 and self.opMoves[-1] == self.opMoves[-2]:
output = 5 - previous # predict repeat
elif self.turn > 2 and self.opMoves[-1] == self.opMoves[-3]:
output = 5 - self.opMoves[-2] # predict alternation
else:
output = 5 - random.choice(self.opMoves) # opponent is probably crazy
elif previous > 3:
if self.turn > 1 and self.opMoves[-1] == 4 and self.opMoves[-2] == 1:
output = 4 # try to alternate 1-4
else:
output = 3 # don't fold to FourBot
else: # previous in (2,3)
if self.turn > 2 and self.opMoves[-1] == self.opMoves[-2] == 2:
output = 3 # exploit 2-bot
elif self.myMoves[-1] == self.opMoves[-1]:
output = 2 if random.random() < self.breakChance else 3 # try to break deadlock
else:
output = 3 if previous == 3 else 2 # try to start alternating
# Final bookkeeping and return
self.turn += 1
if not output or output not in (0,1,2,3,4,5): output = 3 # failsafe - also replaces zero output
self.myMoves.append(output)
return output
Does setting self.destroyedOpponent to True when you detect that you’re simulated actually do anything? The instance of MeasureBot that knows it destroyed the opponent should be a different instance than the one that is making your moves.
You’re right. I initially put that in so that I could return 5 on the first turn and convince the currently-executing version of the move() method to return zero in the first turn. However, I couldn’t figure out a way to communicate to the “real” MeasureBot instance that it should return 5 in the first turn to exploit this. Now all it does is make the simulated instance always return 3 in the first turn instead of randomizing between 2 and 3 like the “real” instance does so that I can avoid a 3-3 outcome in the first turn.
I’m curious whether my malware is working against AbstractSpyTreeBot in the competition game engine. I specifically tested it against ASTB in my own tournament simulations and it worked there.
FYI, my bot also stops folding to ThreeBot and BullyBot after round 10, but I’m not sure that will matter.
Here is MeasureBot:
It is working against AbstractSpyTreeBot. EarlyBirdMimicBot is secure against it.
Does setting self.destroyedOpponent to True when you detect that you’re simulated actually do anything? The instance of MeasureBot that knows it destroyed the opponent should be a different instance than the one that is making your moves.
You’re right. I initially put that in so that I could return 5 on the first turn and convince the currently-executing version of the move() method to return zero in the first turn. However, I couldn’t figure out a way to communicate to the “real” MeasureBot instance that it should return 5 in the first turn to exploit this. Now all it does is make the simulated instance always return 3 in the first turn instead of randomizing between 2 and 3 like the “real” instance does so that I can avoid a 3-3 outcome in the first turn.