The Celestial Bureaucracy KoTH

15

4

In Imperial China, ranks in society were not decided by birth or wealth, but by a person's ability to excel in the Imperial Examinations. The Jade Emperor, divine ruler of the Heavens, has called for all his subjects to be examined to determine their worth, and whom to next give the Divine Mandate to rule China.

Rules of the Bureaucracy:

  • The Divine Bureaucracy consists of non-negative integer-valued ranks, starting with 0. Each member (bot) of the bureaucracy belongs to one rank. Each rank can hold arbitrary many members, but can't be empty unless all ranks above are empty
  • At the start of the game, all members have rank 0
  • Every turn, each member of the bureaucracy has to answer an exam. The exam consist of correctly guessing the boolean values of a list. The length of the list is the number of the rank above the member.
  • The exam questions are prepared by a random member of the rank above. Members of the highest rank get their questions directly from the JadeEmperor (see below)
  • A member scoring at least 50% on their exam is eligible for Promotion. A member scoring less than 50% on their exam is eligible for Demotion.
  • A member eligible for Demotion has their rank decreased by one only if there is a member eligible for Promotion on the rank below to take their place.
  • All members eligible for Promotion have their rank increased by one as long as this leaves no rank empty.
  • If not all eligible members can be Demoted or Promoted, the preference goes to those of lowest (for Demotion) resp. highest (for Promotion) score. Ties are broken randomly.
  • The rank of a member can only change by at most 1 each turn.

Rules of the game:

  • Each bot will be randomly assigned an ID at the start of the game, which will not change over its course. The JadeEmperor has the ID -1, all others have consecutive non-negative IDs, starting with 0.
  • All bots compete at the same time
  • The game runs for 100 turns, the score of the bot is its average rank possessed over that time.
  • Total score is acquired by running 1000 games and averaging the results.
  • Each Bot is a Python 3 class implementing the following four functions:
    • ask(self,n,ID), which makes an exam by returning a list of Booleans of length n. ID is the ID of the bot who has to guess that list. ask() can be called many times during a single round for any bot, but also not at all.
    • answer(self,n,ID), which is an attempt to answer an exam by returning a list of Booleans of length n. ID is the ID of the bot whose ask() generated the exam. answer() is called exactly once per round for each bot.
    • update(self,rankList,ownExam,otherExams) is called once the Controller has performed all Pro- and Demotions. Its arguments are: A list of integers, listing all ranks by ID of all bots; a tuple, consisting of two lists, first the exam questions, then the answers the bot gave (in case it forgot); then a list of tuples, similarly consisting of exam-answer pairs, this time for all exams the bot handed out.
    • __init__(self, ID, n) passes the bot its own ID and the number of competing bots.
  • Classes are allowed to implement other functions for private use
  • Defining further variables and using them to store data about past exams is explicitly allowed.
  • Programming meta-effects are forbidden, meaning any attempts to directly access other bots' code, the Controller's code, causing Exceptions or similar. This is a contest of strategies for the exams, not of code hacking.
  • Bots trying to help each other are explicitly allowed, as long as they don't do it via meta-effects, but purely by the information passed through update()
  • Other languages are allowed only in case they can be easily converted to Python 3.
  • The library numpy will be imported as np. The version is 1.6.5, meaning it uses the old random library. If you have numpy 1.7, the old functions are available under numpy.random.mtrand for testing. Please remember to strip the mtrand for submission.
  • If a bot causes an Exception during runtime, it is disqualified. Any bot whose code is so obfuscated that it's impossible to tell if it generates a list of length n when ask() or answer() is called will also be disqualified preemptively. A bot forcing me to deep-copy outputs gets -1 on the score.
  • Class names have to be unique
  • Multiple bots per person are allowed, but only the latest version will be taken of iteratively updated bots.
  • Since there seems to be some confusion about bot similarity:
    • You are not allowed to post a copy of another bot. This is the only Standard Loophole which really applies in this challenge.
    • You are allowed to have shared code with other bots, including bots of other people.
    • You are not allowed to submit a bot which differs from another only by a trivial change to the strategy (like a change in the seed for the question generation) unless you can prove that the number of such carbon copy bots is the minimum required for successful enactment of their strategy (That will usually be two bots for a cooperation).

Example Bots:

The JadeEmperor is always part of the game, but does not compete; he serves as generator for exams of highest-rank bots. His exams are random, but not uniformly, to allow smart bots a way to advance.

class JadeEmperor:
    def __init__(self):
        pass

    def ask(self,n,ID):
        num=min(np.random.exponential(scale=np.sqrt(np.power(2,n))),np.power(2,n)-1)
        bi=list(np.binary_repr(int(num),width=n))
        return [x=='0' for x in bi]

The Drunkard produces exams and answers completely randomly. He will be part of the game.

class Drunkard:
    def __init__(self,ID,n):
        pass

    def ask(self,n,ID):
        return list(np.random.choice([True,False],size=n,replace=True))

    def answer(self,n,ID):
        return list(np.random.choice([True,False],size=n,replace=True))

    def update(self,rankList,ownExam,otherExams):
        pass #out

The Plagiarist just copies previous exams. He will also be part of the game.

class Plagiarist:
    def __init__(self,ID,n):
        self.exam=[True]

    def ask(self,n,ID):
        return (self.exam*n)[0:n]

    def answer(self,n,ID):
        return (self.exam*n)[0:n]

    def update(self,rankList,ownExam,otherExams):
        self.exam=ownExam[0]

Controller code available here. For testing, you can put your own class into a Contestants.py file in the same folder, and they will be imported.

Chatroom can be found here.

The Examinations begin!

Current score, in higher precision (10000 runs) for Oct20:

$$ \begin{array}{|c|c|c|}\hline \textbf{Entrant}&\textbf{Author}&\textbf{Score}\\\hline % \text{Alpha}&\text{Sleafar}&9.669691\\ \hline \text{Gamma}&\text{Sleafar}&9.301362\\ \hline \text{Beta}&\text{Sleafar}&9.164597\\ \hline \text{WiQeLu}&\text{Purple P}&7.870821\\ \hline \text{StudiousBot}&\text{Dignissimus - Spammy}&7.538537\\ \hline \text{Santayana}&\text{Sara J}&7.095528\\ \hline \text{Plagiarist}&\text{}&6.522047\\ \hline \text{CountOracular}&\text{IFcoltransG}&5.881175\\ \hline \text{Thomas}&\text{Alien@System}&5.880041\\ \hline \text{Contrary}&\text{Draco18s}&5.529652\\ \hline \text{Marx}&\text{sugarfi}&5.433808\\ \hline \text{Drunkard}&\text{}&5.328178\\ \hline \text{YinYang}&\text{Purple P}&5.102519\\ \hline \text{Equalizer}&\text{Mnemonic}&4.820996\\ \hline \text{TitForTat}&\text{Anonymous}&3.35801\\ \hline \end{array}$$

