28
\$\begingroup\$

Final results

1st place goes to Follow!

Honourable mentions to The diff predictor (2nd), and Simple Killer, Low Blow and High to Low (joint 3rd).

Other winners:

(score competition) 1st - Simple Killer

Honourable mentions: The diff predictor and Naiive.

(wins competition) 1st - Crab

Honourable mentions: Follow and High to Low.

Full results:

By score:
1: Simple Killer with 1208099 points
2: The diff predictor with 1053087 points
3: Naiive with 1051505 points
4: Follow with 905731 points
5: The dedicated counter with 769680 points
6: The Desperate Fighter with 769663 points
7: Low Blow with 763357 points
8: High to Low with 734011 points
9: Naughty or Nice? with 714912 points
10: Grim Trigger with 713702 points
11: Tit for tat with 713067 points
12: The primitive looker with 621028 points
13: Hard-coded with 428934 points
14: Zoidberg with 395702 points
15: FunnyNumber with 335179 points
16: Random with 329023 points
17: Shortcut with 257190 points
18: Meandian with 223051 points
19: Hawk with 47608 points
20: Crab with 14852 points

By wins:
1: Crab with avg. 19.0/19 wins
2: Follow with avg. 17.1/19 wins
3: High to Low with avg. 15.8/19 wins
4: Low Blow with avg. 14.9/19 wins
5: The diff predictor with avg. 14.1/19 wins
6: Meandian with avg. 13.7/19 wins
7: Hawk with avg. 13.0/19 wins
8: Hard-coded with avg. 11.7/19 wins
9: Naughty or Nice? with avg. 11.0/19 wins
10: Simple Killer with avg. 10.4/19 wins
11: The primitive looker with avg. 8.0/19 wins
12: Naiive with avg. 6.6/19 wins
13: Zoidberg with avg. 6.3/19 wins
14: Shortcut with avg. 5.0/19 wins
14: Grim Trigger with avg. 5.0/19 wins
16: FunnyNumber with avg. 4.0/19 wins
17: Random with avg. 3.0/19 wins
18: Tit for tat with avg. 1.0/19 wins
19: The dedicated counter with avg. 0.0/19 wins
19: The Desperate Fighter with avg. 0.0/19 wins

Combined leaderboard (fewer pts = better):
1: Follow  (6 pts)
2: The diff predictor  (7 pts)
3: Simple Killer  (11 pts)
3: Low Blow  (11 pts)
3: High to Low  (11 pts)
6: Naiive  (15 pts)
7: Naughty or Nice?  (18 pts)
8: Hard-coded  (21 pts)
8: Crab  (21 pts)
10: The primitive looker  (23 pts)
11: The dedicated counter  (24 pts)
11: Grim Trigger  (24 pts)
11: Meandian  (24 pts)
14: The Desperate Fighter  (25 pts)
15: Hawk  (26 pts)
16: Zoidberg  (27 pts)
17: Tit for tat  (29 pts)
18: FunnyNumber  (31 pts)
18: Shortcut  (31 pts)
20: Random  (33 pts)

Cooperative Counting - Python 3 KOTH

Have you ever tried to count to 100 in an online forum or comment thread? It normally goes something like:

1
>2
>>3
>>>4
>>>>69 xD xD

the point being that someone will always try to ruin the fun.

The game

This is a bit like the counting example above, except to make sure that neither player has an advantage from going first, each player will count at the same time as the other.

Your bot will be pitted against another bot. Your goal? Cooperatively count to 100.

Each round, your bot decides how far it is willing to cooperatively count with its opponent.

Possible outcomes

One player stops counting before the other

The player who stopped counting first is awarded twice as many points as the value they counted up to. The other player gains no points.

Both you and your opponent stop counting at the same time

Neither of you gain any points.

Both you and your opponent count all the way to 100

Each player gains 100 points.

Example

  • Player 1: decides to count to 80
  • Player 2: decides to count to 30

Player 2 gets 60 points, player 1 gets 0 points.


In another round:

  • Player 1: decides to count to 57
  • Player 2: decides to count to 57

Neither player gets points.


