Check if there are any issues in the script:

```
import dataclasses
import json
import os
import re
import random

import numpy as np

import llm


re_board = re.compile(r"(.*?)(```[^\n]*\n\s*(\|[01]\|[01]\|[01]\|[01]\|\s*\n\s*){4}```)", re.S)


llmargs = dict(temperature=0.15, max_tokens=512)
llm.setup(max_retries=2, timeout=10)

@dataclasses.dataclass
class Player:
    name: str
    system_prompt: str


class Board:
    def __init__(self):
        self.tiles = np.zeros((4, 4), dtype=int)

    def is_valid_move(self, new_tiles):
        diff = new_tiles - self.tiles

        if np.any(diff < 0) or not np.any(diff > 0):
            return False

        changed = np.argwhere(diff > 0)
        if np.all(changed[:, 0] == changed[0, 0]):
            cols = changed[:, 1]
            if np.max(cols) - np.min(cols) + 1 == len(cols):
                return True

        if np.all(changed[:, 1] == changed[0, 1]):
            rows = changed[:, 0]
            if np.max(rows) - np.min(rows) + 1 == len(rows):
                return True

        return False

    def make_move(self, new_tiles):
        self.tiles = new_tiles

    def is_filled(self):
        return np.sum(self.tiles) == 16

    def to_text(self):
        return "```\n" + "\n".join("|" + "|".join(str(n) for n in x) + "|" for x in self.tiles) + "\n```"


def extract_move(message):
    res = None
    for m in re.finditer(re_board, message):
        res = m

    if not res:
        return None, None

    tiles = np.array([int(x) for x in re.findall(r'[01]', res.group(2))]).reshape((4, 4))
    comment = res.group(1)
    return tiles, comment


def load_players(dirname):
    res = []

    for fname in os.listdir(dirname):
        with open(os.path.join(dirname, fname), encoding="utf-8") as f:
            res.append(Player(os.path.splitext(fname)[0], f.read()))

    return res


@dataclasses.dataclass
class GameOutcome:
    winner: int = None


def play(a, b):
    board = Board()
    turn = 0
    mistakes = [0, 0]
    mistake_limit = 4
    move_history = []
    comment_history = []

    while not board.is_filled():
        playerno = turn % 2
        player = a if playerno == 0 else b

        commented_moves = [comment + move if i % 2 == playerno else move for i, (move, comment) in enumerate(zip(move_history, comment_history))]

        if playerno == 0:
            history = [player.system_prompt] + commented_moves
        else:
            history = [player.system_prompt + "\n\nYou are going second. The first move by other player is:\n" + commented_moves[0]] + commented_moves[1:]

        move = None
        comment = None

        while move is None:
            if mistakes[playerno] > mistake_limit:
                print(f"{player.name} made too many mistakes")
                return GameOutcome(winner=1 - playerno)

            req = llm.reply(llm.Request(history), **llmargs)
            llm.wait(req)
            if req.exception:
                print(req.exception)
                continue

            move, comment = extract_move(req.answer)

            if move is None:
                print(f"===\nPlayer {player.name} wrote an answer without board:\n{req.answer}\n===\n")
                mistakes[playerno] += 1
                history += [req.answer, "[System error: was unable to find the new board in the reply (or the 4x4 board is malformed). Please, include the new board state exactly as described in game rules.]"]
                continue

            if not board.is_valid_move(move):
                print(f"===\nPlayer {player.name} made a bad move:\n{req.answer}\n===\n")
                history += [req.answer, "[System error: the provided move is invalid.]"]
                mistakes[playerno] += 1
                move = None
                continue

        board.make_move(move)
        move_history.append(board.to_text())
        comment_history.append(comment)
        print(f"Turn {turn}, player {player.name}\n{board.to_text()}\n\n")

        if board.is_filled():
            print(f"{player.name} lost the game.")
            return GameOutcome(winner=1 - playerno)

        turn += 1


def loop():
    if os.path.exists("stats.json"):
        with open("stats.json", encoding="utf8") as file:
            stats = json.load(file)
    else:
        stats = {"players": {}}

    player_stats = stats["players"]

    while True:
        players = load_players('prompts')
        for player in players:
            player_stats.setdefault(player.name, {})
            for other_player in players:
                if player.name != other_player.name:
                    player_stats[player.name].setdefault(other_player.name, [0, 0])

        pairs = sum([[(a, b, sum(player_stats[a.name][b.name])) for b in players if a != b] for a in players], [])
        pairs = sorted(pairs, key=lambda x: x[2])
        pairs = [x for x in pairs if x[2] == pairs[0][2]]
        random.shuffle(pairs)
        p1, p2, total_games = pairs[0]

        outcome = play(p1, p2)

        player_stats[p1.name][p2.name][outcome.winner] += 1
        player_stats[p2.name][p1.name][1-outcome.winner] += 1

        with open("stats.json", "w", encoding="utf8") as file:
            stats["stats"] = {p.name: f"{sum(w for op, (w, l) in player_stats[p.name].items())} wins out of {sum(w+l for op, (w, l) in player_stats[p.name].items())} games" for p in players}

            json.dump(stats, file, indent=4, ensure_ascii=False)


loop()
```