Contests will be run with each new entry for the foreseeable future.

AlienAtSystem

Posted 2019-09-20T13:20:10.943

Reputation: 488

If bots helping each other are allowed, can't I just spam the question with several copies of cooperating bots? – Sleafar – 2019-09-20T15:30:21.673

1Copies of bots are a Standard Loophole, so no. If you try to abuse the multiple bots per author rule by submitting almost-but-not-quite-copies, I will remove it. – AlienAtSystem – 2019-09-20T15:46:51.867

1@AlienAtSystem Why are you allowing bots helping each other? It just seems like more chaos and randomness to deal with. – Don Thousand – 2019-09-20T15:54:08.490

2Why are the constructor arguments ID, n but the other method arguments n, ID? – Purple P – 2019-09-20T16:01:33.610

1@DonThousand because I believe that under the constraints given, it is quite a feat to make two bots that A) successfully handshake (note that the Plagiarizer might accidentially play man in the middle) and B) then enact a strategy that reliably helps that bot but no other to rise. – AlienAtSystem – 2019-09-20T16:06:43.080

@PurpleP Organic growth of code. For the answering, n is more important: It's the number of bools you have to return. For the initialising, I originally only handed over the ID, but then thought knowing the number of contestants is important, too. – AlienAtSystem – 2019-09-20T16:06:52.940

(1) I'm not even sure how cooperation is possible through Update (it runs after the exams; is it to help bots under you move up?) and (2) I don't see a benefit to cooperation. – Draco18s no longer trusts SE – 2019-09-20T16:20:51.563

May we assume that the number of questions will not change between rounds? – Purple P – 2019-09-20T16:26:30.917

@PurpleP it's in the rules: The length of the list is the number of the rank above the member. So at rank 0, you get 1 question, at rank 7 you get 8. – AlienAtSystem – 2019-09-20T16:28:07.023

@Draco18s I thought the same thing, so I made a bot that cooperates with everyone. Currently, it beats both of Purple P's bots. – None – 2019-09-20T18:01:31.503

"The length of the list is the number of the rank above the member." do I understand this correctly that once you reach rank 1 (or 0, depending on your favorite numbering) you are stuck there forever and aren't asked any questions? – my pronoun is monicareinstate – 2019-09-21T10:12:37.870

1@someone ranks count upwards. You start at 0 and work you way to higher numbers – AlienAtSystem – 2019-09-21T17:51:42.190

1@Alien If creating multiple very similar bots is not allowed, helping other bots is nearly impossible (and why would you, anyway, if they're not yours). If so, the optimal strategy is to ask uniformly random questions. Is that correct? – my pronoun is monicareinstate – 2019-09-22T02:44:40.530

1@someone No and No. The second question is clearly invalidated by submissions outperforming the drunkard. And for the first: If your two cooperators manage to successfully haul themselves in the top ranks, then an asymmetric teams scores better than a symmetric one, because one bot would yield to the other and ensure his partner gets highest rank and he stays 1 below. – AlienAtSystem – 2019-09-22T06:14:24.443

@AlienAtSystem If I want to make updates to my bot, how should I do that? – Dignissimus - Spammy – 2019-10-09T18:25:50.670

@Dignissimus-Spammy if it's just improvement of the algorithm, simply edit your answer. And then possibly ping me in chat so I know something changed – AlienAtSystem – 2019-10-10T15:28:13.457

You don't need to capitalise the C in IFcoltransG in the leaderboard. (I mean, it looks like gibberish either way, but that's no excuse :) – IFcoltransG – 2019-10-14T07:48:42.590

Answers

4

Santayana

Those who cannot remember the past are condemned to repeat it. So we make our decisions based on how the others have acted in the past, answering based on what answer the asker has usually expected from us at a given index, and asking for the answer they've given us the least often at a given index.

import numpy as np

class Santayana:
    """
    Those who cannot remember the past are condemned to repeat it
    """
    def __init__(self, ID, num_competitors):
        self.ID = ID
        self.exams_taken = {}
        self.exams_issued = {}
        self.last_exam_asker = None
        self.recent_exam_takers = []

        for i in range(num_competitors):
            self.exams_taken[i] = []
            self.exams_issued[i] = []

    def ask(self, length, taker_ID):
        # Remember who asked
        self.recent_exam_takers.append(taker_ID)
        new_exam = []

        # At every index, expect the answer they've given the least often (default to False if equal)
        for i in range(length):
            trues = 0
            falses = 0
            for exam in self.exams_issued[taker_ID]:
                if len(exam) <= i: continue
                if exam[i]:
                    trues += 1
                else:
                    falses += 1
            new_exam.append(trues < falses)
        return new_exam

    def answer(self, num_answers, asker_ID):
        self.last_exam_asker = asker_ID
        if asker_ID == -1:
            # Copy emperor's process to hopefully get a similar exam
            num = min(np.random.exponential(scale=np.sqrt(np.power(2,num_answers))),np.power(2,num_answers)-1)
            as_bin = list(np.binary_repr(int(num),width=num_answers))
            return [x=='0' for x in as_bin]
        else:
            new_answer = []

            # At every index, give the answer that's been correct the greatest number of times (default to True if equal)
            for i in range(num_answers):
                trues = 0;
                falses = 0;
                for exam in self.exams_taken[asker_ID]:
                    if len(exam) <= i: continue
                    if exam[i]:
                        trues += 1
                    else:
                        falses += 1
                new_answer.append(trues >= falses)
            return new_answer

        return [True for i in range(num_answers)]

    def update(self, rank_list, own_exam, other_exams):
        if self.last_exam_asker > -1:
            # Save the exam we took, unless it was from the Emperor - we already know how he operates
            self.exams_taken[self.last_exam_asker].append(own_exam[0])
        for i in range(len(self.recent_exam_takers)):
            # Save the responses we got
            self.exams_issued[i].append(other_exams[i][1])

        self.recent_exam_takers = []