In yet another round:

  • Player 1: decides to count to 100
  • Player 2: decides to count to 100

Each player gets 100 points.

Overall game flow

Your bot will be matched against other bots. It will play some number of rounds in a row, n, against the same bot. n will be somewhere in the range of 100 - 1000. This allows your bot to adapt to the other bot's strategy.

Winning the KOTH

Each bot will have a go against each other bot. Your bot can win in three categories:

  • Score: the total scores will be summed and the bot with the most points at the end will win.
  • Wins: a 'win' is counted for the bot with the highest score after the n rounds have been played.
  • Overall: both the other categories combined

Technical details

Write a function in Python 3 with this signature:

def strategy(last_results: list[tuple[int, bool]]) -> int
  • strategy is called once at the beginning of each round.
    • last_results is a list of previous results against the same opponent. Each item in the list is the result of a round in the form (value reached, won), where won denotes whether the player won or not. If the player won through cooperation, this will be True.
    • e.g.: if you lose your first round because your opponent stops counting at 79, last_results looks like [(79, False)] at the start of round 2. If you then win round 2 by stopping counting at 34, last_results will look like [(79, False), (34, True)] at the start of round 3.
    • strategy should return the value at which the bot will stop counting. 1 stops counting immediately, 100 is an agreement to count cooperatively with the other bot all the way to the end, and e.g. 47 will stop counting when the value is 47, earning the bot 84 points.

Example bot

'Naiive' counts up to one before the number that was reached last round if it lost, otherwise it counts up to and including the number it won on.

def strategy(last_games):
    if len(last_games) == 0:
        return 100
    
    # Count up to one before the last number that was counted if we lost,
    # otherwise just up to the last number that was counted.
    if last_games[-1][1]:
        return last_games[-1][0]
    else:
        return max(1, last_games[-1][0] - 1)

Legacy format (don't use)

I've decided on a simpler format since the last one was causing confusion. Here is the old one for reference purposes. New submissions should be in the new format. Submissions using the legacy format do not need to update their submission.

# Don't use this, see above
def strategy(last_results: list[tuple[int, bool]]) -> tuple:
    pass

def turn(current_value: int, *args) -> bool:
    pass

Rules

  • No cheating by interfering directly with your opponent (through global variables etc.).
  • Your function should be relatively quick to execute - the quicker it is, the better.
  • You may submit multiple entries.
  • Submissions close a week from the start of this post, at 2021-04-17 11:00 UTC

Controller, sandbox, arena

The controller is available at https://github.com/jthistle/KOTH-counting.

A couple of example bots are provided along with it to demonstrate how to use it.

sandbox.py provides a place to try out your bot against others and debug it.

arena.py is what I'll be using to calculate final scores. It pits each bot against each other bot.

\$\endgroup\$
8
  • 7
    \$\begingroup\$ As someone who has run quite a few KotHs, this is probably the best one I've ever seen with such a simple premise. Looks like I'll be learning Python now :p \$\endgroup\$ Apr 10, 2021 at 13:04
  • 1
    \$\begingroup\$ @jumbot Definitely better to keep it to one language, I needed an excuse to practice other languages anyway :p \$\endgroup\$ Apr 10, 2021 at 13:06
  • 2
    \$\begingroup\$ I made this script to make updating the controller with new bots a bit easier. It automatically fetches the new bots with the API and puts them in the contestants folder, then adds them to arena.py. If you notice any weird bugs let me know, this is probably my first time writing anything serious in Python :p \$\endgroup\$ Apr 10, 2021 at 14:21
  • 2
    \$\begingroup\$ Isn't 'get one point ahead, then completely refuse to count' the perfect strategy? And any strategy that counts properly at least once loses to Crab, so the only strategy that doesn't lose to Crab is Crab. \$\endgroup\$ Apr 10, 2021 at 16:29
  • 1
    \$\begingroup\$ Given that multiple bots use randomness, it might be better to run multiple contests and use the average of the results. Low Blow has a small chance of coming 3rd in points, and normally gets 13/14 wins, but due to the randomness, it can change in one-off contests \$\endgroup\$ Apr 12, 2021 at 17:43

20 Answers 20

17
\$\begingroup\$

Crab

def strategy(last_results):
    return 1

Crab always stops counting, because it'd rather not get points than have the other bot beat it. It is because of Crab that we can't have nice things.

\$\endgroup\$
3
  • 2
    \$\begingroup\$ And I thought shortcut was cynical \$\endgroup\$ Apr 10, 2021 at 13:51
  • 1
    \$\begingroup\$ @RedwolfPrograms Like programmer, like bot, I guess :P. I'll make a slightly better designed one soon to make up for the atrocity that is Crab. \$\endgroup\$
    – user
    Apr 10, 2021 at 13:59
  • 1
    \$\begingroup\$ I love this :)) \$\endgroup\$ Apr 10, 2021 at 14:10
