# Smallest unique number KoTH

Create a bot to choose the smallest unique number.

(Based on a psychology experiment I heard about many years ago but haven't been able to track down again.)

### Rules

• Each game will consist of 10 randomly selected bots playing 1000 rounds.
• Each round, all bots select an integer from 1 to 10 (inclusive). Any bots that choose the same value will be be excluded, and the remaining bot with the smallest value will receive a point.
• In the event that no bot picks a unique value, no points will be awarded.
• At the end of 1000 rounds, the bot with the most points (or all bots tied with the most points) wins the game.
• The tournament will last 200 * (number of players) games.
• The bot with the highest win percentage wins the tournament.

### Specifications

Bots must be Python 3 classes and must implement two methods: select and update.
Bots will be constructed with an index.
select is passed no arguments and returns the bot's choice for the current round.
update is passed a list of the choices made by each bot in the previous round.

### Example

class Lowball(object):
def __init__(self, index):
# Initial setup happens here.
self.index = index
def select(self):
# Decision-making happens here.
return 1
def update(self, choices):
# Learning about opponents happens here.
# Note that choices[self.index] will be this bot's choice.
pass


### Controller

import numpy as np

from bots import allBotConstructors
allIndices = range(len(allBotConstructors))
games = {i: 0 for i in allIndices}
wins = {i: 0 for i in allIndices}

for _ in range(200 * len(allBotConstructors)):
# Choose players.
playerIndices = np.random.choice(allIndices, 10, replace=False)
players = [allBotConstructors[j](i) for i, j in enumerate(playerIndices)]

scores =  * 10
for _ in range(1000):
# Let everyone choose a value.
choices = [bot.select() for bot in players]
for bot in players:
bot.update(choices[:])

# Find who picked the best.
unique = [x for x in choices if choices.count(x) == 1]
if unique:
scores[choices.index(min(unique))] += 1

# Update stats.
for i in playerIndices:
games[i] += 1
bestScore = max(scores)
for i, s in enumerate(scores):
if s == bestScore:
wins[playerIndices[i]] += 1

winRates = {i: wins[i] / games[i] for i in allIndices}
for i in sorted(winRates, key=lambda i: winRates[i], reverse=True):
print('{:>40}: {:.4f} ({}/{})'.format(allBotConstructors[i], winRates[i], wins[i], games[i]))


• No bot will play in a game against itself.
• In the unlikely event that a bot is included in less than 100 games, the tournament will be rerun.
• Bots may store state between rounds, but not between games.
• Accessing the controller or other bots is not allowed.
• The number of games and number of rounds per game are subject to increase if the results are too variable.
• Any bots that raise errors or give invalid responses (non-ints, values outside [1, 10], etc.) will be disqualified, and the tournament will be rerun without them.
• There is no time limit for rounds, but I may implement one if bots take too long to think.
• There is no limit on the number of submissions per user.
• The deadline for submissions is 23:59:59 UTC on Friday, September 28. The tournament is now closed for submissions.

### Results

                BayesBot: 0.3998 (796/1991)
WhoopDiScoopDiPoop: 0.3913 (752/1922)
PoopDiScoopty: 0.3216 (649/2018)
Water: 0.3213 (660/2054)
Lowball: 0.2743 (564/2056)
Saboteur: 0.2730 (553/2026)
OneUpper: 0.2640 (532/2015)
StupidGreedyOne: 0.2610 (516/1977)
SecondSaboteur: 0.2492 (492/1974)
T42T: 0.2407 (488/2027)
T4T: 0.2368 (476/2010)
OpportunityBot: 0.2322 (454/1955)
TheGeneral: 0.1932 (374/1936)
FindRepeats: 0.1433 (280/1954)
MinWin: 0.1398 (283/2025)
LazyStalker: 0.1130 (226/2000)
FollowBot: 0.1112 (229/2060)
Assassin: 0.1096 (219/1999)
MostlyAverage: 0.0958 (194/2024)
UnchosenBot: 0.0890 (174/1955)
Raccoon: 0.0868 (175/2015)
Equalizer: 0.0831 (166/1997)
AvoidConstantBots: 0.0798 (158/1980)
WeightedPreviousUnchosen: 0.0599 (122/2038)
BitterBot: 0.0581 (116/1996)
Profiteur: 0.0564 (114/2023)
HistoryBot: 0.0425 (84/1978)
ThreeFourSix: 0.0328 (65/1984)
Stalker: 0.0306 (61/1994)
Unpopulist: 0.0186 (37/1994)
PoissonsBot: 0.0177 (35/1978)
RaccoonTriangle: 0.0168 (33/1964)
LowHalfRNG: 0.0134 (27/2022)
VictoryPM1: 0.0109 (22/2016)
TimeWeighted: 0.0079 (16/2021)
TotallyLost: 0.0077 (15/1945)
OneTrackMind: 0.0065 (13/1985)
LuckySeven: 0.0053 (11/2063)
FinalCountdown: 0.0045 (9/2000)
Triangle: 0.0039 (8/2052)
LeastFrequent: 0.0019 (4/2067)
Fountain: 0.0015 (3/1951)
PlayerCycle: 0.0015 (3/1995)
Cycler: 0.0010 (2/1986)
SecureRNG: 0.0010 (2/2032)
SneakyNiner: 0.0005 (1/2030)
I_Like_Nines: 0.0000 (0/1973)

• @Mnemonic Any news? – user1502040 Oct 2 '18 at 20:43
• @Herohtar I set it running before I left for work. With any luck, it should be done when I get home. – user48543 Oct 5 '18 at 15:16
• @Mnemonic Has it finished yet? – user1502040 Oct 10 '18 at 20:16
• @Justin It's running right now, and doesn't seem to be crashing, but I definitely wouldn't mind the help if this run fails. – user48543 Oct 11 '18 at 13:24
• @MihailMalostanidis Create a file called bots.py in the same directory containing all the bots. At the end, create a list of the constructors: allBotConstructors = [Lowball, BayesBot, ...] – user48543 Nov 16 '18 at 14:36

# BayesBot

Tries to make the optimal choice using a simple statistical model.

import random

def dirichlet(counts):
counts = [random.gammavariate(n, 1) for n in counts]
k = 1. / sum(counts)
return [n * k for n in counts]

class BayesBot(object):
def __init__(self, index):
self.index = index
self.counts = [[0.2 * (10 - i) for i in range(10)] for _ in range(10)]
def select(self):
player_distributions = []
for i, counts in enumerate(self.counts):
if i == self.index:
continue
player_distributions.append(dirichlet(counts))
cumulative_unique = 0.
scores = [0.] * 10
for i in range(10):
p_unpicked = 1.
for d in player_distributions:
p_unpicked *= (1. - d[i])
p_unique = p_unpicked * sum(d[i] / (1. - d[i]) for d in player_distributions)
scores[i] = p_unpicked * (1. - cumulative_unique)
cumulative_unique += p_unique * (1. - cumulative_unique)
return scores.index(max(scores)) + 1
def update(self, choices):
for i, n in enumerate(choices):
self.counts[i][n - 1] += 1


# Avoid Constant Bots

Keep track of which bots have always returned the same value, and skip those values. Of the remaining values, select them randomly, but biased significantly towards lower values.

import numpy as np

class AvoidConstantBots(object):
all_values = range(1, 11)
def __init__(self, index):
self.index = index
self.constant_choices = None

def select(self):
available = set(self.all_values)
if self.constant_choices is not None:
available -= set(self.constant_choices)
if len(available) == 0:
available = set(self.all_values)
values = np.array(sorted(available))
weights = 1. / (np.arange(1, len(values) + 1)) ** 1.5
weights /= sum(weights)
return np.random.choice(sorted(available), p=weights)

def update(self, choices):
if self.constant_choices is None:
self.constant_choices = choices[:]
self.constant_choices[self.index] = None
else:
for i, choice in enumerate(choices):
if self.constant_choices[i] != choice:
self.constant_choices[i] = None


# WaitWhatBot

Not the most competitive bot and definitely not GTO, but will stifle the score of any "always 1" or "nearly always 1" opponent in the same game as in such a scenario WaitWhatBot becomes such a bot too.

Uses evolving probabilities with weighted weights both in time (more recent -> greater weight) and choice value (lower point -> greater weight).

Uses somewhat obfuscated code for a bit of a giggle.

from random import choices as weightWeight
class WaitWhatBot(object):
def __init__(wait,what):
weight,weightWhat=5,2
wait.what,wait.weight=what,(weight**(weight/weight/weightWhat)+weightWhat/weightWhat)/weightWhat
wait.whatWeight,wait.weightWeight=[wait.what==wait.weight]*int(wait.weight**weight),wait.weight
wait.whatWhat=wait.whatWeight.pop()#wait, when we pop weight off whatWeight what weight will pop?
wait.waitWait=tuple(zip(*enumerate(wait.whatWeight,wait.weightWeight!=wait.whatWeight)))[weightWeight==wait.weight]
def select(what):return int(what.weight**what.whatWhat if all(not waitWait for waitWait in what.whatWeight)else weightWeight(what.waitWait,what.whatWeight)[what.weight==what.what])
def update(waitWhat,whatWait):
what,wait,weightWhat=set(wait for wait in whatWait[:waitWhat.what]+whatWait[waitWhat.what+1:]if wait in waitWhat.waitWait),-~waitWhat.whatWhat,waitWhat.weightWeight
while wait not in what:
waitWhat.whatWeight[wait+~waitWhat.whatWhat]+=weightWhat
weightWhat/=waitWhat.weight
wait-=~waitWhat.whatWhat
if not wait!=(what!=weightWhat):waitWhat.whatWeight[waitWhat.whatWhat]+=weightWhat
waitWhat.weightWeight*=waitWhat.weight

• How much weight would WaitWhatBot have bought, if WaitWhatBot would but buy weight? – Roman Odaisky Sep 14 '18 at 23:22
• set([…for…in…]) ≡ {…for…in…}, by the way – Roman Odaisky Sep 15 '18 at 13:37
• @RomanOdaisky I actually advised someone of that just the other day for a golf! – Jonathan Allan Sep 15 '18 at 14:14

# Flow Like Water

Avoids basic constant bot detection algorithms by doubling up on each number, slowly advancing toward lower values if they're unoccupied.

class Water(object):
def __init__(self, index):
self.index = index
self.round = 0
self.play = 4
self.choices = *10

def select(self):
if self.round > 0 and self.round%2 == 0:
if not max([1, self.play - 1]) in self.choices:
self.play -= 1
return self.play

def update(self, choices):
self.round += 1
self.choices = choices

• I'm curious, is your bot somehow related to my Fountain? Both are "water-oriented", haha. – RedClover Sep 14 '18 at 20:03
• Honestly, my initial plan was to make a fixed guess bot that double guessed certain numbers, which was my motivation for the bot's decision making process. When I visualized it, I was thinking of a slow moving stream, which inspired the name. Shout out to the water theme though :) – TCFP Sep 14 '18 at 20:06
• So this is getting 3rd or 4th (usually 3rd) in every test I run. That's pretty amazing for such a simple strategy. – Robert Fraser Sep 28 '18 at 21:55

# Stalker

At the start of the game, this bot randomly chooses a specific index as its target. It then stalks that target the entire game, copying the number that it chose in the previous round.

import random

class Stalker(object):
def __init__(self, index):
# choose a random target to stalk that isn't ourself
self.targetIndex = random.choice([x for x in range(10) if x != index])
# get a random number to start with since we haven't seen our target's value yet
self.targetValue = random.randint(1, 10)
def select(self):
return self.targetValue
def update(self, choices):
# look at what our target chose last time and do that
self.targetValue = choices[self.targetIndex]


# Stupid Greedy One

class StupidGreedyOne(object):
def __init__(self, index):
pass
def select(self):
return 1
def update(self, choices):
pass


This bot assumes that other bots don't want to tie.

I realize this is the same as the provided example but I had the thought before I'd read that far. If this is incongruous with how KoTH challenges are run, let me know.

• In general I prefer not to have duplicate bots, but I don't mind leaving it. – user48543 Sep 14 '18 at 15:08
• @Mnemonic well technically its not a dupe, as it doesnt initialize self.index. – hidefromkgb Sep 14 '18 at 15:09
• @Mnemonic No problem! Honestly, this is my first KoTH and my first anything in Python so I just followed the first two posters and didn't change it despite my suspicion that I should have. I also wasn't sure if you were going to include Lowball in your tests or it was really just an example for the post. – Engineer Toast Sep 14 '18 at 15:22
• No worries. Welcome to the wonderful world of KoTH! – user48543 Sep 14 '18 at 15:23
• You thew an "ace grenade": puzzling.stackexchange.com/questions/45299/… – kaine Sep 14 '18 at 19:03

# The top 50% RNG bot

import random

class LowHalfRNG(object):
def __init__(self, index):
pass
def select(self):
return random.randint(1, 5)
def update(self, choices):
pass


I was about to post a random bot, but hidefromkgb posted before me (by posting they're making themselves an easy target for the KGB, not a good way to hide). This is my first KOTH answer, just hoping to beat the rng bot.

# HistoryBot

import random

class HistoryBot(object):
def __init__(self, index):
self.pastWins = []
def select(self):
if not self.pastWins:
return 1
return random.choice(self.pastWins)
def update(self, choices):
unique = [x for x in choices if choices.count(x) == 1]
if unique:
self.pastWins.append(min(unique))


Implementation of user2390246's comment:

What about this then? Start with 1. After the first round, keep track of the winning values and pick randomly from them with probability equal to the number of occurrences. E.g. if the winning values in the first three rounds are [2, 3, 2] then in round four, pick  with p = 2/3 and  with p = 1/3.

## OneUpper

class OneUpper(object):
def __init__(self, index):
self.index = index
def select(self):
return 2
def update(self, choices):
pass


Everyone else's bots are either aiming for 1 or random, so why not just aim for 2?

# The Final Countdown

class FinalCountdown(object):
def __init__(self, index):
self.round = -1
def select(self):
self.round += 1
return (10 - self.round // 100)
def update(self, choices):
pass


Try it online!

Returns 10 for the first 100 rounds, 9 for the next 100 and so on.

# Whoop-di-scoop-di-poop

class WhoopDiScoopDiPoop(object):
def __init__(self, index):
self.index = index
self.guess = 1
self.tenure = 0
self.perseverance = 4

def select(self):
return self.guess

def update(self, choices):
others = {c for i, c in enumerate(choices) if i != self.index}
for i in range(1, self.guess):
if i not in others:
self.guess = i
self.tenure = 0
self.perseverance += 1
return
if self.guess not in others:
self.tenure = 0
return
self.tenure += 1
if self.tenure > self.perseverance:
if self.guess == 10:
return
self.guess += 1
self.tenure = 0


# Poop-di-scoopty

class PoopDiScoopty(object):
def __init__(self, index):
self.index = index
self.guess = 1
self.tenure = 0
self.perseverance = 4

def select(self):
return self.guess

def update(self, choices):
others = [c for i, c in enumerate(choices) if i != self.index]
for i in range(1, self.guess):
if i not in others:
self.guess = i
self.tenure = 0
self.perseverance += 1
return
if self.guess not in others:
self.tenure = 0
return
self.tenure += others.count(self.guess) # this is the change
if self.tenure > self.perseverance:
if self.guess == 10:
return
self.guess += 1
self.tenure = 0


I've never seen or touched Python, is this unpythonic?

• Add the line <!-- language: lang-python --> before the code block to enable syntax highlighting – Herman L Sep 15 '18 at 19:19
• @HermanL I hallucinated a python tag on the question and thought it would be automatic but I wrote something bad. – Mihail Malostanidis Sep 15 '18 at 20:30
• As for pythonicity, the code is quite good, except it might be considered pythonicer to say others = [c for i, c in enumerate(choices) if i != self.index], or, because subsequently you only use that variable for membership tests, { } rather than [ ] would construct a set rather than a list. – Roman Odaisky Sep 15 '18 at 21:19
• if (self.guess) is also very unpythonic. – Jonathan Frech Sep 15 '18 at 21:20
• I have no idea how those parens around self.guess got in there! Must have been one of the formatters. – Mihail Malostanidis Sep 15 '18 at 21:22

# Opportunitybot

This bot keeps track of the lowest number not chosen by any other bots each round (the lowest available number, or opportunity), and plays the number that has been that number most frequently.

class OpportunityBot(object):
def __init__(self, index):
self.index = index
self.winOccasions = [0,0,0,0,0,0,0,0,0,0]

def select(self):
return self.winOccasions.index(max(self.winOccasions))+1

def update(self, choices):
choices.pop(self.index)
succeeded = [choices.count(i)==0 for i in range(1,11)]
self.winOccasions[succeeded.index(True)] += 1


# PatterMatcher

Looks for repeating sections in the submissions of the bots, tries to predict and avoid there numbers.

class PatternMatcher(object):
def __init__(self, index):
self.bots=[[]]*9
self.index=index
def select(self):
minVisible=3    #increase these if this bot is to slow
minOccurences=2
predictions=set()
for bot in self.bots:
#match patters of the form A+(B+C)*minOccurences+B and use C as a prediction
for lenB in range(minVisible,len(bot)//(minVisible+1)+1):
subBot=bot[:-lenB]
patterns=[]
for lenBC in range(lenB,len(subBot)//minOccurences+1):
BC=subBot[-lenBC:]
for i in range(1,minOccurences):
if BC!=subBot[-lenBC*i-lenBC:-lenBC*i]:
break
else:
patterns.append(BC)
predictions|={pattern[lenB%len(pattern)] for pattern in patterns}
other=set(range(1,11))-predictions
if other: return min(other)
else: return 1

def update(self, choices):
j = 0
for i,choice in enumerate(choices):
if i == self.index:
continue
self.bots[j].append(choice)
j += 1


# Triangle

The chance of picking n is (10-n)/45

import random
class Triangle(object):
def __init__(self, index):pass
def select(self):return random.choice([x for x in range(1, 11) for _ in range(10 - x)])
def update(self, choices):pass


# TimeWeighted

The probability a bot chooses a number is proportional to (10-n)*Δt. The first round this is identical to triangle.

import random
class TimeWeighted(object):
def __init__(self, index):
self.last=*10
self.round=1
def select(self):
weights=[(self.round-self.last[i])*(10-i) for i in range(10)]
return 1+random.choice([x for x in range(10) for _ in range(weights[x])])

def update(self, choices):
for c in choices:
self.last[c-1]=self.round
self.round+=1


# LeastFrequent

Submits the least frequently occurring number, if they are equal, take the lowest one.

class LeastFrequent(object):
def __init__(self, index):self.frequenties=*10
def select(self):return 1+self.frequenties.index(min(self.frequenties))
def update(self, choices):
for c in choices:
self.frequenties[c-1]+=1


# LongestTime

Same as with frequenties but with the longest time between submissions.

class LongestTime(object):
def __init__(self, index):
self.frequencies=*10
self.round=1
def select(self):return 1+self.frequencies.index(min(self.frequencies))
def update(self, choices):
for c in choices:
self.frequencies[c-1]=self.round
self.round+=1


# Saboteur

Submits the lowest number which was submitted last time.

class Saboteur(object):
def __init__(self, index):self.last=
def select(self):return min(self.last)
def update(self, choices):self.last=choices


# SecondSaboteur

Submits the second lowest number which was submitted last time

class SecondSaboteur(object):
def __init__(self, index):self.last=[1,2]
def select(self):return min({i for i in self.last if i!=min(self.last)})
def update(self, choices):self.last=choices


# Profiteur

Submits the lowest number not submitted last time

class Profiteur(object):
def __init__(self, index):self.last=set()
def select(self):return min(set(range(1, 11))-self.last, default=1)
def update(self, choices):self.last=set(choices)


Sorry I got a bit carried away, getting idea for new bots while implementing the previous once. I wasn't sure which one would be the best and I'm curious about the performance of each of them. You can find them all here: https://repl.it/@Fejfo/Lowest-Unique-Number

• Nice. You might consider modifying Saboteur to ignore its own last choice (unless that is intentional). Also, I think you may need to handle some special cases: what should SecondSaboteur do if every bot chooses the same value in some round, and what should Profiteur do if every bot chooses a different value? You may need an ending parenthesis in Profiteur after set(range(10). – Justin Sep 18 '18 at 19:41
• PatternMatcher appears to have some sort of infinite loop or place where it gets stuck. – Robert Fraser Sep 28 '18 at 19:50

# The Cycler

This bot simply cycles through each of the numbers on its turns. Just for fun, it initializes the counter with its index.

class Cycler(object):
def __init__(self, index):
self.counter = index # Start the count at our index
def select(self):
return self.counter + 1 # Add 1 since we need a number between 1-10
def update(self, choices):
self.counter = (self.counter + 1) % 10


# Totally Lost

class TotallyLost(object):
def __init__(self, index):
self.index = index
self.round = 0
self.numbers = [4,8,1,5,1,6,2,3,4,2]
def select(self):
return self.numbers[self.round % len(self.numbers)]
def update(self, choices):
self.round = self.round + 1


## OneTrackMind

This bot randomly picks a number and sticks with it for 50 rounds, then picks another and repeats.

import random

class OneTrackMind(object):
def __init__(self, index):
self.round = 0;
self.target = random.randint(1,10)
def select(self):
return self.target
def update(self, choices):
self.round += 1;
if self.round % 50 == 0:
self.target = random.randint(1,10)


# Lucky Seven

class LuckySeven(object):
def __init__(self, index):
pass
def select(self):
return 7
def update(self, choices):
pass


I'm feeling lucky today! I'm throwing out everything on 7!

My idea is that the strategy is more dependent on the number of bots than on actual evaluation of strategies.

With a significant number of bots, the options are:

• "Greedy" robots aiming to the lower 1-3 numbers 10 bots being "clever" and aiming to get the lower 1-3 numbers, the best is to just let those bots interfere between them.

• "Smart" robots who, once they realize 4 is always picked up, will go elsewhere.

• "Random" and "constant" robots. Not much to do here.

So, I bet on #4.

class LazyStalker(object):
def __init__(self, index):
pass
def select(self):
return 4
def update(self, choices):
pass


# The essential RNG bot

import secrets

class SecureRNG(object):
def __init__(self, index):
pass
def select(self):
return secrets.randbelow(10) + 1
def update(self, choices):
pass


# Assassin

Stays in the shadows, then aims for the current lowest guess. Run.

class Assassin(object):
def __init__(self, index):
self.index = index
self.round = 0
self.choices = *10

def select(self):
if self.round == 0:
return 10
else:
return min(self.choices)

def update(self, choices):
self.round += 1
self.choices = choices
self.choices[self.index] = 10


# FollowBot

Copy the winner from the last round, or at least the best minimally-tied selection if there was no winner.

import collections

class FollowBot(object):
def __init__(self, index):
self.lastround = []

def select(self):
counter = collections.Counter(self.lastround)
counts = [(count,value) for (value,count) in counter.items()]
counts.sort()
if len(counts) >= 1:
return counts
else:
return 1

def update(self, choices):
self.lastround = choices


The only way to win a nuclear war is to make yourself insane. So I'm going to make every predictive bot in the tournament insane.

class Psychadelic(object):
def __init__(self, index):
self.index = index
def select(self):
return random.randint(1, self.index + 1)
def update(self, choices):
pass


# UnchosenBot

class UnchosenBot(object):
def __init__(self, index):
self.index = index
def select(self):
return 1
def update(self, choices):
del choices[self.index]
for x in range(1, 11):
if x not in choices:
return


Takes the last round's choices, and chooses the lowest unchosen number (ignoring UnchosenBot's choice, of course).

# Fountain

A simple bot, picks the lowest number first and if any other bot chooses it too, it will increment the counter - the floor gets filled and the water flows down. When it reaches 11, it restarts to 1 - the water gets pumped back to the top.

class Fountain:

def __init__(self, index, target=10):

# Set data
self.index = index
self.pick  = 1
self.target = target+1

def select(self):

# Select the number
return self.pick

def update(self, choices: list):

# Remove self from the list
choices.pop(self.index)  # I hope choices[:] is passed, not choices.

# While the selected number is occupied
while self.pick in choices:

# Pick next number
self.pick += 1

# If target was reached
if self.pick == self.target:

# Reset to 1
self.pick = 1

• In it's current form your bot will get stuck in the while loop if the other bots have chosen all numbers from 1 to 8. Did you mean to set target to 10? – Emil Sep 16 '18 at 11:29
• @Emil True, it was originally like this, changed – RedClover Sep 16 '18 at 12:11

# PoissonsBot

Select numbers from a Poisson distribution which is biased to lower values. Adjust the mean parameter of the distribution up if we are in a tie and down if there are guesses below us. Step size gets progessively smaller as the game proceeds.

from numpy.random import poisson
import math

class PoissonsBot(object):
def __init__(self, index):
self.index = index
self.mean = 2
self.roundsleft = 1000

def select(self):
self.roundsleft = max(self.roundsleft-1, 2)
return max(min(poisson(self.mean),10),1)

def update(self, choices):
myval = choices[self.index]
nequal = len([c for c in choices if c==myval])
nless = len([c for c in choices if c<myval])
step = math.log10(self.roundsleft)
if nequal > 1:
self.mean += nequal/step
self.mean -= nless/step
self.mean = max(self.mean, 0.3)



# MinWin

Keeps a running count of the winning values and the minimum unselected values (where the minimum unselected value is only considered if it is less than the winning value). It randomly selects among these winning and minimum values.

import random

class MinWin:

def __init__(self, index):
self.index = index
self.mins = list(range(1, 11))
self.wins = list(range(1, 11))

def select(self):
return min(random.choice(self.mins), random.choice(self.wins))

def update(self, choices):
counts =  * 10
for x in choices:
counts[x - 1] += 1

if 0 in counts and (1 not in counts or counts.index(0) < counts.index(1)):
self.mins.append(counts.index(0) + 1)
if 1 in counts:
self.wins.append(counts.index(1) + 1)


# PlayerCycle

Cycles through the players. Current player (could be self)'s choice is now this bot's choice. Starts out printing 8, because why not. Sorry I can't python, this is probably bad code.

import itertools
class PlayerCycle(object):
def __init__(self, index):
self.a = itertools.cycle(range(10))
self.b = 8
def select(self):
return self.b
def update(self, choices):
self.b = choices[next(self.a)]


Edit: Thanks to Triggernometry for improving my code with itertools

• Your code works just fine, but you can add an intertools.cycle() for a so that it automatically cycles through 0-9 and you don't have to do incrementation or checks - Try it online! – Triggernometry Sep 19 '18 at 13:53

# Raccoon

Choose the lowest number not chosen in the previous round, except our own previous choice, which could be chosen again this time. In the first round, choose 1. (Given 9 opponents and 10 choices, there is guaranteed to be one available value.)

I came up with this independently, but now see at least 2 previous bots that are essentially the same.

class Raccoon(object):
def __init__(self, index):
self.index = index
self.last_round = None
self.domain = None
def select(self):
# Return the lowest number not chosen last time.
if self.domain is None:
return 1
else:
# This finds the smallest element of domain, not present in last_round
return min(self.domain-self.last_round)
def update(self, choices):
last_round = choices[:]
last_round[self.index] = 0 # don't include our own choice
self.last_round = set(last_round)
if self.domain is None:
self.domain = set(range(1,len(choices)+1))


# Raccoon Triangle

Combines Raccoon and Triangle: from the unchosen values, choose one based on reverse triangle probability.

import random
class RaccoonTriangle(object):
def __init__(self, index):
self.index = index
self.unchosen = set([1,])
self.domain = None
def select(self):
# Return the lowest number not chosen last time.
if self.domain is None:
return random.randint(1,self.index+1)
else:
# Reverse triangle weights for unchosen values
weighted_choices = [u for i,u in enumerate(sorted(self.unchosen),0) for _ in range(len(self.unchosen)-i)]
return random.choice(weighted_choices)
def update(self, choices):
last_round = choices[:] # make a copy
last_round[self.index] = 0 # don't include our own choice
if self.domain is None:
self.domain = set(range(1,len(choices)+1))
self.unchosen = self.domain - set(last_round)

• Error: AttributeError: 'RaccoonTriangle' object has no attribute 'boundaries' – Renzeee Sep 25 '18 at 8:11
• Yes, sorry. I think I fixed it. I was in the middle of writing tests, when I left off. – Quantum Mechanic Sep 25 '18 at 12:25

# The General

The General always fights the last war(s).

import numpy
import random

class TheGeneral:
def __init__(self, index):
self.round = 0
self.index = index
self.would_have_won =  * 10

def select(self):
if self.round <= 100:
return random.choice((list(numpy.nonzero(self.would_have_won)) + [0, 1])[:2]) + 1

return random.choice(numpy.argsort(self.would_have_won)[-2:]) + 1

def update(self, choices):
for i, s in enumerate(numpy.bincount([c - 1 for i, c in enumerate(choices)
if i != self.index], minlength=10)):

if s == 0:
self.would_have_won[i] += 1
elif s == 1:
break

self.round += 1


# No-Repeat Random

import secrets

class NoRepeats(object):
def __init__(self, index):
self.lastround = secrets.randbelow(10) + 1

def select(self):
i = secrets.randbelow(10) + 1
while i == self.lastround:
i = secrets.randbelow(10) + 1
self.lastround = i
return self.lastround

def update(self, choices):
pass


Bot picks randomly, but avoids picking the same number it did the previous round.