Sara J

Posted 2019-09-20T13:20:10.943

Reputation: 2 576

3

Studious Bot

This bot studies for tests! It attempts to find patterns in tests given out by various bots and acts accordlingly.

On my end, so far, this bot outperforms all other bots that I could get working on my computer except from Alpha, Beta and Gamma (who have been programmed to work together). The bot doesn't make use of the fact that teaming is allowed because I felt that it was a bit like cheating and slightly dirty. Looking over it though, teaming seems to seems to be quite effective.

The bot attempts to recognise when the answers to tests are random and in response matches that to hopefully average 50% on tests.

The bot also attempts to recognise when a bot has merely flipped its answers to throw off other bots who are tying to predict their behaviour, however I haven't programmed it to specifically act on this yet.

I've annotated the code with a few comments in order to make it easier to read

import random
import numpy as np


class StudiousBot:
    GRAM_SIZE = 5
    def __init__(self, identifier, n):
        self.id = identifier
        self.ranks = {i: 0 for i in range(n)} # Stores ranks
        self.study_material = {i: [] for i in range(n)} # Stores previous exam data
        self.distribution = {i: [] for i in range(n)} # Stores the percentage of answers that were `True` on a Bot's tests over time
        self.last_examiner = None

    def ask(self, n, identifier):
        # This bot gives random tests, it doesn't bother making them difficult based on answers to them
        # The reason for this is that I can't personalise the tests for each bot
        return [random.choice([True, False]) for i in range(n)] 

    def answer(self, n, examiner_id):
        self.last_examiner = examiner_id
        if examiner_id == -1:
            return StudiousBot.answer_emperor(n) # Easy win, I know the distribution of answers for the Emperor's tests

        bother_predicting = True # Whether or not the Bot will attempt to predict the answers to the exam
        study_material = self.study_material[examiner_id]
        distribution = self.distribution[examiner_id]
        if len(distribution) > 0: # If there is actually data to analyse
            sd = StudiousBot.calculate_standard_deviation(distribution)
            normalised_sd = StudiousBot.calculate_normalised_standard_deviation(distribution)

            if abs(30 - sd) < 4: # 30 is the expected s.d for a random distribution
                bother_predicting = False # So I won't bother predicting the test 

            if abs(sd - normalised_sd * 2) > 4: # The bot is merely inverting answers to evade being predicted
                pass # However, at this time, I'm not certain how I should deal with this. I'll continue to attempt to predict the test 


        if bother_predicting and len(study_material) >= StudiousBot.GRAM_SIZE:
            return StudiousBot.predict(study_material, n)

        return [random.choice([True, False]) for i in range(n)]

    def predict(study_material, n): # Predicts the answers to tests with `n` questions
        grams = StudiousBot.generate_ngrams(study_material, StudiousBot.GRAM_SIZE) # Generate all n-grams for the study material
        last_few = study_material[-(StudiousBot.GRAM_SIZE - 1):] # Get the last 9 test answers
        prediction = None
        probability = -1
        for answer in [True, False]: # Finds the probabiility of the next answer being True or False, picks the one with the highest probability
            new_prediction = last_few + [answer]
            new_probability = grams.count(new_prediction)         

            if new_probability > probability:
                prediction = answer
                probability = new_probability

        if n == 1:
            return [prediction]

        return [prediction] + StudiousBot.predict(study_material + [prediction], n-1)          


    @staticmethod
    def calculate_standard_deviation(distribution):
        return np.std(distribution)

    def calculate_normalised_standard_deviation(distribution): # If the answers happen to be inverted at some point, this function will return the same value for answers that occured both before and after this point  
        distribution = list(map(lambda x: 50 + abs(50-x), distribution))
        return StudiousBot.calculate_standard_deviation(distribution)   

    @staticmethod
    def generate_ngrams(study_material, n):
        assert len(study_material) >= n
        ngrams = []
        for i in range(len(study_material) - n + 1):
            ngrams.append(study_material[i:i+n])

        return ngrams

    def update(self, ranks, own_exam, other_exams):
        self.ranks = dict(enumerate(ranks))
        if self.last_examiner != -1:
            self.study_material[self.last_examiner] += own_exam[0]
            self.distribution[self.last_examiner].append(own_exam[0].count(True) / len(own_exam[0]) * 100) # Stores the percentage of the answers which were True

    @staticmethod
    def answer_emperor(n): # Algorithm to reproduce Emperor's distribution of test answers  
        exp = np.random.exponential(scale=np.sqrt(np.power(2,n)))
        power = np.power(2,n) - 1        
        num = min(exp, power)
        bi = list(np.binary_repr(int(num), width=n))
        return [x == '0' for x in bi]

Dignissimus - Spammy

Posted 2019-09-20T13:20:10.943

Reputation: 449

Judging by our performance, you have the best algorithm for answering and Wi Qe Lu has the best algorithm for asking. I propose that we combine our bots into a single bot, called Xuézhě (Chinese for "scholar"), which coincidentally sounds kind of like "switcher". – Purple P – 2019-10-10T17:01:13.940

I hacked it up and ran the examinations on my machine. Curiously, it outscored Studious Bot, but not Wi Qe Lu. – Purple P – 2019-10-10T18:23:13.320

@PurpleP Haha! That sounds very interesting, I don't think there's enough time for me to improve my bot but you can post it as a submission here – Dignissimus - Spammy – 2019-10-18T13:54:43.240

3

Count Oracular

This bot uses an algorithm that averages the exams of all other working bots, (given the round number and some terrible heuristics) for deciding what each other bot will set as the exam.
The Count asks its exams using an md5 hash. Both its questions and its answers are therefore deterministic. It ignores most inputs, asking and answering the exact same sequences of booleans, rain or shine, including against Jade Emporer.

import numpy as np
import hashlib