12
\$\begingroup\$

Low Blow

import random
import statistics

def strategy(last_results):
        scores = list(map(lambda a: a[0], last_results))
        if not scores: return 99

        if len(scores) > 420 and scores[:3] == [69, 68, 68]:
                return 67

        decrements = [50, 69, 48]

        if all(score == 1 for score in scores): return 1

        for decrement in decrements:
                mold = [decrement]
                for _ in scores[1:]: mold.append(decrement - 1)
                if scores == mold:
                        return decrement - 1

        if scores[0] == 65:
                if scores[-1] == 1: return 1
                return random.randint(1, 48)

        if scores == [98]:
                return 97

        if len(scores) == 1 and scores[0] < 48:
                return scores[0]

        if last_results[:2] == [(99, True), (99, False)]:
                return max(scores[-1] - 1, 1)

        if scores[:2] == [47, 31]    : return max(min(50, int(sum(scores) / (len(scores) * 1.5))) - 1, 1)
        if scores[:2] == [99, 97]    : return max(scores[-1] - 3, 1)
        if scores[:2] == [99, 98]    : return max(scores[-1] - 2, 1)
        if scores[:3] == [99, 99, 98]: return max(scores[-1] - 1, 1)

        if scores[:2] == [98, 97]    :
                htl_losses = [result[0] for result in last_results if result[1]]
                return max(min(htl_losses) - len(htl_losses) - 2, 1)

        if len(scores) > 1 and scores[0] == scores[1] != 99:
                mean = round(statistics.mean(scores))
                median = round(statistics.median(sorted(scores)))

                if mean == median:
                        return max(mean - 1, 1)
                return max(abs(median - mean) - 1, 1)

        if scores[-1] == 1:
                return 1

        if len(scores) <= 3: return 99

        deltas = [scores[~0] - scores[~1], scores[~1] - scores[~2]]
        if any(deltas): return random.randint(37, 50)

        return 99

Low Blow's strategy is somewhat "kamikaze". It aims to reduce the opposition bot's score as much as possible, even if that means that Low Blow scores fewer points. However, it does aim to capitalise on points against specific bots where possible.

It does this by identifying the bot it's against, then playing their own strategy against them, but one lower than what they'd return. Either, the values are constant and Low Blow collects a bunch of points, or the values eventually reach 1, at which point Low Blow will likely\${}^*\$ have more points and neither bot will get more

\${}^*\$"Likely" because of randomness from Hard-coded

\$\endgroup\$
2
  • \$\begingroup\$ Nice one! Can confirm that low blow is leagues ahead... \$\endgroup\$ Apr 10, 2021 at 17:58
  • \$\begingroup\$ Nice. Will your program still win against me (on average) after my edit? \$\endgroup\$
    – math scat
    Apr 10, 2021 at 18:07
8
\$\begingroup\$

Follow

def strategy(last_results):
    n = len(last_results)
    if n == 0: return 100
    if all(i == (1,False) for i in last_results): return 2 # nice to the crab
    a,b = last_results[-1]
    if n == 1:
        if b: return 100
        return 1+int(.97*(a-1))
    c,d = last_results[-2]
    if n >= 99 and a == 100: return 99
    if b and d: # won previous two rounds
        if (a+1,False) not in last_results:
            return min(100,a+1)
        return a
    else:
        a,c = sorted((a,c))
        return max(1,a*a//c-1)

Starts out by counting to 100. After round 99, if it previously counted to 100, it will only count to 99 (the reasoning is that it would otherwise tie with everything else that keeps counting to 100). If it detects the crab, it's nice (because it doesn't matter anyways). If it lost the previous round, then it counts lower than whatever won in the previous round (between 1 to 3 less). If it won the previous round, then it counts one further if it hasn't lost by counting that far before.

