<jupyter_start><jupyter_text>player: AI<jupyter_code>class Player:

    def __init__(self, heap):
        self.history = {}
        self.distribution = np.ones((heap + 1, 3), dtype=int)
        self.cutoff = 1000

    def __call__(self, heap):
        # randomize move based on previous games
        dist = self.distribution[heap].cumsum()
        rnd = np.random.randint(dist[2])
        move = 1 if rnd < dist[0] else 2 if rnd < dist[1] else 3
        
        # store move in history
        self.history[heap] = min(heap, move)
        
        return self.history[heap]

    def learn(self, winner):
        # update move distribution
        for heap, move in self.history.items():
            if winner is self:
                self.distribution[heap][move - 1] += 1
            else:
                self.distribution[heap][move - 1] -= 1
                self.distribution[heap] += 1

        # normalize distribution to speed learning up
        normalize = np.argwhere(self.distribution.sum(axis=1) > self.cutoff)
        for heap in normalize:
            self.distribution[heap] -= self.distribution[heap].min() - 1

        # reset game history
        self.history = {}
    
    def strategy(self):
        distribution = self.distribution[1:]
        return distribution.T / distribution.sum(axis=1)<jupyter_output><empty_output><jupyter_text>opponents<jupyter_code>def expert_opponent(heap):
    return heap % 4 or min(heap, np.random.randint(1, 4))
def random_opponent(heap):
    return min(heap, np.random.randint(1, 4))
def take_n_opponent(take):
    return lambda heap: min(heap, take)<jupyter_output><empty_output><jupyter_text>training<jupyter_code>def play(heap, player, opponent):
    players = player, opponent
    wins = 0

    for game in range(100001):
        # update plot periodically
        if game % 10000 == 0:
            print(game, 'games, W/L ratio', wins / 10000)
            wins = 0

        # a single game
        h = heap
        while h:
            h -= players[0](h)
            players = players[1], players[0]

        winner = players[1]
        wins += winner is player
            
        # let player learn
        player.learn(winner)
        
    # plot distribution
    plot_strategy(heap, player)
def plot_strategy(heap, player):
    output_notebook()

    # data
    take_1, take_2, take_3 = player.strategy()
    take_2 += take_1
    take_3 += take_2
    kwargs = {'x': range(1, heap + 1), 'width': .8}

    # plot
    plot = figure(plot_width=600, plot_height=400)
    plot.vbar(**kwargs, bottom=0, top=take_1, legend='take 1', color='#a44444')
    plot.vbar(**kwargs, bottom=take_1, top=take_2, legend='take 2', color='#88a888')
    plot.vbar(**kwargs, bottom=take_2, top=take_3, legend='take 3', color='#ccccac')
    show(plot)<jupyter_output><empty_output><jupyter_text>learning<jupyter_code>HEAP = 21
play(HEAP, Player(HEAP), expert_opponent)
play(HEAP, Player(HEAP), random_opponent)
play(HEAP, Player(HEAP), take_n_opponent(1))
play(HEAP, Player(HEAP), take_n_opponent(3))<jupyter_output>0 games, W/L ratio 0.0
10000 games, W/L ratio 0.9706
20000 games, W/L ratio 0.9971
30000 games, W/L ratio 0.9969
40000 games, W/L ratio 0.9989
50000 games, W/L ratio 0.9997
60000 games, W/L ratio 0.9998
70000 games, W/L ratio 1.0
80000 games, W/L ratio 1.0
90000 games, W/L ratio 0.9999
100000 games, W/L ratio 1.0
{'fraction_non_alphanumeric': 0.12192227825571048, 'fraction_numerical': 0.054286561851082764, 'mean_word_length': 2.7014270032930847, 'pattern_counts': {'":': 0, '<': 18, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '584562', 'license': 'MIT', 'n_tokens_mistral': 1254, 'n_tokens_neox': 1115, 'n_words': 283, 'path': '100days/day 90 - simple nim - AI.ipynb', 'repo_name': 'gopala-kr/ds-notebooks', 'size': '3371'}