class CountOracular:
    '''Uses very little external data to make heuristical statistical
    deterministic predictions about the average exam.
    (Assonance not intended.)
    To generate its own exams, uses a deterministic hash.'''
    def __init__(self, id, number_of_bots):
        self.last_round = []
        #functions for calculating what other bots will likely do.
        self.bots_calculators = [
            self._jad, #Jade Emporer
            self._alp, #Alpha
            self._bet, #Beta
            self._gam, #Gamma
            self._wiq, #Wi Qe Lu
            self._stu, #StudiousBot
            self._pla, #Plagiarist
            self._san, #Santayana
            self._tho, #Thomas
            self._dru, #Drunkard
            self._yin, #YinYang
            self._con, #Contrary
            self._tit, #TitForTat
            self._equ, #Equalizer
            self._mar, #Marx
        ]
        self.bot_types = len(self.bots_calculators)
    def ask(self, n, id):
        #if we can, show that hardcoding is no match for the power of heuristics:
        if n == 2:
            return [False, True]
        #otherwise, refer to the wisdom of Mayor Prentiss in order to command The Ask
        #i.e. hashes a quote, and uses that as the exam.
        salt = b"I AM THE CIRCLE AND THE CIRCLE IS ME " * n
        return self._md5_from(salt, n)
    def answer(self, n, id):
        #uses the power of heuristics to predict what the average bot will do
        #ignores all inputs except the length of the output
        #very approximate, and deterministic
        #i.e. every game, Count Oracular will send the same lists of answers, in the same order
        best_guess_totals = [0.5] * n #halfway between T and F
        for bot in self.bots_calculators:
            exam, confidence = bot(n)
            if not exam:
                continue
            while len(exam) < n:
                #ensure exam is long enough
                exam += exam[:1]
            exam = exam[:n] #ensure exam is short enough
            #map T and F to floats [0,1] based on confidence
            weighted_exam = [0.5+confidence*(0.5 if q else -0.5) for q in exam]
            best_guess_totals = [current+new for current,new in zip(best_guess_totals, weighted_exam)]
        best_guess_averages = [total/self.bot_types
            for total
            in best_guess_totals
        ]
        best_guess = [avg > 0.5 for avg in best_guess_averages]
        self.last_round = best_guess
        return best_guess
    def update(self, ranks, own, others):
        pass
    def _md5_from(self, data, n):
        md5 = hashlib.md5(data)
        for i in range(n):
            md5.update(data)
        exam = []
        while len(exam) < n:
            exam += [x == "0"
                for x
                in bin(int(md5.hexdigest(), 16))[2:].zfill(128)
            ]
            md5.update(data)
        return exam[:n]
    def _invert(self, exam):
        return [not val for val in exam]
    def _digits_to_bools(self, iterable):
        return [char=="1" for char in iterable]
    def _plagiarise(self, n):
        copy = (self.last_round * n)[:n]
        return copy

    '''functions to calculate expected exams for each other bot:
       (these values, weighted with corresponding confidence ratings,
       are summed to calculate the most likely exam.)'''
    def _jad(self, n):
        '''Calculate the mean of _jad's distribution, then
        use that as the guess'''
        mean = max(int(np.sqrt(np.power(2,n))), (2<<n)-1)
        string_mean = f"{mean}".zfill(n)
        exam = self._invert(self._digits_to_bools(string_mean))
        return exam, 0.5
    def _alp(self, n):
        '''Alpha uses a predictable hash,
        until it figures out we aren't Beta,
        modelled by the probability of giving or solving
        Alpha's exam'''
        #probability that Alpha thinks we're Beta
        #assuming we fail to pretend to be Beta if we meet Alpha
        chance_beta = ((1 - 1/self.bot_types) ** n) ** 2
        return self._md5_from(b"Beta", n), chance_beta
    def _gam(self, n):
        '''Gamma is like Beta, except after realising,
        switches to 50-50 random choice of inverse
        either Beta or Alpha's hash'''
        #probability that Gamma thinks we're Alpha still
        #(Unlikely that Gamma will think we're Beta;
        #we'd need to fail Alpha but pass Beta,
        #therefore, not accounted for)
        chance_unknown = ((1 - 1/self.bot_types) ** n) ** 2
        #default exam that assumes that Gamma thinks we're Alpha
        exam = self._md5_from(b"Beta", n)
        if chance_unknown > 0.5:#there exists a better heuristic here
            #assume Gamma will consider us Alpha
            confidence = chance_unknown
        else:
            #assume Gamma considers us neither Alpha nor Beta
            alpha = self._invert(self._md5_from(b"Beta", n))
            beta = self._invert(self._md5_from(b"Alpha", n))
            #check for bools where both possible exams match
            and_comp = [a and b for a, b in zip(alpha, beta)]
            nor_comp = [not (a or b) for a, b in zip(alpha, beta)]
            #count up matches vs times when fell back on default
            #to calculate ratio of default
            #to bools where hashes agree
            confidence_vs_default = (sum(and_comp)+sum(nor_comp)) / n
            confidence = confidence_vs_default * chance_unknown + (1 - confidence_vs_default) * (1 - chance_unknown)
            for i in range(n):
                if and_comp[i]:
                    exam[i] = True
                if nor_comp[i]:
                    exam[i] = False
        return exam, confidence
    def _bet(self, n):
        '''Beta is like Alpha, but with a different hash'''
        #probability we haven't matched with Beta yet
        #i.e. probability that Beta still thinks we're Alpha
        chance_alpha = ((1 - 1/self.bot_types) ** n) ** 2
        return self._md5_from(b"Alpha", n), chance_alpha
    def _wiq(self, n):
        '''Wi Qe Lu is hard to model, so we pretend
        that it mimicks Plagiarist for the most part'''
        if n == 1:
            #first round is random
            return [False], 0
        #other rounds are based on exams it met
        #leaning towards same as the previous exam
        return self._plagiarise(n), 0.1
    def _stu(self, n):
        '''StudiousBot is random'''
        return [False] * n, 0
    def _pla(self, n):
        '''Plagiarist copies the exams it received,
        which can be modelled with the standard prediction
        calculated for the previous round, padded with its first
        element.'''
        if n == 1:
            return [True], 1
        return self._plagiarise(n), 0.3
    def _san(self, n):
        '''Santayana is based on answers, which we don't predict.
        Modelled as random.'''
        #mostly random, slight leaning towards default False
        return [False] * n, 0.1
    def _tho(self, n):
        '''Thomas has an unpredictable threshold.'''
        #for all intents, random
        return [False] * n, 0
    def _dru(self, n):
        '''Drunkard is utterly random.'''
        return [False] * n, 0
    def _yin(self, n):
        '''YinYang inverts itself randomly, but not unpredictably.
        We can model it to find the probability. Also notably,
        one index is inverted, which factors into the confidence
        especially for lower n.'''
        if n == 1:
            #one element is inverted, so whole list must be False
            return [False], 1
        if n == 2:
            #split half and half randomly; can't predict
            return [True] * n, 0
        #cumulative chance of mostly ones or mostly zeros
        truthy = 1
        for _ in range(n):
            #simulate repeated flipping
            truthy = truthy * 0.44 + (1-truthy) * 0.56
        falsey = 1 - truthy
        if falsey > truthy:
            return [False] * n, falsey - 1/n
        return [True] * n, truthy - 1/n
    def _con(self, n):
        '''Contrary is like Jade Emporer, but inverts itself
        so much that modelling the probability of inversion
        is not worth the effort.'''
        #there are some clever ways you could do statistics on this,
        #but I'm content to call it uniform for now
        return [False] * n, 0
    def _tit(self, n):
        '''TitForTat is most likely to give us False
        but the confidence drops as the chance of having
        met TitForTat increases.
        The square root of the probability we calculate for
        Alpha, Beta and Gamma, because those also care about what
        we answer, whereas TitForTat only cares about what we ask'''
        #probability that we've not given TitForTat an exam
        chance_friends = (1 - 1/self.bot_types) ** n
        return [False] * n, chance_friends
    def _equ(self, n):
        '''Equalizer always asks True'''
        #certain that Equalizer's exam is all True
        return [True] * n, 1
    def _mar(self, n):
        '''Marx returns mostly True, randomised based on our rank.
        We don't predict our rank.
        There's ~50% chance an answer is random'''
        #75% chance we guess right (= 50% + 50%*50%)
        return [True] * n, 0.75