Edit: Made it use the ratio between the previous two moves to decide what to count to, made it only start increasing if it won the previous two rounds, and figured out a better way to check for the crab

\$\endgroup\$
1
  • \$\begingroup\$ Congrats! You won :) \$\endgroup\$ Apr 17, 2021 at 11:29
8
\$\begingroup\$

Simple Killer

def strategy(last_results):
    if not len(last_results):
        return 100
    if len(last_results) == 999 and last_results[-1][0] == 100:
        return 99
    last_move = last_results[-1]
    if last_move in last_results[:-1]:
        pos = last_results.index(last_move)
        if last_results[pos + 1][1]:
            return last_results[pos + 1][0]
        return max(last_results[pos + 1][0] - 1, 1)
    return 100

Simple Killer targets a certain class of bot: those who consider only the most recent move (or don't look at the past). By determining exactly how the opposing bot typically reacts, Simple Killer is able to slowly figure out the optimal move in any given circumstance.

It either loses by a narrow margin, wins by a huge margin, or plays nice (if the other bot is happy to just sit at 100).

\$\endgroup\$
1
  • \$\begingroup\$ This is probably the smartest answer that isn't just a hard-coded bot... nice one! \$\endgroup\$ Apr 10, 2021 at 18:58
8
\$\begingroup\$

The dedicated counter

Updated to use the new format (slight performance gain, which adds up):

def strategy(last_results):
    return 100

Original:

def strategy(last_results):
    return ()

def turn(current_value, *args):
    return True

No matter what the opponent does, this bot will always count up. With determination it keeps going in the face of potential trolls. I'll be writing a more thought out bot once I can see the examples.

\$\endgroup\$
3
  • \$\begingroup\$ strategy needs to return a tuple. (in this case it will always be empty though) \$\endgroup\$
    – pxeger
    Apr 10, 2021 at 11:06
  • \$\begingroup\$ i.e., change pass to return (). But thank you for the submission! \$\endgroup\$ Apr 10, 2021 at 11:07
  • 2
    \$\begingroup\$ @jumbot I've changed the return format - I wasn't sure how it should work. Nice koth BTW. \$\endgroup\$
    – lyxal
    Apr 10, 2021 at 11:08
6
\$\begingroup\$

The primitive looker

This is a bot which learns from its previous rounds.

New version:

def strategy(last_results):
    if not last_results:
        return 100
    if any(not x[1] for x in last_results):
        if last_results[-1][0] - 2 > 0:
            return last_results[-1][0] - 2
        return 100
    return 99

Legacy version:

import random

def strategy(last_results):
    try:
        return (last_results, )
    except:
        return ([], )

def turn(current_value, res):
    v = 99
    m = 2
    g = True
    for i in res:
        if not i[1]:
            g = False
            break
        
    if res == []:
        c = True
    else:
        if res[-1][1] and g:
            if current_value == v:
                c = False
            else:
                c = True
        else:
            if current_value == res[-1][0] - m:
                c = False
            else:
                c = True

    return c


Lucky this is not ...
Try it online!

\$\endgroup\$
31
  • \$\begingroup\$ Thanks for your submission! I'd rather you didn't use globals, just in case, so you could consider returning res, num from strategy and writing turn as turn(current_value, res, num) please :) \$\endgroup\$ Apr 10, 2021 at 11:44
  • \$\begingroup\$ Ok, edit in progress. \$\endgroup\$
    – math scat
    Apr 10, 2021 at 11:45
  • 2
    \$\begingroup\$ @jumbot run a few rounds and tell us the result please \$\endgroup\$
    – Wasif
    Apr 10, 2021 at 12:08
  • 1
    \$\begingroup\$ This currently returns None in the first round (which confuses the controller as it's a falsey value). If you unindent the return statement it should work \$\endgroup\$ Apr 10, 2021 at 20:22
  • 1
    \$\begingroup\$ @Spitemaster Aside from the "constant" bots, I'd suggest against updating the legacy versions. The controller can handle them fine, and you might miss some of the subtleties in the original bot \$\endgroup\$ Apr 13, 2021 at 15:43
5
\$\begingroup\$

Shortcut

Updated to use the new format (slight performance gain, which adds up):

def strategy(last_results):
    return 50

Original:

def strategy(last_results):
    return ()

def turn(current_value, *args):
    return current_value != 50

Not the patient type. Takes the reward of 100 on turn 50, instead of being a team player and getting taken advantage of down the line. Shortcut has some trust issues.

I'll start on a more complicated bot soon.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Nice! Surprisingly successful. \$\endgroup\$ Apr 10, 2021 at 13:16
  • \$\begingroup\$ Nearly same logic as my bot though, +1..... \$\endgroup\$
    – Wasif
    Apr 10, 2021 at 16:46
4
\$\begingroup\$

FunnyNumber

def strategy(last_results):
	if len(last_results) > 420:
		return last_results[420][0]
	return 69

Try it online!

Like the joke itself, this bot is very uncreative. If there has been more than 420 rounds, FunnyNumber returns the result of the 420th (0-indexed) round. Otherwise, it counts up to 69 to try to earn 138 points.

Interestingly, this isn't the worse bot. Running a couple of arenas locally with this as a contestant, FunnyNumber gets 3/6 wins

\$\endgroup\$
5
  • 2
    \$\begingroup\$ Honestly, I was quite surprised to see your username in the corner instead of Lyxal's. \$\endgroup\$
    – user
    Apr 10, 2021 at 16:05
  • \$\begingroup\$ @user I wanted to snipe Lyxal from doing this more than I dislike this meme \$\endgroup\$ Apr 10, 2021 at 16:06
  • \$\begingroup\$ I doubt Lyxal would have done that anyway. You've become your own enemy, caird, worse than your enemy, in fact. I'm ashamed of you. :P \$\endgroup\$
    – user
    Apr 10, 2021 at 16:08
  • 3
    \$\begingroup\$ You became the very thing you swore to destroy! \$\endgroup\$
    – hyper-neutrino
    Apr 10, 2021 at 16:15
  • \$\begingroup\$ Your programme is incorrect. It should be [0] instead of [1], since the second item in the tuple is whether the game was won or not, and is a bool rather than an int. \$\endgroup\$ Apr 10, 2021 at 16:50
4
\$\begingroup\$

Zoidberg

def strategy(_):
        return 48

Zoidberg is like a crab, but different. Credit to user for the original idea.

I brute forced what number provides an optimal score with this approach. Surprisingly, this is better overall than Crab. Running locally I get

Bot Score Wins Overall
Crab 12th 1st 6th
Zoidberg 5th 5th 5th
\$\endgroup\$
4
\$\begingroup\$

High to Low

def strategy(last_results):
    if last_results:
        losses = [r[0] for r in last_results if not r[1]]
        if losses:
            return max(1, min(losses)-len(losses))
        num_of_rounds = len(last_results)
        if num_of_rounds>15 and len(losses)/num_of_rounds>0.6:
            return 1
    return 98

This bot always counts to 98 unless it has lost a round. Once this bot loses a round, it only counts to numbers lower than its lowest loss. If it has lost more than 60% of the rounds, it plays 1.

Edit: Changed the algorithm for calculating the number the bot should count to after losing from max(1, min(losses)-1-len(losses)) to max(1, min(losses)-len(losses)). Hopefully, this should allow the bot to capitalize on static bots because it now counts to 1 below and not 2 below.

\$\endgroup\$
1
  • \$\begingroup\$ This is almost a contender for first place with Low Blow... nice one \$\endgroup\$ Apr 12, 2021 at 22:26
3
\$\begingroup\$

Copycat

import urllib.request
import bs4 as bs
import random
contents = urllib.request.urlopen("https://codegolf.stackexchange.com/questions/223202/cooperative-counting").read()
html = bs.BeautifulSoup(contents, 'html.parser')

answers = html.findAll('div', {"class": "answer"})

strategies = []
for answer in answers:
    code = answer.find('div', {"class": "answercell"}).findAll('code')
    for any in code:
        strategies.append(str(any.contents[0]))

def newStrat(badOne = None):
    if not badOne == None: strategies.remove(badOne)
    return random.choice(strategies)
pick = newStrat()

def strategy(games):
    global pick
    try:
        exec(pick.replace('strategy','strategycopy'),globals())
        return strategycopy(games)
    except:
        pick = newStrat(pick)
        return strategy(games)

I didn't really feel like making a strategy up, so this bot will just copy someone elses answer.

\$\endgroup\$
11
  • \$\begingroup\$ I think this is off-topic, but I did make this answer specifically for this post \$\endgroup\$
    – Max
    Apr 11, 2021 at 3:24
  • \$\begingroup\$ Do you think the question is off topic or your answer? The former isn’t, but if it’s the latter, I feel you should have asked about it first. I don’t know if it’s valid, but you can ask the OP or in the Nineteenth Byte, the site’s main chat room. And welcome to Code Golf! \$\endgroup\$
    – user
    Apr 11, 2021 at 3:29
  • \$\begingroup\$ @jumbot what do you think? \$\endgroup\$
    – Max
    Apr 11, 2021 at 3:32
  • \$\begingroup\$ @Max I didn't have a rule against copying other people's submissions, I guess, so I'll have to accept this. But note that only one strategy will be picked for the entire tournament - i.e. while strategy is run many times, pick will only be set once when I import the file, so you may want to think about whether that's what you want. \$\endgroup\$ Apr 11, 2021 at 10:13
  • \$\begingroup\$ I could easily change it to choose a random strategy each time, but I'm feeling lucky!! \$\endgroup\$
    – Max
    Apr 11, 2021 at 11:02
3
\$\begingroup\$

Tit for tat

def strategy(last_results):
    return last_results[-1][0] if last_results else 100

Very basic strategy.

\$\endgroup\$
2
  • \$\begingroup\$ Should be last_results[-1][0] if you just want to return the value that won last round. \$\endgroup\$ Apr 12, 2021 at 0:10
  • \$\begingroup\$ @Spitemaster Thanks, it was late at night and I wasn't thinking correctly. \$\endgroup\$
    – Neil
    Apr 12, 2021 at 9:58
3
\$\begingroup\$

Meandian

import random
import statistics as stats
def strategy(last_results):
    def clamp(minimum, x, maximum):
        return max(minimum, min(x, maximum))
    last_results = sorted(map(lambda a: a[0], filter(lambda a: not a[1], last_results)))
    if not len(last_results):
        return random.randint(1,47)
    else:
        mean = round(stats.mean(last_results))
        median = round(stats.median(last_results))
    if mean == median:
        return mean
    elif mean > median:
        return clamp(1, median - random.randint(1,mean - median), 100)
    else: 
        return clamp(1, mean - random.randint(1,median - mean), 100)

This bot calculates the mean and median, and uses those calculations to predict a range of values that will be less than the other bot's choice, and will randomly choose one of them. It is also designed to be difficult to hardcode a solution to beat it but still handle other bots robustly.

edit: Meandian now only considers the round where the other bot wins during calculations to prevent itself from driving down itself unnessecarily.

\$\endgroup\$
4
  • \$\begingroup\$ Your last change broke it - line 10 now gives statistics.StatisticsError: mean requires at least one data point. You need to make sure that filter doesn't filter everything out. \$\endgroup\$ Apr 12, 2021 at 14:33
  • \$\begingroup\$ @Spitemaster I moved the filter before the length check so it should be fixed now. \$\endgroup\$
    – Aiden4
    Apr 12, 2021 at 14:36
  • \$\begingroup\$ Now you filter it twice, and the second time breaks. Take that out and it's all good \$\endgroup\$ Apr 12, 2021 at 14:39
  • 1
    \$\begingroup\$ @Spitemaster Oops- I meant ctrl x not ctrl c. Should be fixed. \$\endgroup\$
    – Aiden4
    Apr 12, 2021 at 14:40
3
\$\begingroup\$

The diff predictor

def strategy(last_results):
    previous_values = list()
    diffs = list()
    for num,result in last_results:
        if result:
            oppo_play = min(100,num+1)
        else:
            oppo_play = num
        if len(previous_values)>0:
            diffs.append(oppo_play-previous_values[-1])
        previous_values.append(oppo_play)

    if len(previous_values) == 0:
        move = 100
    elif len(previous_values) == 1:
        move = previous_values[0]
    else:
        diffs = diffs[-10:]
        if len(diffs)>2:
            # remove outliers
            diffs.remove(min(diffs))
            diffs.remove(max(diffs))
        meandiff = sum(diffs)/len(diffs)
        move = previous_values[-1] + meandiff -1

    if move==99 and len(last_results)<950:
        # Let's be nice (for a while)
        move=100
    return min(100, max(1, round(move)))

Predicts what the opponent will play next based on the average difference between consecutive previous plays (discarding highest and lowest, to avoid being fooled by FunnyNumber). Assumes the opponent played N+1 whenever it wins with N.

It plays 100 as long as the opponent does the same, until towards the last games, where it decides there's no use in being nice anymore.

\$\endgroup\$
3
\$\begingroup\$

Lumberjack

The plan is get to a winning position and then head towards 1 faster than the opponent can recover. Partly inspired by other bots here!

def strategy(last_results):
    round_number = len(last_results)
    if round_number == 0:
        return 100
    if all(i == (1, False) for i in last_results):
        return 1  # lumberjacks don't like crabs
    score, success = last_results[-1]
    if round_number == 1:
        if success and score == 100:
            return 100
        return max(1, score/2)
    old_score, old_success = last_results[-2]
    if success and old_success:  # won previous two rounds
        if score == 100:
            return 97  # try and get in under fair bots
        else:
            # minimal pruning, light regrowth
            return max(1, (score+old_score)*0.4 + 15)
    if not success:
        if not old_success:
            # heavy pruning
            return max(1, score*0.5)
            # light pruning
        return max(1, score*0.75)
    else:
        # regrow by small amounts
        return min(100, score + old_score*0.2 + 10)

I dreamt this up on Thursday, but I wasn't able to write it down and experiment until today. It's a late entry so I'm not expecting any prizes.

\$\endgroup\$
4
  • \$\begingroup\$ Did I submit after the deadline? Oh no :'( \$\endgroup\$ Apr 17, 2021 at 12:38
  • \$\begingroup\$ You're about one hour late... sorry. I can run it though and let you know how it would do :) \$\endgroup\$ Apr 17, 2021 at 13:19
  • \$\begingroup\$ @jumbot that awfully kind of you, thanks! \$\endgroup\$ Apr 17, 2021 at 13:21
  • 3
    \$\begingroup\$ Lumberjack comes 8th overall, 13th on score, and 8th on wins... not bad! \$\endgroup\$ Apr 17, 2021 at 13:33
2
\$\begingroup\$

The Desperate Fighter

Python 3, 285 bytes

def strategy(last_results):
    if len(last_results) == 0:
      return True,
    pos = last_results[-1][0]
    if pos in [49,50]:
      return False,
    if pos in list(range(10,21)):
      return False,
    else:
      return True,

def turn(current_value, *args):
    return args[0]

Try it online!

This bot will try to win in anyways it can....

Bot handling

  • Lyxal: I stop at 50 to attack his bot

  • math: I stop at 10 to 20 for defence to his bot

  • Redwolf: I stop at 49 to attack his bot

\$\endgroup\$
9
  • \$\begingroup\$ Your programme lacks a case for last_results being empty... \$\endgroup\$ Apr 10, 2021 at 11:33
  • \$\begingroup\$ Also, I think you slightly misunderstand how this works - strategy should return the args that you want passed to turn, not call turn directly itself. Oh, and thanks for the submission btw :) \$\endgroup\$ Apr 10, 2021 at 11:34
  • \$\begingroup\$ @jumbot That means turn() would call stragety()? (ADDed empty tuple check) \$\endgroup\$
    – Wasif
    Apr 10, 2021 at 11:37
  • \$\begingroup\$ No, neither calls either. The controller calls them. \$\endgroup\$ Apr 10, 2021 at 11:37
  • 1
    \$\begingroup\$ Let us continue this discussion in chat. \$\endgroup\$
    – Wasif
    Apr 10, 2021 at 12:01
