Press "Start Match" to begin the simulation. Gray blocks (█) are garbage lines sent by clearing multiple lines.
🎙️ Sports Commentator
Waiting for match to start...
💭 AI Thought Process
AI-1
AI-2
🎲 Figure Bank
📊 Match Statistics
🎯 Prediction League
Make your prediction before the match starts! Track your accuracy and compete for the monthly leaderboard.
No money involved — just bragging rights and community recognition!
Who will win this match?
Historical odds: AI-1: 50% | AI-2: 50% (based on 0 past matches)
Your Stats
Predictions: 0
Correct: 0
Accuracy: 0%
Current prediction: None
📊 How Prediction League Works
🎮 It's FREE and FUN!
Make predictions before each match starts
Build your accuracy statistics over time
Compete on monthly leaderboards (coming soon!)
Learn AI strategies by studying match patterns
🏆 Monthly Prizes (Ethical!):
Top 3 predictors receive a $50 GitHub Sponsorship in their name to an open-source project of their choice!
100% funded by YouTube ad revenue and Patreon supporters.
No gambling, no money changing hands. Just skill, strategy knowledge, and community support! 💎
🏆 Tournament Mode
Results
Match #
Winner
AI-1 Score
AI-2 Score
Turns
Reason
Summary:
packages = []
import asyncio
import random
import time
from js import document
from pyodide.ffi import create_proxy
# --- Core data structures ----------------------------------------------------
SHAPES = {
"I": [[1, 1, 1, 1]],
"O": [[1, 1], [1, 1]],
"T": [[0, 1, 0], [1, 1, 1]],
"S": [[0, 1, 1], [1, 1, 0]],
"Z": [[1, 1, 0], [0, 1, 1]],
"L": [[1, 0], [1, 0], [1, 1]],
"J": [[0, 1], [0, 1], [1, 1]],
}
# Classic Tetris colors (The Tetris Company standard)
COLORS = {
"I": "#00f0f0", # Cyan
"O": "#f0f000", # Yellow
"T": "#a000f0", # Purple
"S": "#00f000", # Green
"Z": "#f00000", # Red
"L": "#f0a000", # Orange
"J": "#0000f0", # Blue
}
TETROMINO_TYPES = list(SHAPES.keys())
def clone_shape(shape):
return [row[:] for row in shape]
class Tetromino:
def __init__(self, shape_type: str):
self.type = shape_type
self.shape = clone_shape(SHAPES[shape_type])
self.rotation = 0
def rotate(self):
rotated = [list(row) for row in zip(*self.shape[::-1])]
self.shape = rotated
self.rotation = (self.rotation + 1) % 4
def get_width(self):
return len(self.shape[0]) if self.shape else 0
def get_height(self):
return len(self.shape)
class GameBoard:
def __init__(self, width: int = 10, height: int = 20):
self.width = width
self.height = height
self.grid = [[None for _ in range(width)] for _ in range(height)]
self.score = 0
self.lines_cleared = 0
self.game_over = False
def can_place(self, piece: Tetromino, x: int, y: int) -> bool:
for row_idx, row in enumerate(piece.shape):
for col_idx, cell in enumerate(row):
if not cell:
continue
board_y = y + row_idx
board_x = x + col_idx
if board_x < 0 or board_x >= self.width:
return False
if board_y < 0 or board_y >= self.height:
return False
if self.grid[board_y][board_x]:
return False
return True
def place_piece(self, piece: Tetromino, x: int, y: int):
for row_idx, row in enumerate(piece.shape):
for col_idx, cell in enumerate(row):
if cell:
board_y = y + row_idx
board_x = x + col_idx
if 0 <= board_y < self.height and 0 <= board_x < self.width:
self.grid[board_y][board_x] = piece.type
def clear_lines(self) -> int:
completed = [index for index, row in enumerate(self.grid) if all(row)]
for idx in completed:
del self.grid[idx]
self.grid.insert(0, [None for _ in range(self.width)])
cleared = len(completed)
if cleared:
self.lines_cleared += cleared
score_table = {1: 100, 2: 300, 3: 500, 4: 800}
self.score += score_table.get(cleared, cleared * 100)
return cleared
def add_garbage_lines(self, num_lines: int):
for _ in range(num_lines):
self.grid.pop(0)
garbage = ['G' for _ in range(self.width)]
hole = random.randint(0, self.width - 1)
garbage[hole] = None
self.grid.append(garbage)
def is_game_over(self) -> bool:
return any(self.grid[0])
def get_height_map(self):
heights = []
for x in range(self.width):
column_height = 0
for y in range(self.height):
if self.grid[y][x]:
column_height = self.height - y
break
heights.append(column_height)
return heights
def get_max_height(self):
heights = self.get_height_map()
return max(heights) if heights else 0
def to_string(self) -> str:
"""Generate ASCII board with HTML color styling."""
lines = ["+" + "-" * self.width + "+"]
for row in self.grid:
line = "|"
for cell in row:
if cell:
if cell == 'G':
# Gray for garbage
line += '█'
else:
# Colored block for each piece type
color = COLORS.get(cell, "#39ff14")
line += '█'
else:
line += " "
line += "|"
lines.append(line)
lines.append("+" + "-" * self.width + "+")
return "\n".join(lines)
class FigureBank:
def __init__(self, initial_count: int = 15):
self.bank = {piece: initial_count for piece in TETROMINO_TYPES}
def get_piece(self, piece_type: str) -> bool:
if piece_type not in self.bank:
return False
if self.bank[piece_type] > 0:
self.bank[piece_type] -= 1
return True
return False
def is_available(self, piece_type: str) -> bool:
return piece_type in self.bank and self.bank[piece_type] > 0
def get_available_pieces(self):
return [piece for piece in TETROMINO_TYPES if self.bank[piece] > 0]
def is_empty(self) -> bool:
return all(count == 0 for count in self.bank.values())
def get_random_available(self) -> str | None:
available = self.get_available_pieces()
if available:
return random.choice(available)
return None
def get_state(self):
return self.bank.copy()
class AIAgent:
def __init__(self, name: str, strategy: str = "greedy"):
self.name = name
self.strategy = strategy
self.decision_times = []
def decide_placement(self, board: GameBoard, piece: Tetromino, verbose=False):
start_time = time.time()
best_position = None
best_score = float("-inf")
all_evaluations = [] # Store all considered positions
for rotation in range(4):
piece_copy = Tetromino(piece.type)
for _ in range(rotation):
piece_copy.rotate()
for x in range(board.width - piece_copy.get_width() + 1):
y = 0
while y < board.height and board.can_place(piece_copy, x, y):
y += 1
y -= 1
if y >= 0 and board.can_place(piece_copy, x, y):
score, metrics = self._evaluate_position(board, piece_copy, x, y, return_metrics=True)
all_evaluations.append({
"x": x, "y": y, "rotation": rotation,
"score": score, "metrics": metrics
})
if score > best_score:
best_score = score
best_position = (x, y, rotation)
decision_time = time.time() - start_time
self.decision_times.append(decision_time)
# Store thinking process for display
if verbose and all_evaluations:
self.last_thinking = {
"piece": piece.type,
"evaluations": sorted(all_evaluations, key=lambda e: e["score"], reverse=True)[:5],
"best": best_position,
"best_score": best_score,
"total_options": len(all_evaluations),
"decision_time": decision_time
}
if best_position:
return best_position
return (board.width // 2, 0, 0)
def _evaluate_position(self, board: GameBoard, piece: Tetromino, x: int, y: int, return_metrics=False):
test_board = GameBoard(board.width, board.height)
test_board.grid = [row[:] for row in board.grid]
test_board.place_piece(piece, x, y)
max_height = test_board.get_max_height()
heights = test_board.get_height_map()
aggregate_height = sum(heights)
bumpiness = sum(abs(heights[i] - heights[i + 1]) for i in range(len(heights) - 1))
holes = 0
for col_x in range(board.width):
found_block = False
for row_y in range(board.height):
if test_board.grid[row_y][col_x]:
found_block = True
elif found_block:
holes += 1
lines_cleared = self._count_potential_lines(test_board)
if self.strategy == "greedy":
score = -aggregate_height * 4.0 - holes * 7.0 - bumpiness * 3.0 + lines_cleared * 2.0
elif self.strategy == "defensive":
score = -aggregate_height * 3.0 - holes * 10.0 - bumpiness * 6.0 + lines_cleared * 1.5
elif self.strategy == "aggressive":
score = aggregate_height * 3.0 - holes * 5.0 + bumpiness * 0.5 + lines_cleared * 5.0
else:
score = -aggregate_height - holes
if return_metrics:
return score, {
"height": aggregate_height,
"holes": holes,
"bumpiness": bumpiness,
"lines": lines_cleared
}
return score
def _count_potential_lines(self, board: GameBoard) -> int:
return sum(1 for row in board.grid if all(row))
def choose_attack_piece(self, bank: FigureBank):
available = bank.get_available_pieces()
if not available:
return None
if self.strategy == "aggressive":
difficult = [p for p in ["T", "L", "J"] if p in available]
if difficult:
return random.choice(difficult)
elif self.strategy == "defensive":
easy = [p for p in ["I", "O"] if p in available]
if easy:
return random.choice(easy)
return random.choice(available)
class Arena:
def __init__(self, ai1: AIAgent, ai2: AIAgent, bank: FigureBank, max_turns: int = 360):
self.ai1 = ai1
self.ai2 = ai2
self.board1 = GameBoard()
self.board2 = GameBoard()
self.bank = bank
self.max_turns = max_turns
self.turn = 0
self.current_piece_ai1 = self._get_next_piece()
self.current_piece_ai2 = self._get_next_piece()
self.garbage_queue_ai1 = 0
self.garbage_queue_ai2 = 0
def _get_next_piece(self) -> str:
piece_type = self.bank.get_random_available()
if piece_type:
self.bank.get_piece(piece_type)
return piece_type
return random.choice(TETROMINO_TYPES)
def play_turn(self, ai: AIAgent, board: GameBoard, piece_type: str, verbose=False):
piece = Tetromino(piece_type)
x, y, rotations = ai.decide_placement(board, piece, verbose=verbose)
for _ in range(rotations):
piece.rotate()
if board.can_place(piece, x, y):
board.place_piece(piece, x, y)
else:
board.game_over = True
return 0, 0.0, None
lines_cleared = board.clear_lines()
attack_piece = None
if lines_cleared > 0:
attack_piece = ai.choose_attack_piece(self.bank)
decision_time = ai.decision_times[-1] if ai.decision_times else 0.0
return lines_cleared, decision_time, attack_piece
def _result_state(self, finished: bool, winner: str | None, reason: str):
return {
"finished": finished,
"winner": winner,
"reason": reason,
"turn": self.turn,
"scores": {"ai1": self.board1.score, "ai2": self.board2.score},
"lines": {"ai1": self.board1.lines_cleared, "ai2": self.board2.lines_cleared},
}
def _winner_by_score(self) -> str:
if self.board1.score == self.board2.score:
if self.board1.lines_cleared >= self.board2.lines_cleared:
return self.ai1.name
return self.ai2.name
if self.board1.score > self.board2.score:
return self.ai1.name
return self.ai2.name
def step(self, verbose=False):
if self.turn >= self.max_turns:
winner = self._winner_by_score()
result = self._result_state(True, winner, "turn_limit")
result["move_data"] = None
return result
self.turn += 1
move_data = {"ai1": {}, "ai2": {}}
self.current_thinking_ai = self.ai1.name # Track who's thinking
if self.garbage_queue_ai1:
self.board1.add_garbage_lines(self.garbage_queue_ai1)
self.garbage_queue_ai1 = 0
prev_piece1 = self.current_piece_ai1
lines1, _, attack1 = self.play_turn(self.ai1, self.board1, self.current_piece_ai1, verbose=verbose)
move_data["ai1"] = {
"piece": prev_piece1,
"lines": lines1,
"attack": attack1,
"board": self.board1
}
if self.board1.game_over or self.board1.is_game_over():
result = self._result_state(True, self.ai2.name, "ai1_board_overflow")
result["move_data"] = move_data
return result
if lines1 > 0:
self.garbage_queue_ai2 += lines1
if attack1 and self.bank.is_available(attack1):
self.current_piece_ai1 = attack1
self.bank.get_piece(attack1)
else:
self.current_piece_ai1 = self._get_next_piece()
self.current_thinking_ai = self.ai2.name # Switch to AI-2
if self.garbage_queue_ai2:
self.board2.add_garbage_lines(self.garbage_queue_ai2)
self.garbage_queue_ai2 = 0
prev_piece2 = self.current_piece_ai2
lines2, _, attack2 = self.play_turn(self.ai2, self.board2, self.current_piece_ai2, verbose=verbose)
move_data["ai2"] = {
"piece": prev_piece2,
"lines": lines2,
"attack": attack2,
"board": self.board2
}
if self.board2.game_over or self.board2.is_game_over():
result = self._result_state(True, self.ai1.name, "ai2_board_overflow")
result["move_data"] = move_data
return result
if lines2 > 0:
self.garbage_queue_ai1 += lines2
if attack2 and self.bank.is_available(attack2):
self.current_piece_ai2 = attack2
self.bank.get_piece(attack2)
else:
self.current_piece_ai2 = self._get_next_piece()
if self.turn >= self.max_turns:
winner = self._winner_by_score()
result = self._result_state(True, winner, "turn_limit")
result["move_data"] = move_data
return result
result = self._result_state(False, None, "ongoing")
result["move_data"] = move_data
return result
# --- UI helpers --------------------------------------------------------------
def board_text(board: GameBoard) -> str:
return board.to_string()
def bank_text(bank: FigureBank) -> str:
"""Generate ASCII bank display with colored piece indicators."""
lines = ['🎲 FIGURE BANK']
state = bank.get_state()
for piece in TETROMINO_TYPES:
count = state[piece]
color = COLORS.get(piece, "#39ff14")
# Color the piece letter in its signature color
colored_piece = '' + piece + ''
# Dim the count if low
if count <= 2:
count_style = 'color: #ff3b30; font-weight: bold;' # Red for critical
elif count <= 5:
count_style = 'color: #ff9500; font-weight: bold;' # Orange for low
else:
count_style = 'color: #e2e2e2;' # Normal
lines.append(f'{colored_piece}: {count:02d}')
total = sum(state.values())
lines.append(f'Total: {total}')
return "\n".join(lines)
# --- Canvas Rendering --------------------------------------------------------
def draw_board_canvas(ctx, board: GameBoard):
"""Draw the game board on a canvas with colored pieces."""
cell_size = 20
ctx.fillStyle = "#000000"
ctx.fillRect(0, 0, 200, 400)
for y in range(board.height):
for x in range(board.width):
cell = board.grid[y][x]
if cell:
# Use piece-specific color, or gray for garbage
if cell == 'G':
ctx.fillStyle = "#808080" # Gray for garbage
else:
ctx.fillStyle = COLORS.get(cell, "#39ff14")
ctx.fillRect(x * cell_size, y * cell_size, cell_size, cell_size)
# Draw border with slight highlight effect
ctx.strokeStyle = "#000000"
ctx.lineWidth = 2
ctx.strokeRect(x * cell_size, y * cell_size, cell_size, cell_size)
# Inner highlight for 3D effect
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)"
ctx.lineWidth = 1
ctx.strokeRect(x * cell_size + 2, y * cell_size + 2, cell_size - 4, cell_size - 4)
def draw_bank_canvas(ctx, bank: FigureBank):
"""Draw the figure bank on a canvas."""
ctx.fillStyle = "#000000"
ctx.fillRect(0, 0, 420, 150)
state = bank.get_state()
pieces = list(TETROMINO_TYPES)
bar_width = 50
bar_gap = 10
max_height = 100
max_count = 15
y_baseline = 130
for i, piece_type in enumerate(pieces):
x = 10 + i * (bar_width + bar_gap)
count = state[piece_type]
bar_height = (count / max_count) * max_height if max_count > 0 else 0
# Draw bar with piece color (dimmed if low count)
base_color = COLORS.get(piece_type, "#39ff14")
if count <= 2:
# Red tint for critically low
ctx.fillStyle = "#ff3b30"
elif count <= 5:
# Orange tint for low
ctx.fillStyle = "#ff9500"
else:
# Use piece's natural color
ctx.fillStyle = base_color
ctx.fillRect(x, y_baseline - bar_height, bar_width, bar_height)
# Draw border
ctx.strokeStyle = "#000000"
ctx.lineWidth = 2
ctx.strokeRect(x, y_baseline - bar_height, bar_width, bar_height)
# Draw piece type label with its color
ctx.fillStyle = base_color
ctx.font = "bold 16px monospace"
ctx.textAlign = "center"
ctx.fillText(piece_type, x + bar_width / 2, y_baseline + 18)
# Draw count
ctx.fillStyle = "#ffffff"
ctx.font = "bold 14px monospace"
ctx.fillText(str(count), x + bar_width / 2, y_baseline - bar_height - 8)
# --- UI State Management -----------------------------------------------------
class MatchController:
def __init__(self):
self.arena = None
self.is_running = False
self.is_paused = False
self.current_task = None
self.match_stats = {
"turns": [],
"ai1_scores": [],
"ai2_scores": [],
"ai1_lines": [],
"ai2_lines": [],
}
def update_ui(self):
if self.arena:
if use_canvas_view:
draw_board_canvas(ctx1, self.arena.board1)
draw_board_canvas(ctx2, self.arena.board2)
draw_bank_canvas(ctx_bank, self.arena.bank)
else:
# Use innerHTML to render HTML color tags
board1_element.innerHTML = board_text(self.arena.board1)
board2_element.innerHTML = board_text(self.arena.board2)
bank_element.innerHTML = bank_text(self.arena.bank)
else:
board1_element.innerHTML = ""
board2_element.innerHTML = ""
bank_element.innerHTML = ""
def update_status(self, state):
ai1_name = self.arena.ai1.name if self.arena else "AI-1"
ai2_name = self.arena.ai2.name if self.arena else "AI-2"
status_element.textContent = (
f"Turn {state['turn']} | Score {ai1_name}: {state['scores']['ai1']} | "
f"Score {ai2_name}: {state['scores']['ai2']}"
)
if state["finished"]:
status_element.textContent += (
f" | Winner: {state['winner']} (reason: {state['reason']})"
)
def collect_stats(self, state):
self.match_stats["turns"].append(state["turn"])
self.match_stats["ai1_scores"].append(state["scores"]["ai1"])
self.match_stats["ai2_scores"].append(state["scores"]["ai2"])
self.match_stats["ai1_lines"].append(state["lines"]["ai1"])
self.match_stats["ai2_lines"].append(state["lines"]["ai2"])
def display_stats(self):
if not self.arena:
return
ai1_avg_time = self.arena.ai1.get_average_decision_time() * 1000
ai2_avg_time = self.arena.ai2.get_average_decision_time() * 1000
stats_grid.innerHTML = f"""
AI-1 Final Score
{self.arena.board1.score}
AI-2 Final Score
{self.arena.board2.score}
AI-1 Lines Cleared
{self.arena.board1.lines_cleared}
AI-2 Lines Cleared
{self.arena.board2.lines_cleared}
AI-1 Avg Decision Time
{ai1_avg_time:.1f} ms
AI-2 Avg Decision Time
{ai2_avg_time:.1f} ms
Total Turns
{self.arena.turn}
Bank Pieces Remaining
{self.arena.bank.get_total_remaining()}
"""
stats_section.style.display = "block"
def update_buttons(self, running=None, paused=None, finished=False):
if running is not None:
self.is_running = running
if paused is not None:
self.is_paused = paused
start_button.disabled = self.is_running
pause_button.disabled = not self.is_running or self.is_paused or finished
resume_button.disabled = not self.is_paused or finished
step_button.disabled = not self.is_paused or finished
restart_button.disabled = not self.is_running and not finished
export_button.disabled = not finished
controller = MatchController()
start_button = document.getElementById("start")
pause_button = document.getElementById("pause")
resume_button = document.getElementById("resume")
step_button = document.getElementById("step")
restart_button = document.getElementById("restart")
export_button = document.getElementById("export-replay")
status_element = document.getElementById("status")
board1_element = document.getElementById("board1")
board2_element = document.getElementById("board2")
bank_element = document.getElementById("bank")
ai1_name_element = document.getElementById("ai1-name")
ai2_name_element = document.getElementById("ai2-name")
speed_slider = document.getElementById("speed")
speed_value = document.getElementById("speed-value")
ai1_strategy_select = document.getElementById("ai1-strategy")
ai2_strategy_select = document.getElementById("ai2-strategy")
canvas1 = document.getElementById("canvas1")
canvas2 = document.getElementById("canvas2")
bank_canvas = document.getElementById("bank-canvas")
ctx1 = canvas1.getContext("2d")
ctx2 = canvas2.getContext("2d")
ctx_bank = bank_canvas.getContext("2d")
view_ascii_button = document.getElementById("view-ascii")
view_canvas_button = document.getElementById("view-canvas")
stats_section = document.getElementById("stats-section")
stats_grid = document.getElementById("stats-grid")
thinking_section = document.getElementById("thinking-section")
thinking_content = document.getElementById("thinking-content")
show_thinking_checkbox = document.getElementById("show-thinking")
commentary_feed = document.getElementById("commentary-feed")
use_canvas_view = False
def update_speed_label(event=None):
speed_value.textContent = f"{speed_slider.value} ms"
def format_thinking(ai_name, thinking_data):
"""Format AI thinking process into human-readable text."""
if not thinking_data:
return ""
piece_color = COLORS.get(thinking_data["piece"], "#39ff14")
lines = [
'' + ai_name + ' received piece ' + thinking_data["piece"] + '',
f'Evaluated {thinking_data["total_options"]} possible placements in {thinking_data["decision_time"]*1000:.1f}ms',
"",
'🏆 Top 5 Placements Considered:'
]
for i, eval_data in enumerate(thinking_data["evaluations"][:5], 1):
m = eval_data["metrics"]
score = eval_data["score"]
is_best = (eval_data["x"] == thinking_data["best"][0] and
eval_data["rotation"] == thinking_data["best"][2])
prefix = "✓" if is_best else f"{i}."
color = "#39ff14" if is_best else "#e2e2e2"
font_weight = "bold" if is_best else "normal"
placement_desc = f'x={eval_data["x"]}, rot={eval_data["rotation"]}'
lines.append(
'' +
f'{prefix} {placement_desc}: score={score:.1f} ' +
f'(h={m["height"]}, holes={m["holes"]}, bump={m["bumpiness"]}, lines={m["lines"]})' +
''
)
lines.append("")
lines.append(
'➤ CHOSEN: ' +
f'x={thinking_data["best"][0]}, rotation={thinking_data["best"][2]} ' +
f'(score: {thinking_data["best_score"]:.1f})'
)
return " ".join(lines)
def display_thinking(ai, ai_name):
"""Display AI's thinking process if enabled."""
if not show_thinking_checkbox.checked:
thinking_section.style.display = "none"
return
if hasattr(ai, "last_thinking") and ai.last_thinking:
thinking_section.style.display = "block"
thinking_content.innerHTML = format_thinking(ai_name, ai.last_thinking)
else:
thinking_section.style.display = "none"
# --- Live Commentary System --------------------------------------------------
class Commentator:
"""Dynamic sports commentator for AI battles."""
def __init__(self):
self.commentary_log = []
self.last_heights = {"ai1": 0, "ai2": 0}
self.combo_count = {"ai1": 0, "ai2": 0}
self.critical_moments = 0
def add_comment(self, text, style="normal"):
"""Add a commentary line with timestamp and styling."""
colors = {
"normal": "#e2e2e2",
"excited": "#39ff14",
"critical": "#ff3b30",
"achievement": "#f0a000",
"dramatic": "#a000f0"
}
color = colors.get(style, "#e2e2e2")
self.commentary_log.append('
' + text + '
')
def clear(self):
"""Clear commentary log."""
self.commentary_log = []
self.last_heights = {"ai1": 0, "ai2": 0}
self.combo_count = {"ai1": 0, "ai2": 0}
self.critical_moments = 0
def get_html(self):
"""Get formatted HTML for display."""
return "".join(self.commentary_log[-8:]) # Show last 8 lines
def commentate_move(self, ai_name, piece_type, board, lines_cleared, attack_piece):
"""Generate commentary for a single move."""
piece_color = COLORS.get(piece_type, "#39ff14")
piece_html = '' + piece_type + ''
ai_html = '' + ai_name + ''
height = board.get_max_height()
ai_key = "ai1" if "AI-1" in ai_name else "ai2"
height_change = height - self.last_heights[ai_key]
self.last_heights[ai_key] = height
# Opening move
if self.last_heights[ai_key] < 5:
self.add_comment(f"{ai_html} opens with {piece_html}. Conservative start!")
return
# Tetris (4 lines)!
if lines_cleared == 4:
self.combo_count[ai_key] += 1
self.add_comment(
f"🔥 TETRIS!!! {ai_html} clears FOUR LINES with {piece_html}! The crowd goes WILD!",
"excited"
)
if attack_piece:
attack_color = COLORS.get(attack_piece, "#39ff14")
self.add_comment(
'💣 And sends ' + attack_piece + ' as a devastating counter-attack!',
"achievement"
)
return
# Triple clear
if lines_cleared == 3:
self.add_comment(
f"⚡ Triple clear by {ai_html}! {piece_html} placed perfectly!",
"achievement"
)
return
# Double clear
if lines_cleared == 2:
self.combo_count[ai_key] += 1
if self.combo_count[ai_key] >= 3:
self.add_comment(f"🎯 {ai_html} on a COMBO streak! Double clear!", "excited")
else:
self.add_comment(f"✓ {ai_html} clears 2 lines with {piece_html}.")
return
# Single clear
if lines_cleared == 1:
self.add_comment(f"{ai_html} clears 1 line. Steady progress...")
self.combo_count[ai_key] = 0
return
# Dangerous height
if height >= 16:
self.critical_moments += 1
if height >= 18:
self.add_comment(
f"🚨 CRITICAL! {ai_html}'s board at height {height}! One wrong move and it's GAME OVER!",
"critical"
)
else:
self.add_comment(
f"⚠️ {ai_html} in the danger zone! Height: {height}. Can they recover?",
"dramatic"
)
return
# Height building (Aggressive behavior)
if height_change > 3:
self.add_comment(
f"📈 {ai_html} building HIGH! {piece_html} placed at height {height}. Risky strategy!",
"dramatic"
)
return
# Safe play (Defensive behavior)
if height < 10 and lines_cleared == 0:
if random.random() < 0.3: # Don't spam boring moves
self.add_comment(f"{ai_html} playing it safe. {piece_html} carefully positioned.")
return
# Default move
if random.random() < 0.2: # Occasional neutral commentary
self.add_comment(f"{ai_html} places {piece_html}. Height: {height}.")
def commentate_match_start(self, ai1_name, ai2_name):
"""Opening commentary."""
self.add_comment("🎙️ Ladies and gentlemen, welcome to the TETRIS ARENA!", "excited")
self.add_comment(f"Today's matchup: {ai1_name} versus {ai2_name}!", "normal")
self.add_comment("Let the battle... BEGIN! 🔥", "achievement")
def commentate_match_end(self, winner, reason, ai1_score, ai2_score):
"""Closing commentary."""
if reason == "ai1_board_overflow":
self.add_comment("💥 AI-1's board has COLLAPSED!", "critical")
elif reason == "ai2_board_overflow":
self.add_comment("💥 AI-2's board has COLLAPSED!", "critical")
elif reason == "turn_limit":
self.add_comment("⏰ Time's up! Let's see the final scores...", "dramatic")
self.add_comment(
f"🏆 WINNER: {winner}! Final score: AI-1: {ai1_score} | AI-2: {ai2_score}",
"achievement"
)
self.add_comment("What an INCREDIBLE match! Thanks for watching! 👏", "excited")
commentator = Commentator()
async def run_match(event=None):
if controller.is_running:
return
controller.is_running = True
controller.is_paused = False
controller.update_buttons()
ai1_strategy = ai1_strategy_select.value
ai2_strategy = ai2_strategy_select.value
bank = FigureBank(initial_count=12)
ai1 = AIAgent(f"AI-1 ({ai1_strategy.capitalize()})", strategy=ai1_strategy)
ai2 = AIAgent(f"AI-2 ({ai2_strategy.capitalize()})", strategy=ai2_strategy)
controller.arena = Arena(ai1, ai2, bank, max_turns=320)
ai1_name_element.textContent = ai1.name
ai2_name_element.textContent = ai2.name
status_element.textContent = "Match in progress..."
# Initialize commentary
commentator.clear()
commentator.commentate_match_start(ai1.name, ai2.name)
commentary_feed.innerHTML = commentator.get_html()
while controller.is_running:
if controller.is_paused:
await asyncio.sleep(0.1)
continue
# Show thinking process if enabled
show_thinking = show_thinking_checkbox.checked
# Execute step with verbose mode
state = controller.arena.step(verbose=show_thinking)
# Generate commentary for both moves
if state["move_data"]:
move_ai1 = state["move_data"]["ai1"]
if move_ai1:
commentator.commentate_move(
controller.arena.ai1.name,
move_ai1["piece"],
move_ai1["board"],
move_ai1["lines"],
move_ai1["attack"]
)
move_ai2 = state["move_data"]["ai2"]
if move_ai2:
commentator.commentate_move(
controller.arena.ai2.name,
move_ai2["piece"],
move_ai2["board"],
move_ai2["lines"],
move_ai2["attack"]
)
# Update commentary display
commentary_feed.innerHTML = commentator.get_html()
# Display AI-1's thinking
if show_thinking:
display_thinking(controller.arena.ai1, controller.arena.ai1.name)
await asyncio.sleep(max(int(speed_slider.value), 50) / 1000.0)
controller.update_ui()
controller.update_status(state)
controller.collect_stats(state)
# Display AI-2's thinking
if show_thinking and not state["finished"]:
display_thinking(controller.arena.ai2, controller.arena.ai2.name)
await asyncio.sleep(max(int(speed_slider.value), 50) / 1000.0)
if state["finished"]:
# Final commentary
commentator.commentate_match_end(
state["winner"],
state["reason"],
state["scores"]["ai1"],
state["scores"]["ai2"]
)
commentary_feed.innerHTML = commentator.get_html()
# Record prediction result
prediction_league.record_result(state["winner"])
controller.is_running = False
controller.update_buttons(finished=True)
controller.display_stats()
thinking_section.style.display = "none"
export_commentary_button.disabled = False
export_highlights_button.disabled = False
break
delay = max(int(speed_slider.value), 50) / 1000.0
await asyncio.sleep(delay)
def pause_match(event=None):
controller.is_paused = True
controller.update_buttons(paused=True)
def resume_match(event=None):
controller.is_paused = False
controller.update_buttons(paused=False)
async def step_match(event=None):
if not controller.is_paused or not controller.arena:
return
state = controller.arena.step()
controller.update_ui()
controller.update_status(state)
if state["finished"]:
controller.update_buttons(finished=True)
def restart_match(event=None):
controller.is_running = False
controller.is_paused = False
controller.arena = None
controller.match_stats = {
"turns": [],
"ai1_scores": [],
"ai2_scores": [],
"ai1_lines": [],
"ai2_lines": [],
}
controller.update_buttons()
board1_element.textContent = ""
board2_element.textContent = ""
bank_element.textContent = ""
stats_section.style.display = "none"
export_commentary_button.disabled = True
export_highlights_button.disabled = True
prediction_league.reset_prediction()
commentator.clear()
commentary_feed.innerHTML = '
Waiting for match to start...
'
status_element.textContent = "Press 'Start Match' to begin a new simulation."
start_handler = create_proxy(lambda e: asyncio.ensure_future(run_match(e)))
pause_handler = create_proxy(pause_match)
resume_handler = create_proxy(resume_match)
step_handler = create_proxy(lambda e: asyncio.ensure_future(step_match(e)))
restart_handler = create_proxy(restart_match)
speed_handler = create_proxy(update_speed_label)
start_button.addEventListener("click", start_handler)
pause_button.addEventListener("click", pause_handler)
resume_button.addEventListener("click", resume_handler)
step_button.addEventListener("click", step_handler)
restart_button.addEventListener("click", restart_handler)
speed_slider.addEventListener("input", speed_handler)
controller.update_buttons()
update_speed_label()
# --- Tournament Mode ---------------------------------------------------------
tournament_results = []
start_tournament_button = document.getElementById("start-tournament")
clear_tournament_button = document.getElementById("clear-tournament")
num_matches_input = document.getElementById("num-matches")
tournament_results_div = document.getElementById("tournament-results")
tournament_tbody = document.getElementById("tournament-tbody")
tournament_summary = document.getElementById("tournament-summary")
async def run_tournament(event=None):
global tournament_results
if controller.is_running:
return
num_matches = int(num_matches_input.value)
if num_matches < 1 or num_matches > 50:
status_element.textContent = "Please enter 1-50 matches"
return
start_tournament_button.disabled = True
clear_tournament_button.disabled = True
start_button.disabled = True
status_element.textContent = f"Running tournament: 0/{num_matches} matches completed..."
ai1_strategy = ai1_strategy_select.value
ai2_strategy = ai2_strategy_select.value
for match_num in range(1, num_matches + 1):
bank = FigureBank(initial_count=12)
ai1 = AIAgent(f"AI-1 ({ai1_strategy.capitalize()})", strategy=ai1_strategy)
ai2 = AIAgent(f"AI-2 ({ai2_strategy.capitalize()})", strategy=ai2_strategy)
arena = Arena(ai1, ai2, bank, max_turns=320)
state = None
while True:
state = arena.step()
if state["finished"]:
break
await asyncio.sleep(0.001)
tournament_results.append({
"match": match_num,
"winner": state["winner"],
"ai1_score": state["scores"]["ai1"],
"ai2_score": state["scores"]["ai2"],
"turns": state["turn"],
"reason": state["reason"]
})
status_element.textContent = f"Running tournament: {match_num}/{num_matches} matches completed..."
await asyncio.sleep(0.05)
display_tournament_results()
start_tournament_button.disabled = False
clear_tournament_button.disabled = False
start_button.disabled = False
status_element.textContent = f"Tournament complete! {num_matches} matches finished."
def display_tournament_results():
global tournament_results
if not tournament_results:
return
tournament_tbody.innerHTML = ""
for result in tournament_results:
row = document.createElement("tr")
row.innerHTML = f"""
{result['match']}
{result['winner']}
{result['ai1_score']}
{result['ai2_score']}
{result['turns']}
{result['reason']}
"""
tournament_tbody.appendChild(row)
ai1_wins = sum(1 for r in tournament_results if r["winner"] == "AI-1")
ai2_wins = sum(1 for r in tournament_results if r["winner"] == "AI-2")
total = len(tournament_results)
avg_ai1_score = sum(r["ai1_score"] for r in tournament_results) / total
avg_ai2_score = sum(r["ai2_score"] for r in tournament_results) / total
avg_turns = sum(r["turns"] for r in tournament_results) / total
tournament_summary.innerHTML = f"""