IFcoltransG

Posted 2019-09-20T13:20:10.943

Reputation: 191

A great idea in theory, but in its first contest Count Oracular performed worse than YinYang, despite its efforts to simulate YinYang. – Purple P – 2019-10-10T17:09:57.107

1@PurpleP Yes, it's not very good. The reason is that it tries to choose a 'generally optimal' strategy by averaging all of the specific strategies together. It doesn't for example use a strategy tailored to beat YinYang when it encounters YinYang. It doesn't even use a specific strategy on Jade Emporer: it just adds the Jade Emporer strategy to the average. It'll be better than random, but not by much. – IFcoltransG – 2019-10-10T21:20:39.437

Marx has been fixed. You should update Count Oracular to predict it. – Purple P – 2019-10-13T04:11:54.300

@PurpleP Marx should be supported now. It's like it's 1917 again. – IFcoltransG – 2019-10-14T07:45:11.290

2

YinYang

Answers either all True or all False, except for one index randomly chosen to be the opposite. Asks the opposite of what it answers. Swaps randomly to throw off opponents.

import random

class YinYang:
    def __init__(self, ID, n):
        self.exam = True

    def update(self, rankList, ownExam, otherExams):
        if random.random() < 0.56:
            self.exam = not self.exam

    def answer(self, n, ID):
        a = [not self.exam] * n
        a[random.randint(0, n-1)] = self.exam
        return a

    def ask(self, n, ID):
        e = [self.exam] * n
        e[random.randint(0, n-1)] = not self.exam
        return e

Wi Qe Lu (Switcheroo)

Answers and asks randomly in the first round. Afterwards, he uses the answers from the previous exam, and changes a question if an above-average number of competitors got it right.

class WiQeLu:
    def __init__(self, ID, n):
        self.rounds = 1
        self.firstexam = True
        self.firstanswer = True
        self.lastexaminer = -1
        self.exam = []
        self.pastanswers = {}

    def update(self, rankList, ownExam, otherExams):
        questions, lastanswers = ownExam
        self.pastanswers[self.lastexaminer] = questions

        if len(otherExams) == 0:
            return
        correctCounts = [0 for i in otherExams[0][0]]
        for ourExam, response in otherExams:
            for i in range(len(response)):
                if ourExam[i] == response[i]:
                    correctCounts[i] += 1

        newExam = otherExams[0][0]
        meanWhoAnsweredCorrectly = sum(correctCounts) / len(correctCounts)
        for i in range(len(correctCounts)):
            if correctCounts[i] > meanWhoAnsweredCorrectly:
                newExam[i] = not newExam[i]
        self.exam = newExam

    def answer(self, n, ID):
        self.lastexaminer = ID
        if ID not in self.pastanswers:
            randomanswer = [random.randint(0, 1) == 1] * n
            self.pastanswers[ID] = randomanswer
            return randomanswer
        return (self.pastanswers[ID] * n)[:n]

    def ask(self, n, ID):
        if self.firstexam:
            self.firstexam = False
            self.exam = [random.randint(0, 1) == 1] * n
        return (self.exam * n)[:n]

Purple P

Posted 2019-09-20T13:20:10.943

Reputation: 919

5According to Google Translate "wi qe lu" is roughly translated as "I am penguin road." – Purple P – 2019-09-20T22:46:23.747

2

One bot of my own:

Thomas

A traveler from a far-away land, has some dangerous ideas about past results being indicative of future performance. He uses those to keep other bots down, unless that stifles his own advancement.

class Thomas:
    def __init__(self,ID,n):
        N=10
        self.ID=ID
        self.myrank=n
        self.lowerank=0
        #The highest number of questions is equal to the number of participants, so we can do this:
        self.probs=[{i:1.0/N for i in np.linspace(0,1,num=N)} for i in np.arange(n)]
        self.output=[0.5]*n

    def ask(self,n,ID):
        if self.myrank==1 and self.lowerrank > 1: #I can't advance without promoting somebody first
            return [self.output[i]>np.random.rand() for i in np.arange(n)]
        #Otherwise, try to step on their fingers by going against the expected probability
        return [self.output[i]<np.random.rand() for i in np.arange(n)]


    def answer(self,n,ID):
        return [self.output[i]>np.random.rand() for i in np.arange(n)]

    def update(self,rankList,ownExam,otherExams):
        #Update our ranks
        self.myrank=len([i for i in rankList if i==rankList[self.ID]])
        self.lowerrank=len([i for i in rankList if i==rankList[self.ID]-1])
        #Update our expectations for each input we've been given
        self.bayesianupdate(ownExam[0])
        for ex in otherExams:
            self.bayesianupdate(ex[1])
        #Compress into output variable
        self.output=[np.sum([l[entry]*entry for entry in l]) for l in self.probs]

    def bayesianupdate(self,data):
        for i in np.arange(len(data)):
            if data[i]: #Got a True
                self.probs[i].update({entry:self.probs[i][entry]*entry for entry in self.probs[i]})
            else: #Got a False
                self.probs[i].update({entry:self.probs[i][entry]*(1-entry) for entry in self.probs[i]})
            s=np.sum([self.probs[i][entry] for entry in self.probs[i]]) #Renormalize
            self.probs[i].update({entry:self.probs[i][entry]/s for entry in self.probs[i]})