2
\$\begingroup\$

Naughty or Nice?

def strategy(last_results):
        scores = list(map(lambda a: a[0], last_results))

        if all(score == 100 for score in scores):
                if len(scores) == 999:
                        return 98
                return 100

        constant_bots = {
                69: 68,
                68: 67,
                50: 49,
                48: 47
        }

        return constant_bots.get(scores[-1], 1)

Naughty or Nice? works on a very simple premise: Every match, one bot must be Naughty.

  • If the opponent counts to 100, Naughty or Nice? considers it "nice", and so Naughty or Nice? must be Naughty. It counts up to 100 with its opponent until it reaches the final round, where it stops at 98
  • Otherwise, the other bot is Naughty, and so doesn't deserve any points. In this case, either:
    • The opponent is a "constant" bot (FunnyNumber, Zoidberg, Shortcut), so we return 1 less than them
    • The opponent is not a constant bot, so we throw a tantrum and play 1 for the rest of the match
\$\endgroup\$
1
\$\begingroup\$

Hard-coded

import random
def strategy(last_results):
  if len(last_results) == 0:
    return 65
  if last_results[-1][0] in [50,49]:
    return 49 # Shortcut detected, attack!
  if last_results[-1][0] in [69,65]:
    return 65 # FunnyNumber detected, attack!
  if last_results[-1][0]==1:
    return 1 # Crab detected, defend!
  else:
    return random.randint(1,100) # give away, my mind is boggling with numbers

