The Dobble Algorithm

I played a few games of Dobble with my niece and got curious about the algorithm used to generate the cards. It turns out it’s not trivial and one of the best ways to interrogate it is as a finite projective plane.

I got the most useful information from www.101computing.net/the-dobble-algorithm which has actual code although the clearest explanation was on www.petercollingridge.co.uk/blog/mathematics-toys-and-games/dobble

The maximum number of possible cards is n^2 + n + 1 where n is the number of symbols per card minus one.

Number of Symbols on CardMax Number of Cards
23
413
521
857
12133

The number of symbols on the card doesn’t have to be a prime number plus one but the algorithms found online require that.

I thought it was pretty interesting that 8 symbols per card allows 57 cards but the game itself comes with 55 cards. Presumably because once you put one card in the center 54 is divisible by both 2 and 3 for even number of cards per player in the common cases. I wonder which two cards are missing but not enough to look through them all!

Emit logs in JSON format from fastapi/gunicorn, for DataDog

import logging

from pythonjsonlogger import jsonlogger

def init() -> None:
    log_in_json()
    map_levelname_to_status()

# Force all loggers to talk JSON rather than text so DataDog can parse the output.
def log_in_json() -> None:
    loggers = [
        logging.getLogger("uvicorn.access"),
        logging.getLogger("uvicorn.error"),
        logging.getLogger("uvicorn"),
        logging.getLogger(),
    ]
    for logger in loggers:
        for handler in logger.handlers:
            logger.removeHandler(handler)
        logger.level = logging.DEBUG
        log_handler = logging.StreamHandler()
        formatter = jsonlogger.JsonFormatter(
            "%(asctime)s %(levelname)s %(name)s %(message)s"
        )
        log_handler.setFormatter(formatter)
        logger.addHandler(log_handler)

# DataDog is expecting 'status' for log level but the python default is 'levelname'.
def map_levelname_to_status() -> None:
    old_factory = logging.getLogRecordFactory()

    def record_factory(*args: str, **kwargs: str) -> logging.LogRecord:
        record = old_factory(*args, **kwargs)
        record.status = record.levelname  # type: ignore
        return record

    logging.setLogRecordFactory(record_factory)