Files
matrix/wordle.py
Jared Vititoe d0fd260336 Auto-share Wordle result to origin room on game end
Instead of posting "Check your DMs" when !wordle is used in a public
room, the bot now silently routes the game to DMs and automatically
posts the spoiler-free emoji share grid back to the origin room (e.g.
Commands) when the game ends — win, lose, or give up.

Also removed the "use !wordle share" prompt from win/loss messages
since sharing now happens automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 14:23:57 -05:00

792 lines
26 KiB
Python

"""Wordle game for Matrix bot.
Full implementation with daily puzzles, statistics tracking,
hard mode, shareable results, and rich HTML rendering.
"""
import json
import logging
import time
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path
from nio import AsyncClient
from utils import send_text, send_html, get_or_create_dm
from config import BOT_PREFIX
from wordlist_answers import ANSWERS
from wordlist_valid import VALID_GUESSES
logger = logging.getLogger("matrixbot")
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
_WORDLE_EPOCH = date(2021, 6, 19)
# Build lookup sets at import time
_ANSWER_LIST = [w.upper() for w in ANSWERS]
_VALID_SET = frozenset(w.upper() for w in VALID_GUESSES) | frozenset(_ANSWER_LIST)
# Tile colors (Wordle official palette)
_TILE = {
2: {"bg": "#538d4e", "label": "correct"}, # Green
1: {"bg": "#b59f3b", "label": "present"}, # Yellow
0: {"bg": "#3a3a3c", "label": "absent"}, # Gray
}
_EMPTY_BG = "#121213"
_EMPTY_BORDER = "#3a3a3c"
# Emoji squares for plain-text fallback & share
_EMOJI = {2: "\U0001f7e9", 1: "\U0001f7e8", 0: "\u2b1b"}
# Keyboard layout
_KB_ROWS = ["QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"]
# Stats file
STATS_FILE = Path("wordle_stats.json")
# Congratulations messages by guess number
_CONGRATS = {
1: "Genius!",
2: "Magnificent!",
3: "Impressive!",
4: "Splendid!",
5: "Great!",
6: "Phew!",
}
# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------
@dataclass
class WordleGame:
player_id: str
room_id: str
target: str
guesses: list = field(default_factory=list)
results: list = field(default_factory=list)
hard_mode: bool = False
daily_number: int = 0
started_at: float = field(default_factory=time.time)
finished: bool = False
won: bool = False
origin_room_id: str = "" # Public room where game was started (for share)
# Module-level state
_active_games: dict[str, WordleGame] = {}
_all_stats: dict[str, dict] = {}
# ---------------------------------------------------------------------------
# Stats persistence
# ---------------------------------------------------------------------------
def _load_stats():
global _all_stats
if STATS_FILE.exists():
try:
_all_stats = json.loads(STATS_FILE.read_text())
except (json.JSONDecodeError, OSError) as e:
logger.error("Failed to load wordle stats: %s", e)
_all_stats = {}
else:
_all_stats = {}
def _save_stats():
try:
tmp = STATS_FILE.with_suffix(".tmp")
tmp.write_text(json.dumps(_all_stats, indent=2))
tmp.rename(STATS_FILE)
except OSError as e:
logger.error("Failed to save wordle stats: %s", e)
def _get_player_stats(player_id: str) -> dict:
if player_id not in _all_stats:
_all_stats[player_id] = {
"games_played": 0,
"games_won": 0,
"current_streak": 0,
"max_streak": 0,
"guess_distribution": {str(i): 0 for i in range(1, 7)},
"last_daily": -1,
"hard_mode": False,
"last_daily_result": None,
"last_daily_guesses": None,
}
return _all_stats[player_id]
def _record_game_result(player_id: str, game: WordleGame):
stats = _get_player_stats(player_id)
stats["games_played"] += 1
if game.won:
stats["games_won"] += 1
stats["current_streak"] += 1
stats["max_streak"] = max(stats["max_streak"], stats["current_streak"])
num_guesses = str(len(game.guesses))
stats["guess_distribution"][num_guesses] = (
stats["guess_distribution"].get(num_guesses, 0) + 1
)
else:
stats["current_streak"] = 0
stats["last_daily"] = game.daily_number
stats["last_daily_result"] = game.results
stats["last_daily_guesses"] = game.guesses
stats["last_daily_won"] = game.won
stats["last_daily_hard"] = game.hard_mode
stats["last_origin_room"] = game.origin_room_id
_save_stats()
# ---------------------------------------------------------------------------
# Core algorithms
# ---------------------------------------------------------------------------
def get_daily_word() -> tuple[str, int]:
"""Return (word, puzzle_number) for today's daily puzzle."""
today = date.today()
puzzle_number = (today - _WORDLE_EPOCH).days
word = _ANSWER_LIST[puzzle_number % len(_ANSWER_LIST)]
return word, puzzle_number
def evaluate_guess(guess: str, target: str) -> list[int]:
"""Evaluate a guess against the target. Returns list of 5 scores:
2 = correct position (green), 1 = wrong position (yellow), 0 = absent (gray).
Handles duplicate letters correctly with a two-pass approach.
"""
result = [0] * 5
target_remaining = list(target)
# Pass 1: mark exact matches (green)
for i in range(5):
if guess[i] == target[i]:
result[i] = 2
target_remaining[i] = None
# Pass 2: mark present-but-wrong-position (yellow)
for i in range(5):
if result[i] == 2:
continue
if guess[i] in target_remaining:
result[i] = 1
target_remaining[target_remaining.index(guess[i])] = None
return result
def validate_hard_mode(
guess: str,
previous_guesses: list[str],
previous_results: list[list[int]],
) -> str | None:
"""Return None if valid, or an error message if hard mode violated."""
for prev_guess, prev_result in zip(previous_guesses, previous_results):
for i, (letter, score) in enumerate(zip(prev_guess, prev_result)):
if score == 2 and guess[i] != letter:
return (
f"Hard mode: position {i + 1} must be "
f"'{letter}' (green from previous guess)"
)
if score == 1 and letter not in guess:
return (
f"Hard mode: guess must contain "
f"'{letter}' (yellow from previous guess)"
)
return None
# ---------------------------------------------------------------------------
# HTML rendering
# ---------------------------------------------------------------------------
def _tile_span(letter: str, bg: str) -> str:
"""Render a single letter tile using Matrix-compatible attributes."""
return (
f'<font data-mx-bg-color="{bg}" data-mx-color="#ffffff">'
f"<b>\u00a0{letter}\u00a0</b></font>"
)
def render_grid_html(game: WordleGame) -> str:
"""Render the Wordle grid as inline spans (compatible with Cinny)."""
rows = []
for row_idx in range(6):
tiles = []
if row_idx < len(game.guesses):
guess = game.guesses[row_idx]
result = game.results[row_idx]
for letter, score in zip(guess, result):
bg = _TILE[score]["bg"]
tiles.append(_tile_span(letter, bg))
else:
for _ in range(5):
tiles.append(_tile_span("\u00a0", _EMPTY_BG))
rows.append("".join(tiles))
return "<br>".join(rows)
def render_keyboard_html(game: WordleGame) -> str:
"""Render a virtual keyboard showing letter states."""
letter_states: dict[str, int] = {}
for guess, result in zip(game.guesses, game.results):
for letter, score in zip(guess, result):
letter_states[letter] = max(letter_states.get(letter, -1), score)
kb_rows = []
for row in _KB_ROWS:
keys = []
for letter in row:
state = letter_states.get(letter, -1)
if state == -1:
bg, color = "#818384", "#ffffff"
elif state == 0:
bg, color = "#3a3a3c", "#555555"
elif state == 1:
bg, color = "#b59f3b", "#ffffff"
else:
bg, color = "#538d4e", "#ffffff"
keys.append(
f'<font data-mx-bg-color="{bg}" data-mx-color="{color}">'
f"{letter}</font>"
)
kb_rows.append(" ".join(keys))
return "<br>" + "<br>".join(kb_rows)
def render_grid_plain(game: WordleGame) -> str:
"""Plain text grid with emoji squares and letter markers."""
_marker = {2: "!", 1: "?", 0: "."} # ! = correct, ? = wrong spot, . = absent
lines = []
for guess, result in zip(game.guesses, game.results):
emoji_row = "".join(_EMOJI[s] for s in result)
# Show each letter with a marker: [C!] = correct, [R?] = wrong spot, [A.] = absent
marked = " ".join(f"{letter}{_marker[score]}" for letter, score in zip(guess, result))
lines.append(f"{emoji_row} {marked}")
return "\n".join(lines)
def render_keyboard_plain(game: WordleGame) -> str:
"""Plain text keyboard status."""
letter_states: dict[str, int] = {}
for guess, result in zip(game.guesses, game.results):
for letter, score in zip(guess, result):
letter_states[letter] = max(letter_states.get(letter, -1), score)
lines = []
symbols = {-1: " ", 0: "\u2717", 1: "?", 2: "\u2713"}
for row in _KB_ROWS:
chars = []
for letter in row:
state = letter_states.get(letter, -1)
if state == 0:
chars.append("\u00b7") # dimmed
elif state >= 1:
chars.append(letter)
else:
chars.append(letter.lower())
lines.append(" ".join(chars))
return "\n".join(lines)
def render_stats_html(stats: dict) -> str:
"""Render player statistics as HTML (Matrix-compatible)."""
played = stats["games_played"]
won = stats["games_won"]
win_pct = (won / max(played, 1)) * 100
streak = stats["current_streak"]
max_streak = stats["max_streak"]
dist = stats["guess_distribution"]
max_count = max((int(v) for v in dist.values()), default=1) or 1
html = "<strong>Wordle Statistics</strong><br><br>"
html += (
f"<b>{played}</b> Played | "
f"<b>{win_pct:.0f}%</b> Win | "
f"<b>{streak}</b> Streak | "
f"<b>{max_streak}</b> Best<br><br>"
)
html += "<strong>Guess Distribution</strong><br>"
for i in range(1, 7):
count = int(dist.get(str(i), 0))
is_max = count == max_count and count > 0
bar_len = max(int((count / max_count) * 10), 1) if max_count > 0 else 1
bar = "\u2588" * bar_len # Block character for bar
if is_max and count > 0:
html += f'{i} <font data-mx-bg-color="#538d4e" data-mx-color="#ffffff"> {bar} {count} </font><br>'
else:
html += f'{i} <font data-mx-bg-color="#3a3a3c" data-mx-color="#ffffff"> {bar} {count} </font><br>'
return html
def render_stats_plain(stats: dict) -> str:
"""Plain text stats."""
played = stats["games_played"]
won = stats["games_won"]
win_pct = (won / max(played, 1)) * 100
streak = stats["current_streak"]
max_streak = stats["max_streak"]
dist = stats["guess_distribution"]
max_count = max((int(v) for v in dist.values()), default=1) or 1
lines = [
"Wordle Statistics",
f"Played: {played} | Win: {win_pct:.0f}% | Streak: {streak} | Max: {max_streak}",
"",
"Guess Distribution:",
]
for i in range(1, 7):
count = int(dist.get(str(i), 0))
bar_len = max(round((count / max_count) * 16), 1) if count > 0 else 0
bar = "\u2588" * bar_len
lines.append(f" {i}: {bar} {count}")
return "\n".join(lines)
def generate_share(stats: dict) -> str:
"""Generate the shareable emoji grid from last completed daily."""
results = stats.get("last_daily_result")
if not results:
return ""
won = stats.get("last_daily_won", False)
hard = stats.get("last_daily_hard", False)
daily_num = stats.get("last_daily", 0)
score = str(len(results)) if won else "X"
mode = "*" if hard else ""
header = f"Wordle {daily_num} {score}/6{mode}\n\n"
rows = []
for result in results:
rows.append("".join(_EMOJI[s] for s in result))
return header + "\n".join(rows)
# ---------------------------------------------------------------------------
# Subcommand handlers
# ---------------------------------------------------------------------------
async def wordle_help(client: AsyncClient, room_id: str):
"""Show help text with rules and commands."""
p = BOT_PREFIX
plain = (
f"Wordle - Guess the 5-letter word in 6 tries!\n\n"
f"Commands:\n"
f" {p}wordle Start today's daily puzzle (or show current game)\n"
f" {p}wordle <word> Submit a 5-letter guess\n"
f" {p}wordle stats View your statistics\n"
f" {p}wordle hard Toggle hard mode\n"
f" {p}wordle share Share your last daily result\n"
f" {p}wordle give up Forfeit current game\n"
f" {p}wordle help Show this help\n\n"
f"Rules:\n"
f" - Each guess must be a valid 5-letter English word\n"
f" - Green = correct letter, correct position\n"
f" - Yellow = correct letter, wrong position\n"
f" - Gray = letter not in the word\n"
f" - Hard mode: must use all revealed hints in subsequent guesses\n"
f" - Everyone gets the same daily word!"
)
html = (
"<h4>Wordle</h4>"
"<p>Guess the 5-letter word in 6 tries!</p>"
"<strong>Commands:</strong>"
"<ul>"
f"<li><code>{p}wordle</code> — Start today's daily puzzle</li>"
f"<li><code>{p}wordle &lt;word&gt;</code> — Submit a guess</li>"
f"<li><code>{p}wordle stats</code> — View your statistics</li>"
f"<li><code>{p}wordle hard</code> — Toggle hard mode</li>"
f"<li><code>{p}wordle share</code> — Share your last result</li>"
f"<li><code>{p}wordle give up</code> — Forfeit current game</li>"
"</ul>"
"<strong>How to play:</strong>"
"<ul>"
'<li><font data-mx-bg-color="#538d4e" data-mx-color="#ffffff"><b> G </b></font> '
"Green = correct letter, correct position</li>"
'<li><font data-mx-bg-color="#b59f3b" data-mx-color="#ffffff"><b> Y </b></font> '
"Yellow = correct letter, wrong position</li>"
'<li><font data-mx-bg-color="#3a3a3c" data-mx-color="#ffffff"><b> X </b></font> '
"Gray = letter not in the word</li>"
"</ul>"
"<p><em>Hard mode:</em> You must use all revealed hints in subsequent guesses.</p>"
"<p>Everyone gets the same daily word!</p>"
)
await send_html(client, room_id, plain, html)
async def wordle_start_or_status(client: AsyncClient, room_id: str, sender: str, origin_room_id: str = ""):
"""Start a new daily game or show current game status."""
# Check for active game
if sender in _active_games:
game = _active_games[sender]
if not game.finished:
guesses_left = 6 - len(game.guesses)
grid_plain = render_grid_plain(game)
kb_plain = render_keyboard_plain(game)
plain = (
f"Wordle {game.daily_number}"
f"Guess {len(game.guesses) + 1}/6\n\n"
f"{grid_plain}\n\n{kb_plain}"
)
grid_html = render_grid_html(game)
kb_html = render_keyboard_html(game)
mode = " (Hard Mode)" if game.hard_mode else ""
html = (
f''
f"<strong>Wordle {game.daily_number}{mode}</strong> — "
f"Guess {len(game.guesses) + 1}/6<br><br>"
f"{grid_html}{kb_html}"
)
await send_html(client, room_id, plain, html)
return
# Check if already completed today's puzzle
word, puzzle_number = get_daily_word()
stats = _get_player_stats(sender)
if stats["last_daily"] == puzzle_number:
await send_text(
client, room_id,
f"You already completed today's Wordle (#{puzzle_number})! "
f"Use {BOT_PREFIX}wordle stats to see your results "
f"or {BOT_PREFIX}wordle share to share them."
)
return
# Start new game
hard_mode = stats.get("hard_mode", False)
game = WordleGame(
player_id=sender,
room_id=room_id,
target=word,
hard_mode=hard_mode,
daily_number=puzzle_number,
origin_room_id=origin_room_id or room_id,
)
_active_games[sender] = game
mode_str = " (Hard Mode)" if hard_mode else ""
grid_html = render_grid_html(game)
kb_html = render_keyboard_html(game)
plain = (
f"Wordle #{puzzle_number}{mode_str}\n"
f"Guess a 5-letter word! You have 6 attempts.\n"
f"Type {BOT_PREFIX}wordle <word> to guess."
)
html = (
f''
f"<strong>Wordle #{puzzle_number}{mode_str}</strong><br>"
f"Guess a 5-letter word! You have 6 attempts.<br>"
f"Type <code>{BOT_PREFIX}wordle &lt;word&gt;</code> to guess."
f"<br><br>{grid_html}{kb_html}"
)
await send_html(client, room_id, plain, html)
async def wordle_guess(
client: AsyncClient, room_id: str, sender: str, guess: str
):
"""Process a guess."""
if sender not in _active_games:
await send_text(
client, room_id,
f"No active game. Start one with {BOT_PREFIX}wordle"
)
return
game = _active_games[sender]
if game.finished:
await send_text(
client, room_id,
f"Your game is already finished! "
f"Use {BOT_PREFIX}wordle to start a new daily puzzle."
)
return
# Validate word
if guess not in _VALID_SET:
await send_text(client, room_id, f"'{guess.lower()}' is not in the word list. Try again.")
return
# Hard mode validation
if game.hard_mode and game.guesses:
violation = validate_hard_mode(guess, game.guesses, game.results)
if violation:
await send_text(client, room_id, violation)
return
# Evaluate
result = evaluate_guess(guess, game.target)
game.guesses.append(guess)
game.results.append(result)
# Check win
if all(s == 2 for s in result):
game.finished = True
game.won = True
origin = game.origin_room_id
_record_game_result(sender, game)
del _active_games[sender]
num = len(game.guesses)
congrats = _CONGRATS.get(num, "Nice!")
grid_plain = render_grid_plain(game)
plain = (
f"{congrats} Wordle {game.daily_number} {num}/6"
f"{'*' if game.hard_mode else ''}\n\n"
f"{grid_plain}"
)
grid_html = render_grid_html(game)
mode = "*" if game.hard_mode else ""
html = (
f''
f"<strong>{congrats}</strong> "
f"Wordle {game.daily_number} {num}/6{mode}<br><br>"
f"{grid_html}"
)
await send_html(client, room_id, plain, html)
if origin and origin != room_id:
await wordle_share(client, origin, sender)
return
# Check loss (6 guesses used)
if len(game.guesses) >= 6:
game.finished = True
game.won = False
origin = game.origin_room_id
_record_game_result(sender, game)
del _active_games[sender]
grid_plain = render_grid_plain(game)
plain = (
f"Wordle {game.daily_number} X/6"
f"{'*' if game.hard_mode else ''}\n\n"
f"{grid_plain}\n\n"
f"The word was: {game.target}\n"
f"Better luck tomorrow!"
)
grid_html = render_grid_html(game)
mode = "*" if game.hard_mode else ""
html = (
f''
f"<strong>Wordle {game.daily_number} X/6{mode}</strong><br><br>"
f"{grid_html}<br>"
f'The word was: <font data-mx-color="#538d4e"><strong>'
f"{game.target}</strong></font><br>"
f"<em>Better luck tomorrow!</em>"
)
await send_html(client, room_id, plain, html)
if origin and origin != room_id:
await wordle_share(client, origin, sender)
return
# Still playing — show grid + keyboard
guesses_left = 6 - len(game.guesses)
grid_plain = render_grid_plain(game)
kb_plain = render_keyboard_plain(game)
plain = (
f"Wordle {game.daily_number}"
f"Guess {len(game.guesses) + 1}/6\n\n"
f"{grid_plain}\n\n{kb_plain}"
)
grid_html = render_grid_html(game)
kb_html = render_keyboard_html(game)
mode = " (Hard Mode)" if game.hard_mode else ""
html = (
f''
f"<strong>Wordle {game.daily_number}{mode}</strong> — "
f"Guess {len(game.guesses) + 1}/6<br><br>"
f"{grid_html}{kb_html}"
)
await send_html(client, room_id, plain, html)
async def wordle_stats(client: AsyncClient, room_id: str, sender: str):
"""Show player statistics."""
stats = _get_player_stats(sender)
if stats["games_played"] == 0:
await send_text(
client, room_id,
f"No Wordle stats yet! Start a game with {BOT_PREFIX}wordle"
)
return
plain = render_stats_plain(stats)
html = render_stats_html(stats)
await send_html(client, room_id, plain, html)
async def wordle_toggle_hard(client: AsyncClient, room_id: str, sender: str):
"""Toggle hard mode for the player."""
stats = _get_player_stats(sender)
new_mode = not stats.get("hard_mode", False)
stats["hard_mode"] = new_mode
_save_stats()
# Also update active game if one exists
if sender in _active_games:
game = _active_games[sender]
if not game.guesses:
# Only allow toggling before first guess
game.hard_mode = new_mode
elif new_mode:
await send_text(
client, room_id,
"Hard mode enabled for future games. "
"Cannot enable mid-game after guessing."
)
return
else:
game.hard_mode = False
status = "enabled" if new_mode else "disabled"
plain = (
f"Hard mode {status}. "
+ ("You must use all revealed hints in subsequent guesses."
if new_mode else "Standard rules apply.")
)
await send_text(client, room_id, plain)
async def wordle_share(client: AsyncClient, room_id: str, sender: str):
"""Share the last completed daily result."""
stats = _get_player_stats(sender)
share_text = generate_share(stats)
if not share_text:
await send_text(
client, room_id,
f"No completed daily puzzle to share. Play one with {BOT_PREFIX}wordle"
)
return
await send_text(client, room_id, share_text)
async def wordle_give_up(client: AsyncClient, room_id: str, sender: str):
"""Forfeit the current game."""
if sender not in _active_games:
await send_text(
client, room_id,
f"No active game to give up. Start one with {BOT_PREFIX}wordle"
)
return
game = _active_games[sender]
if game.finished:
del _active_games[sender]
return
game.finished = True
game.won = False
origin = game.origin_room_id
_record_game_result(sender, game)
del _active_games[sender]
grid_plain = render_grid_plain(game)
plain = (
f"Game over! The word was: {game.target}\n\n"
f"{grid_plain}\n\n"
f"Better luck tomorrow!"
)
grid_html = render_grid_html(game)
html = (
f''
f"<strong>Game Over</strong><br><br>"
f"{grid_html}<br>"
f'The word was: <font data-mx-color="#538d4e"><strong>'
f"{game.target}</strong></font><br>"
f"<em>Better luck tomorrow!</em>"
)
await send_html(client, room_id, plain, html)
if origin and origin != room_id:
await wordle_share(client, origin, sender)
# ---------------------------------------------------------------------------
# Main router
# ---------------------------------------------------------------------------
async def _get_dm_room(client: AsyncClient, room_id: str, sender: str) -> tuple[str, str]:
"""Get or create DM room for the sender. Returns (dm_room_id, origin_room_id).
If already in a DM, returns (room_id, stored_origin or room_id).
If in a public room, creates/finds DM and returns (dm_room_id, room_id).
"""
# Check if this is already a DM (2 members)
room = client.rooms.get(room_id)
if room and room.member_count == 2:
# Already in DM — use origin from active game if available
game = _active_games.get(sender)
origin = game.origin_room_id if game and game.origin_room_id else room_id
return room_id, origin
# Public room — find/create DM
dm_room = await get_or_create_dm(client, sender)
if dm_room:
return dm_room, room_id
# Fallback to public room if DM creation fails
logger.warning("Could not create DM with %s, falling back to public room", sender)
return room_id, room_id
async def handle_wordle(
client: AsyncClient, room_id: str, sender: str, args: str
):
"""Main entry point — dispatches to subcommands."""
parts = args.strip().split(None, 1)
subcmd = parts[0].lower() if parts else ""
sub_args = parts[1] if len(parts) > 1 else ""
# Share always goes to the public room
if subcmd == "share":
await wordle_share(client, room_id, sender)
return
# All other commands route through DM
dm_room, origin = await _get_dm_room(client, room_id, sender)
# Silently redirect to DM — origin room will get an auto-share when the game ends
if subcmd == "help":
await wordle_help(client, dm_room)
elif subcmd == "stats":
await wordle_stats(client, dm_room, sender)
elif subcmd == "hard":
await wordle_toggle_hard(client, dm_room, sender)
elif subcmd == "give" and sub_args.lower().startswith("up"):
await wordle_give_up(client, dm_room, sender)
elif subcmd == "":
await wordle_start_or_status(client, dm_room, sender, origin)
elif len(subcmd) == 5 and subcmd.isalpha():
await wordle_guess(client, dm_room, sender, subcmd.upper())
else:
await send_text(
client, room_id,
f"Invalid wordle command or guess. "
f"Guesses must be exactly 5 letters. "
f"Try {BOT_PREFIX}wordle help"
)
# ---------------------------------------------------------------------------
# Load stats on module import
# ---------------------------------------------------------------------------
_load_stats()