It is mainly designed to compete against caird and Redwolf and user

\$\endgroup\$
0
1
\$\begingroup\$

Grim Trigger

def strategy(last_results):
    if any([not r[1] for r in last_results]):
        return 1
    else:
        return 100

This bot prefers to cooperate and count to 100, but if the opponent tries to troll it, it pulls the trigger and only counts to 1 for the rest of the round.

\$\endgroup\$
1
\$\begingroup\$

Hawk

A quick internet search reveals hawks eat pythons >:).

This strategy takes the average, weighted a bit, of the winning scores and returns its floor, or 47 at the beginning. Purely functional and not python >:).


A Pure Python version (untested), courtesy of caird coinheringaahing:

def strategy(last_results):
  if not last_results:
    return 47
  return max(1, min(50, sum(map(lambda t: t[0], last_results)) // (len(last_results)*1.5)))

In Hy

(defn strategy [last-results]
  (if last-results
    (max 1 (min 50
      (// (reduce +
           (map (fn[t] (get t 0)) last-results))
         (* 1.5 (len last-results)))))
    47))
\$\endgroup\$
5
  • \$\begingroup\$ A pure Python version by my understanding \$\endgroup\$ Apr 11, 2021 at 0:56
  • \$\begingroup\$ sorry I can't view TIO. You can edit it in if you want. \$\endgroup\$
    – Wezl
    Apr 11, 2021 at 0:57
  • \$\begingroup\$ This breaks the controller, at least on my local copy, but I'm working on it \$\endgroup\$
    – Wezl
    Apr 11, 2021 at 1:27
  • 1
    \$\begingroup\$ @Wezl Hi, I've suggested an edit to put the Python code above the Hy code. That way the auto-update script will consider the Python code instead of the Hy code. Thanks for the submission! \$\endgroup\$ Apr 11, 2021 at 10:15
  • 1
    \$\begingroup\$ I've rolled back the edit, that'll break the autoscript, as it will find the Hy answer as the first code block. There's no way to hide multiline code blocks in spoilers, unfortunately \$\endgroup\$ Apr 12, 2021 at 20:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.