[Question]
  [
## 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.
  
[Answer]
# 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.
[Answer]
# 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
[Answer]
# 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
[Answer]
# 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).
[Answer]
# 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.
[Answer]
# 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 [code-golf](/questions/tagged/code-golf "show questions tagged 'code-golf'")...  
[Try it online!](https://tio.run/##rZHBboMwDIbveQofW6mVxnpqJa57gt1QVEVgaCQIyHFQeXqWhDIajeNy@mN//n9HGSZ@9OYyz7obemIgZaq@E6LCGiyTYmymQ6ss3wmta9kebwL8YZoWEQ4hOzKQcCc4xj4@Sxz4L1vIQMScUDiUjggN30fVOjx5bE0aIYfrNcrOy8@oGq@@yWG81D2BBm3C0BYUkAb8c0AXmfwtR6HrwEKeQyG3ifLdFFuLW2sZKM6Z9F7RtNmaLyB5QfAeU2SN@FLeOumkWbvr7GN7qeueHxLO0P3TCuLt60oh5vkH)
[Answer]
# 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.
[Answer]
# FunnyNumber
```
def strategy(last_results):
	if len(last_results) > 420:
		return last_results[420][0]
	return 69
```
[Try it online!](https://tio.run/##K6gsycjPM/7/PyU1TaG4pCixJDW9UiMnsbgkvii1uDSnpFjTioszM00hJzUPVVjBTsHEyAAoyVmUWlJalKeALBsNlIqNNojlgkmaWf7/DwA "Python 3 – 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
[Answer]
# Zoidberg
```
def strategy(_):
        return 48
```
Zoidberg is like a crab, but different. Credit to [user](https://codegolf.stackexchange.com/users/95792/user) for [the original idea](https://chat.stackexchange.com/transcript/message/57613061#57613061).
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 |
 
[Answer]
# 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.
[Answer]
# 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.
[Answer]
## Tit for tat
```
def strategy(last_results):
    return last_results[-1][0] if last_results else 100
```
Very basic strategy.
[Answer]
# 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.
[Answer]
# 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.
[Answer]
# 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.
[Answer]
# The Desperate Fighter
# [Python 3](https://docs.python.org/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!](https://tio.run/##dY/dCsIwDIXv9xS57GRC58@FQm99Au/GGEWzOSjdSFNhT19bdeBvrpKc8yWcceLLYNchnLEFx6QZu0kY7bghdN6wy/cZxBoHBwpehWpZ1pWs72rfgkH7zoFSIB8wACF7snAkj8VMpJO9hWqzK7ay/nAetHFfVtM7FqRth6KUxarM8/8Uxu7n9yxFTbM4eSK03Fy1iQIsNHVz3CeRNjFjCDc "Python 3 – 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
[Answer]
# 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
[Answer]
# 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
[Answer]
# 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.
[Answer]
# 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](http://hylang.org/)
```
(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))
```
]