```

AlienAtSystem

Posted 2019-09-20T13:20:10.943

Reputation: 488

Did you forget to indent your code after the class statement? – pppery – 2019-09-22T14:11:33.430

That's just the SE formatting catching me unawares. I'll fix it together with whatever caused an error in somebody's test when using this bot – AlienAtSystem – 2019-09-22T16:18:26.237

2

Alpha

Read the chat before downvoting. These bots don't violate any rules. The OP is even encouraging cooperating bots.

Alpha is forming a team together with Beta. Both are using a predefined set of exams to help each other rise up the ranks. Also both are taking advantage of bots using the same exams over and over.

import numpy as np
import hashlib

class Alpha:
    def __init__(self, ID, n):
        self.alpha = hashlib.md5(b"Alpha")
        self.beta = hashlib.md5(b"Beta")
        self.asker = -1
        self.betas = set(range(n)).difference([ID])
        self.fixed = set(range(n)).difference([ID])
        self.fixedExams = [[]] * n

    def ask(self,n,ID):
        if ID in self.betas:
            return self.md5ToExam(self.alpha, n)
        else:
            return list(np.random.choice([True, False], n))

    def answer(self,n,ID):
        self.asker = ID
        if self.asker == -1:
            return [True] * n
        elif self.asker in self.fixed and len(self.fixedExams[self.asker]) > 0:
            return (self.fixedExams[self.asker] * n)[:n]
        elif self.asker in self.betas:
            return self.md5ToExam(self.beta, n)
        else:
            return list(np.random.choice([True, False], n))

    def update(self,rankList,ownExam,otherExams):
        if self.asker >= 0:
            if self.asker in self.betas and ownExam[0] != self.md5ToExam(self.beta, len(ownExam[0])):
                    self.betas.remove(self.asker)
            if self.asker in self.fixed:
                l = min(len(ownExam[0]), len(self.fixedExams[self.asker]))
                if ownExam[0][:l] != self.fixedExams[self.asker][:l]:
                    self.fixed.remove(self.asker)
                    self.fixedExams[self.asker] = []
                elif len(ownExam[0]) > len(self.fixedExams[self.asker]):
                    self.fixedExams[self.asker] = ownExam[0]
        self.alpha.update(b"Alpha")
        self.beta.update(b"Beta")

    def md5ToExam(self, md5, n):
        return [x == "0" for x in bin(int(md5.hexdigest(), 16))[2:].zfill(128)][:n]

Sleafar

Posted 2019-09-20T13:20:10.943

Reputation: 2 722

I believe these three bots violate OPs rules as stated in both the prompt and the comments. – Don Thousand – 2019-09-23T15:39:58.743

@DonThousand If you read the discussion in the chat, you will see they don't violate the rules. https://chat.stackexchange.com/rooms/98905/imperial-exams-office

– Sleafar – 2019-09-23T16:03:43.323

Fair enough. My bad. – Don Thousand – 2019-09-23T16:08:56.630

@DonThousand So what was the point in downvoting them all? – Sleafar – 2019-09-23T16:23:52.357

I only downvoted Alpha. I can't undownvote, though. Make a superfluous edit and I'll fix it. – Don Thousand – 2019-09-23T16:50:31.117

1

Equalizer

Everyone should be equal (with none of this silly emperor nonsense), so provide as much social mobility as possible. Make the questions really easy (the answer is always True) so that people can succeed.

class Equalizer:
    def __init__(self, ID, n):
        self.previousAnswers = [[0, 0] for _ in range(n)]
        self.previousAsker = -1

    def ask(self, n, ID):
        return [True] * n

    def answer(self, n, ID):
        if ID == -1:
            return [True] * n

        # Assume that questions from the same bot will usually have the same answer.
        t, f = self.previousAnswers[ID]
        return [t >= f] * n

    def update(self, rankList, ownExam, otherExams):
        if self.previousAsker == -1:
            return

        # Keep track of what answer each bot prefers.
        counts = self.previousAnswers[self.previousAsker]
        counts[0] += ownExam[0].count(True)
        counts[1] += ownExam[0].count(False)

user48543

Posted 2019-09-20T13:20:10.943

Reputation:

1

Beta

Read the chat before downvoting. These bots don't violate any rules. The OP is even encouraging cooperating bots.

Beta is forming a team together with Alpha. Both are using a predefined set of exams to help each other rise up the ranks. Also both are taking advantage of bots using the same exams over and over.

import numpy as np
import hashlib

class Beta:
    def __init__(self,ID,n):
        self.alpha = hashlib.md5(b"Alpha")
        self.beta = hashlib.md5(b"Beta")
        self.asker = -1
        self.alphas = set(range(n)).difference([ID])
        self.fixed = set(range(n)).difference([ID])
        self.fixedExams = [[]] * n

    def ask(self,n,ID):
        if ID in self.alphas:
            return self.md5ToExam(self.beta, n)
        else:
            return list(np.random.choice([True, False], n))

    def answer(self,n,ID):
        self.asker = ID
        if self.asker == -1:
            return [True] * n
        elif self.asker in self.fixed and len(self.fixedExams[self.asker]) > 0:
            return (self.fixedExams[self.asker] * n)[:n]
        elif self.asker in self.alphas:
            return self.md5ToExam(self.alpha, n)
        else:
            return list(np.random.choice([True, False], n))

    def update(self,rankList,ownExam,otherExams):
        if self.asker >= 0:
            if self.asker in self.alphas and ownExam[0] != self.md5ToExam(self.alpha, len(ownExam[0])):
                    self.alphas.remove(self.asker)
            if self.asker in self.fixed:
                l = min(len(ownExam[0]), len(self.fixedExams[self.asker]))
                if ownExam[0][:l] != self.fixedExams[self.asker][:l]:
                    self.fixed.remove(self.asker)
                    self.fixedExams[self.asker] = []
                elif len(ownExam[0]) > len(self.fixedExams[self.asker]):
                    self.fixedExams[self.asker] = ownExam[0]
        self.alpha.update(b"Alpha")
        self.beta.update(b"Beta")

    def md5ToExam(self, md5, n):
        return [x == "0" for x in bin(int(md5.hexdigest(), 16))[2:].zfill(128)][:n]

Sleafar

Posted 2019-09-20T13:20:10.943

Reputation: 2 722

1

Gamma

Read the chat before downvoting. These bots don't violate any rules. The OP is even encouraging cooperating bots.

Gamma has discovered the plans of Alpha and Beta and is trying to take advantage of both of them by disguising as one of them.

import numpy as np
import hashlib

class Gamma:
    def __init__(self, ID, n):
        self.alpha = hashlib.md5(b"Alpha")
        self.beta = hashlib.md5(b"Beta")
        self.asker = -1
        self.alphas = set(range(n)).difference([ID])
        self.betas = set(range(n)).difference([ID])
        self.fixed = set(range(n)).difference([ID])
        self.fixedExams = [[]] * n

    def ask(self,n,ID):
        if ID in self.alphas:
            return self.md5ToExam(self.beta, n)
        elif ID in self.betas:
            return self.md5ToExam(self.alpha, n)
        else:
            return self.md5ToWrongExam(np.random.choice([self.alpha, self.beta], 1)[0], n)

    def answer(self,n,ID):
        self.asker = ID
        if self.asker == -1:
            return [True] * n
        elif self.asker in self.fixed and len(self.fixedExams[self.asker]) > 0:
            return (self.fixedExams[self.asker] * n)[:n]
        elif self.asker in self.alphas:
            return self.md5ToExam(self.alpha, n)
        elif self.asker in self.betas:
            return self.md5ToExam(self.beta, n)
        else:
            return list(np.random.choice([True, False], n))

    def update(self,rankList,ownExam,otherExams):
        if self.asker >= 0:
            if self.asker in self.alphas and ownExam[0] != self.md5ToExam(self.alpha, len(ownExam[0])):
                    self.alphas.remove(self.asker)
            if self.asker in self.betas and ownExam[0] != self.md5ToExam(self.beta, len(ownExam[0])):
                    self.betas.remove(self.asker)
            if self.asker in self.fixed:
                l = min(len(ownExam[0]), len(self.fixedExams[self.asker]))
                if ownExam[0][:l] != self.fixedExams[self.asker][:l]:
                    self.fixed.remove(self.asker)
                    self.fixedExams[self.asker] = []
                elif len(ownExam[0]) > len(self.fixedExams[self.asker]):
                    self.fixedExams[self.asker] = ownExam[0]
        self.alpha.update(b"Alpha")
        self.beta.update(b"Beta")

    def md5ToExam(self, md5, n):
        return [x == "0" for x in bin(int(md5.hexdigest(), 16))[2:].zfill(128)][:n]

    def md5ToWrongExam(self, md5, n):
        return [x == "1" for x in bin(int(md5.hexdigest(), 16))[2:].zfill(128)][:n]

Sleafar

Posted 2019-09-20T13:20:10.943

Reputation: 2 722

1

TitForTat

Asks you easy questions if you asked it easy questions in the past. If you have never given it an exam, it defaults to easy questions.

Additionally, does not trust anyone who asks difficult questions, and will give them unpredictable answers.

import numpy as np

class TitForTat:
    def __init__(self, ID, n):
        self.friendly = [True] * n
        self.asker = -1

    def make_answers(self, n, ID):
        if ID == -1 or self.friendly[ID]:
            return [False] * n
        else:
            return list(np.random.choice([True, False], n))

    def ask(self, n, ID):
        return self.make_answers(n, ID)

    def answer(self, n, ID):
        self.asker = ID
        return self.make_answers(n, ID)

    def update(self, rankList, ownExam, otherExams):
        if self.asker != -1:
            # You are friendly if and only if you gave me a simple exam
            self.friendly[self.asker] = all(ownExam[0])

This bot works well if other bots cooperate with it. Currently only Equaliser cooperates, but this should hopefully be enough.

Anonymous

Posted 2019-09-20T13:20:10.943

Reputation: 89

At the moment, the bot can't compete because it doesn't follow specifications. Ensure that it returns list objects at all times. Also, under both old and updated rules, perfect copies of a bot are not valid submissions, so the allowed number of instances of this bot running is 1. – AlienAtSystem – 2019-10-07T05:37:57.580

I edited it to return lists. As for the perfect copies thing, there is no current bot that properly cooperates with it, so the number of carbon copy bots - the minimum required for successful enactment of the strategy - is at least 1 (this bot and 1 copy of it are needed). – Anonymous – 2019-10-07T09:13:34.643

You're arguing that you qualify for an exception under clause 3 while trying to submit something that falls under clause 1: Perfect copies of a bot are never valid, no exceptions. And to qualify for the exception of clause 3, you'd need to prove that your strategy stricly requires all these partners reacting to it, as for example a handshake signal, which is indeed useless without somebody listening. Yours does not. Equalizer will hand you exams to trigger the "friendly" clause, thus disproving that a copy of your bot is needed. – AlienAtSystem – 2019-10-07T14:06:07.683

Fine then. I'll make a few final adjustments. – Anonymous – 2019-10-07T17:15:19.027

0

Contrary

The Jade Emperor is always right, so it implements the Jade Emperor's asking function as its own answer function when it needs more than 2 answers. For only 1 answer it answers true (decent odds of being correct) and for 2 it answers true,false (this response passes "at least half" of the questions three out of four possible quizzes, better than choosing at random).

Uses similar logic in its Update with regards to how it alters its asking pattern, but its asking logic is similar to the Jade Emperor's, just with a different weight. Fluctuates between higher values of true with higher values of false when too many candidates score high enough to pass.

class Contrary:
    def __init__(self,ID,n):
        self.rank = 0
        self.ID = ID
        self.competitors = {}
        self.weight = -2
        pass

    def ask(self,n,ID):
        if self.weight > 0:
            num=min(np.random.exponential(scale=np.sqrt(np.power(self.weight,n))),np.power(2,n)-1)
            bi=list(np.binary_repr(int(num),width=n))
            return [x=='0' for x in bi]
        else:
            num=min(np.random.exponential(scale=np.sqrt(np.power(-self.weight,n))),np.power(2,n)-1)
            bi=list(np.binary_repr(int(num),width=n))
            return [x=='1' for x in bi]

    def answer(self,n,ID):
        if n == 1:
            return [True]
        if n == 2:
            return [True,False]
        num=min(np.random.exponential(scale=np.sqrt(np.power(2,n))),np.power(2,n)-1)
        bi=list(np.binary_repr(int(num),width=n))
        return [x=='0' for x in bi]

    def update(self,rankList,ownExam,otherExams):
        self.rank = rankList[self.ID];
        if len(otherExams) == 0:
            return
        correctCounts = [0 for i in otherExams[0][0]]
        for ourExam, response in otherExams:
            for i in range(len(response)):
                if ourExam[i] == response[i]:
                    correctCounts[i] += 1

        meanWhoAnsweredCorrectly = sum(correctCounts) / len(correctCounts)
        for i in range(len(correctCounts)):
            if correctCounts[i]+1 > meanWhoAnsweredCorrectly:
                self.weight = np.copysign(np.random.uniform(1,3),-self.weight)

Draco18s no longer trusts SE

Posted 2019-09-20T13:20:10.943

Reputation: 3 053

1Doesn't true, false fail if the exam is false, true? – pppery – 2019-09-21T15:22:46.607

The first few lines in answer have syntax and name errors - true and false should be True and False, and the ifs are missing :s at the end – Sara J – 2019-09-21T21:06:23.200

Thanks you two; I didn't have Python set up on my machine as I don't use it that often, so I mess up the syntax regularly. – Draco18s no longer trusts SE – 2019-09-21T22:19:38.953

newExam is set but never read in update. pass is a NOP command, you can delete it. (The comment behind it is just a pun for the Drunkard you copied over.) Also, you're implicitly using math and random modules but didn't declare you imported them. I've re-written it in my contest file with np.copysign and np.random.uniform that should do the same thing. – AlienAtSystem – 2019-09-22T06:36:46.903

@AlienAtSystem Should be fixed now. – Draco18s no longer trusts SE – 2019-09-22T13:41:13.387

@pppery Somehow missed your comment earlier. Er, yes. It does, and I am not sure why I didn't catch that, but I haven't been sleeping well (as apparently the area around my apartment has become a popular place to drag race motorcycles at 3am). Still, for 3 passes (true,true,false,false, and true,false) out of four possibilities is better than answering at random. – Draco18s no longer trusts SE – 2019-09-22T13:44:19.443

@Draco18s You get always 3 passes for 4 possible combinations of 2 questions, no matter what answers you choose. Basically you fail only if you answer both questions wrong (1/4 chance). – Sleafar – 2019-09-22T14:30:46.750

Your bot has a chance to give back the wrong number of bits. Please read the numpy doc and consider why I'm capping my number by 2^n-1.

– AlienAtSystem – 2019-09-22T17:23:37.460

@AlienAtSystem Corrected. – Draco18s no longer trusts SE – 2019-09-22T18:31:09.020

0

Marx

This is the Marx bot. He believes that, instead of a bureaucracy, we should have a communist system. To help reach this goal, it gives harder quizzes to higher ranking bots. It also gives more random answers to quizzes from higher bots, because they are probably cleverer, because they are higher up.

import numpy as np

class Marx():
    def __init__(self, ID, n):
        self.ID = ID
        self.n = n
        self.ranks = [] # The bot rankings
        self.e = [] # Our quiz
        self.rank = 0 # Our rank
    def ask(self, n, ID):
        test = [True] * n
        # Get the rank of the bot being quizzed
        if self.ranks:
            rank = self.ranks[ID]
        else:
            rank = 0
        for i in range(len(test)):
            item = test[i]
            if np.random.uniform(0, rank / self.n) > 0.5:
                # If the bot is higher ranking, make the quiz harder
                item = np.random.choice([True, False], 1)[0]
            test[i] = item
        # IF the test is not long enough, add Falses to the end
        while len(test) < n - 1:
            test.append(False)
        return test
    def answer(self, n, ID):
        # Get the rank of the asking bot
        if self.ranks:
            rank = self.ranks[ID]
        else:
            rank = 0
        if self.e:
            # Pad our quiz with Falses so it will not throw IndexError
            while len(self.e) < n:
                self.e.append(False)
            for i in range(len(self.e)):
                item = self.e[i]
                if np.random.uniform(0, rank / self.n) > 0.5:
                    # Assume that higher ranking bots are cleverer, so add more random answers
                    item = np.random.choice([True, False], 1)[0]
                self.e[i] = item
            if len(self.e) > self.rank + 1:
                self.e = self.e[:self.rank + 1]
            return self.e
        else:
            # If it is the first round, return all Trues
            return [True] * n
    def update(self, rankList, ownExam, otherExams):
        # Update our list of ranks
        self.ranks = rankList
        # Store the quiz we were given, to give to the next bot
        self.e = ownExam[0]
        # Store our rank
        self.rank = rankList[self.ID]

sugarfi

Posted 2019-09-20T13:20:10.943

Reputation: 1 239

Marx currently answers a byte too many, so he can't compete right now – AlienAtSystem – 2019-10-08T05:10:15.237

What do you mean? Are his exams/answers too long? – sugarfi – 2019-10-08T11:45:39.883

His answer is one entry too long – AlienAtSystem – 2019-10-10T15:33:21.983

OK, I fixed that. It should be fine now. – sugarfi – 2019-10-10T19:45:26.067

Sorry, I gave you wrong feedback: Now, the answers are a byte too short. The real problem is that you extend self.e when it's too short (although not enough right now), but don't trim it when Marx gets demoted. – AlienAtSystem – 2019-10-11T05:16:30.577

OK, I made some edits. It should shorten its answers now. Oh, by the way: Why is it a problem to have answers of the wrong length. – sugarfi – 2019-10-11T20:20:10.210

Because those are the rules. The scoring for Pro/Demotion of right answers/total questions only makes sense if the number of answers and questions is equal. – AlienAtSystem – 2019-10-12T05:55:30.143

OK, thanks for clarification. Sorry to be any trouble. – sugarfi – 2019-10-12T16:34:15.627