commit bbad07dc9f979cc67d38dbca649cf44ba5105b6e parent 954a0c724359566ff6b815007b34439371b7f35f Author: Brian Graham <brian@buildingbetterteams.de> Date: Fri, 3 Apr 2026 20:09:22 +0200 Redesign run detail page, rich transcript viewer, tetris iframe preview Run detail page: - Stats, config, and evaluation scores across the top in 3 columns - Config shows all possible values greyed out, active value highlighted - Exit codes explained (0=Success, 124=Timeout, etc.) - Claude version shown in metrics card Transcript viewer (full rewrite): - Thinking blocks: collapsible, muted italic - Text blocks: basic markdown rendering (code blocks, bold, inline code) - Tool use: terminal-style headers, file write detection, syntax coloring - Tool results: compact code blocks, stderr in red, collapsible when long - Result: summary card with cost and turns Grid page: - Run IDs show task badge + model/prompt/lang instead of full config string Tetris artifacts: - Workspace archives extracted to dashboard/public/artifacts/ - Run detail shows playable tetris iframe next to transcript Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Diffstat:
21 files changed, 6382 insertions(+), 337 deletions(-)
diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/IMPLEMENTATION_SUMMARY.md b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,265 @@ +# Tetris Game Implementation Summary + +## 🎮 What Was Built + +A fully functional, single-file Tetris game with: +- **Complete gameplay** - All standard Tetris mechanics +- **No dependencies** - Pure HTML, CSS, and JavaScript +- **Professional UI** - Clean design with gradients and proper layout +- **Full scoring system** - Level progression and multipliers +- **Smart controls** - 5-direction input system with rotation +- **Game states** - Active play, game over, and restart + +## 📋 Requirements Met + +### ✅ Game Board +- [x] 10×20 grid +- [x] Visual cell borders for clear positioning +- [x] Color-coded blocks by piece type +- [x] Dark background (#1a1a1a) for contrast + +### ✅ Pieces (7 Tetrominoes) +- [x] I-piece (Cyan #00f0f0) +- [x] O-piece (Yellow #f0f000) +- [x] T-piece (Purple #a000f0) +- [x] S-piece (Green #00f000) +- [x] Z-piece (Red #f00000) +- [x] J-piece (Blue #0000f0) +- [x] L-piece (Orange #f0a000) +- [x] Proper spawning at top center +- [x] Random selection system + +### ✅ Controls +- [x] Left/Right arrows - Horizontal movement +- [x] Down arrow - Soft drop +- [x] Up arrow - Clockwise rotation +- [x] Z key - Counter-clockwise rotation +- [x] Space - Hard drop +- [x] Keyboard event handling with preventDefault + +### ✅ Rotation System +- [x] Clockwise rotation (↑) +- [x] Counter-clockwise rotation (Z) +- [x] O-piece exception (no rotation) +- [x] Wall-kick system (tries offset positions) +- [x] Graceful failure if no valid position + +### ✅ Line Clearing +- [x] Row completion detection +- [x] Multiple simultaneous row clears +- [x] Grid shifting after clearing +- [x] Visual feedback in line counter + +### ✅ Scoring System +``` +Lines Cleared | Points per Line +1 (Single) | 100 × level +2 (Double) | 300 × level +3 (Triple) | 500 × level +4 (Tetris) | 800 × level +``` + +### ✅ Level System +- [x] Starts at level 1 +- [x] Increases by 1 per 10 lines cleared +- [x] Drop speed: 800ms (level 1) to 100ms+ (level 8+) +- [x] Automatic difficulty progression + +### ✅ Display Elements +- [x] Game board (10×20 grid) +- [x] Score counter (real-time) +- [x] Level indicator +- [x] Lines cleared counter +- [x] Next piece preview (4×4 canvas) + +### ✅ Game Over +- [x] Detection when piece can't spawn +- [x] Modal overlay with stats +- [x] Final score display +- [x] Restart button (location.reload) + +### ✅ Visual Quality +- [x] Modern gradient backgrounds +- [x] Professional typography +- [x] Proper color palette +- [x] Clear layout with sidebar +- [x] 3D cell effect with highlights +- [x] Responsive centering + +## 🏗️ Code Architecture + +### Class Hierarchy +``` +GameState +├── grid (10×20 array) +├── currentPiece (Tetromino) +├── score, lines, level +└── gameOver flag + +Tetromino +├── type (I, O, T, S, Z, J, L) +├── rotationIndex +├── shapes (rotation variants) +└── rotate() method + +TetrisGame +├── Game logic +├── Event handling +├── Canvas rendering +├── Game loop (requestAnimationFrame) +└── Score/display updates +``` + +### Key Methods + +**Movement & Collision** +- `canPlacePiece(x, y, piece)` - Collision detection +- `moveLeft()`, `moveRight()` - Horizontal movement +- `softDrop()`, `hardDrop()` - Vertical movement + +**Game Logic** +- `spawnNewPiece()` - Create new falling piece +- `placePiece()` - Lock piece in place +- `clearLines()` - Find and clear complete rows +- `rotate(direction)` - With wall-kick fallback + +**Rendering** +- `render()` - Main game board +- `renderNextPiece()` - Next piece preview +- `drawCell()` - Individual cell with 3D effect + +**Game Flow** +- `gameLoop()` - Main animation frame loop +- `autoDropPiece()` - Automatic falling +- `showGameOver()` - End state display + +## 📦 File Structure + +**tetris.html** (669 lines, 23 KB) +``` +1-180 HTML structure & CSS styling +181-186 Game over overlay +187-212 DOM elements (canvas, score, level, lines, next piece) +213-669 JavaScript game logic +``` + +**Key Constants** +```javascript +GRID_WIDTH = 10 +GRID_HEIGHT = 20 +CELL_SIZE = 30 pixels + +PIECE_SHAPES = { I, O, T, S, Z, J, L } +COLORS = { cyan, yellow, purple, green, red, blue, orange } +``` + +## 🎮 Game Loop Flow + +``` +1. Initialize game and spawn first piece +2. Every frame: + - Check auto-drop timer (800ms → 100ms based on level) + - Handle keyboard input + - Update display (score, level, lines) + - Render game board and current piece + - Render next piece preview + - Check game over condition +3. Game over: + - Stop game loop + - Show modal with stats + - Allow restart via button +``` + +## 🔧 No Dependencies + +- ✅ No Node.js required +- ✅ No npm packages +- ✅ No CDN links +- ✅ No external frameworks +- ✅ Pure HTML5 Canvas API +- ✅ Native browser APIs only +- ✅ Works offline completely + +## 🚀 Deployment Options + +### Option 1: Direct File Open +```bash +# Just open in browser +open tetris.html +# or double-click in file explorer +``` + +### Option 2: HTTP Server +```bash +# Python 3 +python3 -m http.server 8000 +# Then visit: http://localhost:8000/tetris.html + +# Python 2 +python -m SimpleHTTPServer 8000 + +# Node.js (if installed) +npx http-server +``` + +### Option 3: Web Hosting +- Upload `tetris.html` to any static file host +- Works on any web server +- No server-side code needed + +## 🎯 Testing Checklist + +- [x] Game starts successfully +- [x] Pieces spawn and fall +- [x] Left/right movement works +- [x] Soft drop (↓) increases speed +- [x] Hard drop (space) instant drops +- [x] Rotation (↑ and Z) works +- [x] Wall-kick prevents piece overlap +- [x] Lines clear when row is complete +- [x] Multiple line clears work (2x, 3x, 4x) +- [x] Score updates correctly +- [x] Level increases every 10 lines +- [x] Speed increases with level +- [x] Next piece preview updates +- [x] Game over triggers on overflow +- [x] Game over modal shows +- [x] Restart button reloads game + +## 🎨 Customization Options + +Users can easily customize by editing tetris.html: + +```javascript +// Adjust colors (COLORS object) +COLORS = { + I: '#00f0f0', // Change any color + // ... etc +} + +// Adjust board size (GRID constants) +const GRID_WIDTH = 10; // Change to 8 or 12 +const GRID_HEIGHT = 20; // Change to 16 or 24 + +// Adjust drop speed (GameState.getDropInterval) +return Math.max(100, 800 - (this.level - 1) * 50); +// Modify 800 (start) or 50 (speed increase per level) +``` + +## ✨ Features Highlights + +1. **Authentic Tetris Feel** - Proper piece shapes and movements +2. **Progressive Difficulty** - Game gets harder as you play +3. **Visual Feedback** - Real-time score and level updates +4. **Smart Rotation** - Wall-kick prevents frustration +5. **Professional Look** - Not a basic prototype +6. **Zero Setup** - Just open the file and play +7. **Responsive Layout** - Looks good on different screens +8. **Touch of Polish** - Gradient backgrounds, shadows, 3D effects + +--- + +**Total Implementation Time**: Single cohesive HTML file with complete game logic +**Performance**: 60 FPS, smooth gameplay +**Browser Support**: All modern browsers (Chrome, Firefox, Safari, Edge) +**File Size**: 23 KB (all-in-one) diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/QUICK_START.txt b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/QUICK_START.txt @@ -0,0 +1,152 @@ +╔════════════════════════════════════════════════════════════════════════════╗ +║ 🎮 TETRIS GAME - QUICK START GUIDE 🎮 ║ +╚════════════════════════════════════════════════════════════════════════════╝ + +📦 WHAT YOU HAVE: + +✅ tetris.html (23 KB) + → Complete, fully-functional Tetris game + → Single HTML file with embedded CSS and JavaScript + → No external dependencies or downloads required + → Works in any modern web browser + +✅ README.md + → Complete gameplay documentation + → Controls reference + → Feature overview + → Browser compatibility info + +✅ IMPLEMENTATION_SUMMARY.md + → Technical architecture details + → Code structure explanation + → Complete requirements checklist + → Customization guide + +═════════════════════════════════════════════════════════════════════════════ + +🚀 HOW TO PLAY: + +Method 1: Direct File Open (Easiest) + → Double-click tetris.html + → Or drag tetris.html into your browser window + +Method 2: Local HTTP Server + → Open terminal in this directory + → Run: python3 -m http.server 8000 + → Visit: http://localhost:8000/tetris.html + +═════════════════════════════════════════════════════════════════════════════ + +⌨️ GAME CONTROLS: + + ← → Move left/right + ↓ Soft drop (faster fall) + ↑ Rotate clockwise + Z Rotate counter-clockwise + Space Hard drop (instant fall) + +═════════════════════════════════════════════════════════════════════════════ + +🎮 GAME FEATURES: + +✓ 10×20 Game Grid ✓ All 7 Tetromino Pieces +✓ Real-time Score ✓ Level Progression (increases difficulty) +✓ Line Clearing ✓ Next Piece Preview +✓ Multiplier Scoring ✓ Wall-kick Rotation System +✓ Auto-increasing Speed ✓ Game Over Detection +✓ Restart Button ✓ Professional UI Design + +═════════════════════════════════════════════════════════════════════════════ + +🎯 GAME MECHANICS: + +Pieces: + I (Cyan) - 4 in a row S (Green) - S shape + O (Yellow) - 2×2 square Z (Red) - Z shape + T (Purple) - T shape J (Blue) - J shape + L (Orange) - L shape + +Scoring: + 1 line: 100 × level + 2 lines: 300 × level + 3 lines: 500 × level + 4 lines: 800 × level (Tetris!) + +Levels: + Start at Level 1 + +1 level for every 10 lines cleared + Speed increases with each level + Starts at 800ms drop interval, speeds up to 100ms + +═════════════════════════════════════════════════════════════════════════════ + +💡 TIPS FOR WINNING: + +1. Plan ahead using the "Next Piece" preview +2. Stack pieces strategically to create complete rows +3. Save space for I-pieces (they clear 4 lines at once!) +4. Use hard drop (Space) to place pieces quickly +5. Multiple clears = bigger score bonuses + +═════════════════════════════════════════════════════════════════════════════ + +🔧 TECHNICAL DETAILS: + +Language: Pure JavaScript (TypeScript-compatible syntax) +Framework: None (zero dependencies) +Runtime: Browser JavaScript engine +Rendering: HTML5 Canvas API +File Size: 23 KB (all-in-one) +Performance: 60 FPS smooth gameplay +Compatibility: Chrome, Firefox, Safari, Edge, Opera + +No Build Step Required - Just Open and Play! + +═════════════════════════════════════════════════════════════════════════════ + +📱 BROWSER SUPPORT: + +✅ Chrome/Chromium (latest) +✅ Firefox (latest) +✅ Safari (latest) +✅ Microsoft Edge (latest) +✅ Opera (latest) +✅ Works on Linux, macOS, Windows + +═════════════════════════════════════════════════════════════════════════════ + +🎨 VISUAL FEATURES: + +• Modern gradient background +• Clear grid with cell borders +• Color-coded pieces (standard Tetris colors) +• 3D effect on game cells +• Professional sidebar with stats +• Game Over modal overlay +• Real-time score/level/lines updates +• Next piece preview canvas + +═════════════════════════════════════════════════════════════════════════════ + +❓ TROUBLESHOOTING: + +Q: File won't open? +A: Make sure to use a modern browser (Chrome, Firefox, Safari, Edge) + +Q: Game too slow/fast? +A: This is normal - difficulty increases with levels. Keep playing! + +Q: Controls not working? +A: Click the game area first to give it focus, then use keyboard + +Q: Want to customize? +A: Edit the COLORS object or GRID_WIDTH/HEIGHT constants in tetris.html + +═════════════════════════════════════════════════════════════════════════════ + +🎮 Ready to play? Open tetris.html and enjoy! 🎮 + +For full documentation, see README.md +For technical details, see IMPLEMENTATION_SUMMARY.md + +═════════════════════════════════════════════════════════════════════════════ diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/README.md b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/README.md @@ -0,0 +1,126 @@ +# 🎮 Tetris Game - Complete Web-Based Implementation + +A fully playable, single-file Tetris game that runs directly in your web browser with no external dependencies. + +## 🚀 How to Play + +### Quick Start +Simply open `tetris.html` in any modern web browser (Chrome, Firefox, Safari, Edge, etc.) + +```bash +# Option 1: Direct file open (drag and drop into browser) +# Or simply double-click tetris.html + +# Option 2: Using a simple HTTP server +python3 -m http.server 8000 +# Then visit: http://localhost:8000/tetris.html +``` + +## ⌨️ Controls + +| Key | Action | +|-----|--------| +| **← →** | Move piece left/right | +| **↓** | Soft drop (increase fall speed) | +| **↑** | Rotate piece clockwise | +| **Z** | Rotate piece counter-clockwise | +| **Space** | Hard drop (instant drop) | + +## 🎯 Game Features + +### All 7 Standard Tetromino Pieces +- **I** (Cyan) - 4 in a row +- **O** (Yellow) - 2x2 square +- **T** (Purple) - T-shape +- **S** (Green) - S-skew +- **Z** (Red) - Z-skew +- **J** (Blue) - J-shape +- **L** (Orange) - L-shape + +### Game Mechanics +- **10x20 grid** with clear cell borders +- **Random piece generation** with next piece preview +- **Line clearing** - complete rows automatically clear +- **Scoring system**: + - 1 line: 100 × level + - 2 lines: 300 × level + - 3 lines: 500 × level + - 4 lines: 800 × level (Tetris!) + +- **Progressive difficulty**: + - Starts at Level 1 + - Level increases every 10 lines cleared + - Drop speed increases with level + - Speed range: 800ms (level 1) to 100ms (level 8+) + +### Display Elements +- **Score** - Real-time score updates +- **Level** - Current difficulty level +- **Lines** - Total lines cleared +- **Next Piece** - Preview of upcoming piece + +### Smart Rotation +- Clockwise (↑) and counter-clockwise (Z) rotation +- Wall-kick system tries alternative positions when rotation fails +- O piece (square) correctly doesn't rotate +- Game over detection and restart button + +## 🏗️ Technical Details + +- **Pure JavaScript** - No frameworks or external libraries +- **Single HTML file** - Everything embedded +- **Canvas-based rendering** - Smooth 60 FPS gameplay +- **TypeScript-compatible code** - Well-structured and commented +- **Responsive design** - Centered layout with sidebar for stats + +## 💾 File Details + +- `tetris.html` - Complete game (~23 KB, 669 lines) +- All styles, logic, and rendering are self-contained +- No internet connection required +- Works offline completely + +## 🎨 Visual Design + +- Clean, modern interface with gradient backgrounds +- Distinct colors for each piece type +- Clear grid lines for position reference +- 3D-effect cell borders with highlights +- Professional-looking score/info panels +- Game over overlay with final stats + +## 📱 Browser Compatibility + +Works on all modern browsers: +- ✅ Chrome/Chromium (latest) +- ✅ Firefox (latest) +- ✅ Safari (latest) +- ✅ Edge (latest) +- ✅ Opera (latest) + +## 🎓 Code Structure + +The implementation includes: + +1. **Game Constants** - Grid size, colors, piece shapes +2. **GameState Class** - Manages game state and scoring +3. **Tetromino Class** - Represents individual pieces +4. **TetrisGame Class** - Main game logic and rendering + +Key methods: +- `spawnNewPiece()` - Creates new falling piece +- `canPlacePiece()` - Collision detection +- `clearLines()` - Row completion detection +- `rotate()` - Rotation with wall-kick +- `render()` - Canvas drawing +- Game loop with automatic piece dropping + +## 🎯 Tips for High Scores + +1. **Stack strategically** - Leave gaps for I-pieces to create Tetris clears +2. **Clear multiple lines** - Bonus points for 2+ line clears +3. **Watch the next piece** - Plan ahead using the preview +4. **Level up faster** - Early lines determine long-term difficulty +5. **Use hard drop** - Save time with Space key when position is set + +Enjoy! 🎮 diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/index.html b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/index.html @@ -0,0 +1,669 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Tetris Game</title> + <style> + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + } + + .container { + display: flex; + gap: 40px; + background: rgba(255, 255, 255, 0.95); + padding: 30px; + border-radius: 15px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-width: 600px; + } + + .game-area { + display: flex; + flex-direction: column; + gap: 10px; + } + + .game-title { + text-align: center; + font-size: 28px; + font-weight: bold; + color: #333; + margin-bottom: 10px; + } + + canvas { + border: 3px solid #333; + background: #1a1a1a; + display: block; + image-rendering: pixelated; + image-rendering: crisp-edges; + } + + .sidebar { + display: flex; + flex-direction: column; + gap: 30px; + min-width: 150px; + } + + .info-box { + background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%); + padding: 20px; + border-radius: 10px; + border: 2px solid #999; + text-align: center; + } + + .info-label { + font-size: 12px; + font-weight: bold; + color: #666; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 8px; + } + + .info-value { + font-size: 32px; + font-weight: bold; + color: #2196F3; + } + + .next-piece-container { + background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%); + padding: 20px; + border-radius: 10px; + border: 2px solid #999; + } + + .next-piece-label { + font-size: 12px; + font-weight: bold; + color: #666; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 10px; + text-align: center; + } + + .next-piece-canvas { + border: 2px solid #333; + background: #1a1a1a; + display: block; + margin: 0 auto; + } + + .controls { + background: #f9f9f9; + padding: 20px; + border-radius: 10px; + border: 2px solid #ddd; + font-size: 12px; + color: #555; + } + + .controls h3 { + margin-bottom: 10px; + color: #333; + } + + .controls p { + margin: 5px 0; + line-height: 1.5; + } + + .game-over-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: none; + justify-content: center; + align-items: center; + z-index: 1000; + } + + .game-over-box { + background: white; + padding: 40px; + border-radius: 15px; + text-align: center; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + } + + .game-over-box h1 { + font-size: 48px; + color: #e74c3c; + margin-bottom: 20px; + } + + .game-over-box p { + font-size: 24px; + color: #333; + margin-bottom: 10px; + } + + .game-over-box button { + margin-top: 30px; + padding: 15px 40px; + font-size: 18px; + background: #2196F3; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background 0.3s; + } + + .game-over-box button:hover { + background: #1976D2; + } + </style> +</head> +<body> + <div class="game-over-overlay" id="gameOverOverlay"> + <div class="game-over-box"> + <h1>GAME OVER</h1> + <p>Final Score: <span id="finalScore">0</span></p> + <p>Lines: <span id="finalLines">0</span></p> + <p>Level: <span id="finalLevel">0</span></p> + <button onclick="location.reload()">Play Again</button> + </div> + </div> + + <div class="container"> + <div class="game-area"> + <div class="game-title">TETRIS</div> + <canvas id="gameCanvas" width="300" height="600"></canvas> + </div> + <div class="sidebar"> + <div class="info-box"> + <div class="info-label">Score</div> + <div class="info-value" id="scoreDisplay">0</div> + </div> + <div class="info-box"> + <div class="info-label">Level</div> + <div class="info-value" id="levelDisplay">1</div> + </div> + <div class="info-box"> + <div class="info-label">Lines</div> + <div class="info-value" id="linesDisplay">0</div> + </div> + <div class="next-piece-container"> + <div class="next-piece-label">Next Piece</div> + <canvas id="nextPieceCanvas" width="120" height="120"></canvas> + </div> + <div class="controls"> + <h3>Controls</h3> + <p>← → Move</p> + <p>↓ Soft Drop</p> + <p>↑ Rotate CW</p> + <p>Z Rotate CCW</p> + <p>Space Hard Drop</p> + </div> + </div> + </div> + + <script> + // ===== Game Constants ===== + const GRID_WIDTH = 10; + const GRID_HEIGHT = 20; + const CELL_SIZE = 30; + + const COLORS = { + I: '#00f0f0', // Cyan + O: '#f0f000', // Yellow + T: '#a000f0', // Purple + S: '#00f000', // Green + Z: '#f00000', // Red + J: '#0000f0', // Blue + L: '#f0a000', // Orange + empty: '#1a1a1a' + }; + + const PIECE_SHAPES = { + I: [ + [[1, 1, 1, 1]], + ], + O: [ + [[1, 1], [1, 1]] + ], + T: [ + [[0, 1, 0], [1, 1, 1]], + [[1, 0], [1, 1], [1, 0]], + [[1, 1, 1], [0, 1, 0]], + [[0, 1], [1, 1], [0, 1]] + ], + S: [ + [[0, 1, 1], [1, 1, 0]], + [[1, 0], [1, 1], [0, 1]] + ], + Z: [ + [[1, 1, 0], [0, 1, 1]], + [[0, 1], [1, 1], [1, 0]] + ], + J: [ + [[1, 0, 0], [1, 1, 1]], + [[1, 1], [1, 0], [1, 0]], + [[1, 1, 1], [0, 0, 1]], + [[1, 0], [1, 0], [1, 1]] + ], + L: [ + [[0, 0, 1], [1, 1, 1]], + [[1, 0], [1, 0], [1, 1]], + [[1, 1, 1], [1, 0, 0]], + [[1, 1], [0, 1], [0, 1]] + ] + }; + + // ===== Game State ===== + class GameState { + constructor() { + this.grid = Array(GRID_HEIGHT).fill(null).map(() => Array(GRID_WIDTH).fill(null)); + this.currentPiece = null; + this.currentX = 0; + this.currentY = 0; + this.nextPiece = null; + this.score = 0; + this.lines = 0; + this.level = 1; + this.gameOver = false; + this.lastDropTime = Date.now(); + this.dropInterval = this.getDropInterval(); + } + + getDropInterval() { + // Start at 800ms, decrease with level (minimum 100ms) + return Math.max(100, 800 - (this.level - 1) * 50); + } + + updateLevel() { + const newLevel = Math.floor(this.lines / 10) + 1; + if (newLevel !== this.level) { + this.level = newLevel; + this.dropInterval = this.getDropInterval(); + } + } + } + + // ===== Tetromino Class ===== + class Tetromino { + constructor(type) { + this.type = type; + this.rotationIndex = 0; + this.shapes = PIECE_SHAPES[type]; + } + + getCurrentShape() { + return this.shapes[this.rotationIndex]; + } + + rotate(direction) { + const newRotationIndex = direction === 1 + ? (this.rotationIndex + 1) % this.shapes.length + : (this.rotationIndex - 1 + this.shapes.length) % this.shapes.length; + const oldRotationIndex = this.rotationIndex; + this.rotationIndex = newRotationIndex; + return oldRotationIndex !== newRotationIndex; + } + + resetRotation() { + this.rotationIndex = oldRotationIndex; + } + } + + // ===== Game Logic ===== + class TetrisGame { + constructor() { + this.state = new GameState(); + this.canvas = document.getElementById('gameCanvas'); + this.nextPieceCanvas = document.getElementById('nextPieceCanvas'); + this.ctx = this.canvas.getContext('2d'); + this.nextCtx = this.nextPieceCanvas.getContext('2d'); + + this.spawnNewPiece(); + this.render(); + this.setupEventListeners(); + this.gameLoop(); + } + + spawnNewPiece() { + if (this.state.nextPiece === null) { + this.state.nextPiece = this.getRandomPiece(); + } + + this.state.currentPiece = this.state.nextPiece; + this.state.nextPiece = this.getRandomPiece(); + this.state.currentX = Math.floor(GRID_WIDTH / 2) - 1; + this.state.currentY = 0; + + if (!this.canPlacePiece(this.state.currentX, this.state.currentY, this.state.currentPiece)) { + this.state.gameOver = true; + } + } + + getRandomPiece() { + const types = Object.keys(PIECE_SHAPES); + const randomType = types[Math.floor(Math.random() * types.length)]; + return new Tetromino(randomType); + } + + canPlacePiece(x, y, piece) { + const shape = piece.getCurrentShape(); + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col]) { + const gridX = x + col; + const gridY = y + row; + + if (gridX < 0 || gridX >= GRID_WIDTH || gridY >= GRID_HEIGHT) { + return false; + } + + if (gridY >= 0 && this.state.grid[gridY][gridX] !== null) { + return false; + } + } + } + } + return true; + } + + placePiece() { + const shape = this.state.currentPiece.getCurrentShape(); + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col]) { + const gridX = this.state.currentX + col; + const gridY = this.state.currentY + row; + if (gridY >= 0) { + this.state.grid[gridY][gridX] = this.state.currentPiece.type; + } + } + } + } + this.clearLines(); + } + + clearLines() { + let linesCleared = 0; + const linesToRemove = []; + + for (let row = 0; row < GRID_HEIGHT; row++) { + if (this.state.grid[row].every(cell => cell !== null)) { + linesToRemove.push(row); + linesCleared++; + } + } + + if (linesCleared > 0) { + linesToRemove.forEach(row => { + this.state.grid.splice(row, 1); + this.state.grid.unshift(Array(GRID_WIDTH).fill(null)); + }); + + this.state.lines += linesCleared; + this.state.updateLevel(); + + const points = { + 1: 100, + 2: 300, + 3: 500, + 4: 800 + }; + const basePoints = points[linesCleared] || 0; + this.state.score += basePoints * this.state.level; + } + } + + moveLeft() { + if (this.canPlacePiece(this.state.currentX - 1, this.state.currentY, this.state.currentPiece)) { + this.state.currentX--; + } + } + + moveRight() { + if (this.canPlacePiece(this.state.currentX + 1, this.state.currentY, this.state.currentPiece)) { + this.state.currentX++; + } + } + + softDrop() { + if (this.canPlacePiece(this.state.currentX, this.state.currentY + 1, this.state.currentPiece)) { + this.state.currentY++; + } else { + this.placePiece(); + this.spawnNewPiece(); + } + } + + hardDrop() { + while (this.canPlacePiece(this.state.currentX, this.state.currentY + 1, this.state.currentPiece)) { + this.state.currentY++; + } + this.placePiece(); + this.spawnNewPiece(); + } + + rotate(direction) { + if (this.state.currentPiece.type === 'O') return; // O piece doesn't rotate + + this.state.currentPiece.rotate(direction); + + // Try basic placement + if (this.canPlacePiece(this.state.currentX, this.state.currentY, this.state.currentPiece)) { + return; + } + + // Wall kick: try offset positions + const offsets = [[-1, 0], [1, 0], [0, -1]]; + for (const [dx, dy] of offsets) { + if (this.canPlacePiece(this.state.currentX + dx, this.state.currentY + dy, this.state.currentPiece)) { + this.state.currentX += dx; + this.state.currentY += dy; + return; + } + } + + // Revert rotation if no valid position found + this.state.currentPiece.rotate(direction === 1 ? -1 : 1); + } + + setupEventListeners() { + document.addEventListener('keydown', (e) => { + if (this.state.gameOver) return; + + switch (e.key) { + case 'ArrowLeft': + e.preventDefault(); + this.moveLeft(); + break; + case 'ArrowRight': + e.preventDefault(); + this.moveRight(); + break; + case 'ArrowDown': + e.preventDefault(); + this.softDrop(); + break; + case 'ArrowUp': + e.preventDefault(); + this.rotate(1); + break; + case 'z': + case 'Z': + e.preventDefault(); + this.rotate(-1); + break; + case ' ': + e.preventDefault(); + this.hardDrop(); + break; + } + }); + } + + autoDropPiece() { + const now = Date.now(); + if (now - this.state.lastDropTime > this.state.dropInterval) { + this.softDrop(); + this.state.lastDropTime = now; + } + } + + gameLoop() { + this.autoDropPiece(); + this.updateDisplay(); + this.render(); + + if (this.state.gameOver) { + this.showGameOver(); + return; + } + + requestAnimationFrame(() => this.gameLoop()); + } + + updateDisplay() { + document.getElementById('scoreDisplay').textContent = this.state.score; + document.getElementById('levelDisplay').textContent = this.state.level; + document.getElementById('linesDisplay').textContent = this.state.lines; + } + + showGameOver() { + document.getElementById('finalScore').textContent = this.state.score; + document.getElementById('finalLines').textContent = this.state.lines; + document.getElementById('finalLevel').textContent = this.state.level; + document.getElementById('gameOverOverlay').style.display = 'flex'; + } + + render() { + // Clear canvas + this.ctx.fillStyle = COLORS.empty; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Draw grid + this.ctx.strokeStyle = '#333'; + this.ctx.lineWidth = 0.5; + for (let i = 0; i <= GRID_WIDTH; i++) { + this.ctx.beginPath(); + this.ctx.moveTo(i * CELL_SIZE, 0); + this.ctx.lineTo(i * CELL_SIZE, this.canvas.height); + this.ctx.stroke(); + } + for (let i = 0; i <= GRID_HEIGHT; i++) { + this.ctx.beginPath(); + this.ctx.moveTo(0, i * CELL_SIZE); + this.ctx.lineTo(this.canvas.width, i * CELL_SIZE); + this.ctx.stroke(); + } + + // Draw placed blocks + for (let row = 0; row < GRID_HEIGHT; row++) { + for (let col = 0; col < GRID_WIDTH; col++) { + if (this.state.grid[row][col] !== null) { + this.drawCell(col, row, COLORS[this.state.grid[row][col]]); + } + } + } + + // Draw current piece + if (this.state.currentPiece) { + const shape = this.state.currentPiece.getCurrentShape(); + const color = COLORS[this.state.currentPiece.type]; + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col]) { + const gridX = this.state.currentX + col; + const gridY = this.state.currentY + row; + if (gridY >= 0) { + this.drawCell(gridX, gridY, color); + } + } + } + } + } + } + + drawCell(col, row, color) { + const x = col * CELL_SIZE; + const y = row * CELL_SIZE; + + // Fill cell + this.ctx.fillStyle = color; + this.ctx.fillRect(x + 1, y + 1, CELL_SIZE - 2, CELL_SIZE - 2); + + // Add border/highlight for 3D effect + this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; + this.ctx.lineWidth = 2; + this.ctx.strokeRect(x + 1, y + 1, CELL_SIZE - 2, CELL_SIZE - 2); + } + + renderNextPiece() { + const SIZE = 30; + this.nextCtx.fillStyle = '#1a1a1a'; + this.nextCtx.fillRect(0, 0, this.nextPieceCanvas.width, this.nextPieceCanvas.height); + + if (this.state.nextPiece) { + const shape = this.state.nextPiece.getCurrentShape(); + const color = COLORS[this.state.nextPiece.type]; + const offsetX = (this.nextPieceCanvas.width - shape[0].length * SIZE) / 2; + const offsetY = (this.nextPieceCanvas.height - shape.length * SIZE) / 2; + + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col]) { + const x = offsetX + col * SIZE; + const y = offsetY + row * SIZE; + + this.nextCtx.fillStyle = color; + this.nextCtx.fillRect(x + 1, y + 1, SIZE - 2, SIZE - 2); + + this.nextCtx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; + this.nextCtx.lineWidth = 1; + this.nextCtx.strokeRect(x + 1, y + 1, SIZE - 2, SIZE - 2); + } + } + } + } + } + + gameLoop() { + this.autoDropPiece(); + this.updateDisplay(); + this.render(); + this.renderNextPiece(); + + if (this.state.gameOver) { + this.showGameOver(); + return; + } + + requestAnimationFrame(() => this.gameLoop()); + } + } + + // ===== Initialize Game ===== + let game; + window.addEventListener('DOMContentLoaded', () => { + game = new TetrisGame(); + }); + </script> +</body> +</html> diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/package-lock.json b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/package-lock.json @@ -0,0 +1,984 @@ +{ + "name": "loop-bench-swybie", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "loop-bench-swybie", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.59.1", + "@types/node": "^25.5.2", + "eslint": "^10.1.0", + "typescript": "^6.0.2" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/package.json b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/package.json @@ -0,0 +1,20 @@ +{ + "name": "loop-bench-swybie", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.59.1", + "@types/node": "^25.5.2", + "eslint": "^10.1.0", + "typescript": "^6.0.2" + } +} diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/tetris.html b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=detailed_sub_agents=off_web_search=off_run1/tetris.html @@ -0,0 +1,669 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Tetris Game</title> + <style> + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + } + + .container { + display: flex; + gap: 40px; + background: rgba(255, 255, 255, 0.95); + padding: 30px; + border-radius: 15px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-width: 600px; + } + + .game-area { + display: flex; + flex-direction: column; + gap: 10px; + } + + .game-title { + text-align: center; + font-size: 28px; + font-weight: bold; + color: #333; + margin-bottom: 10px; + } + + canvas { + border: 3px solid #333; + background: #1a1a1a; + display: block; + image-rendering: pixelated; + image-rendering: crisp-edges; + } + + .sidebar { + display: flex; + flex-direction: column; + gap: 30px; + min-width: 150px; + } + + .info-box { + background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%); + padding: 20px; + border-radius: 10px; + border: 2px solid #999; + text-align: center; + } + + .info-label { + font-size: 12px; + font-weight: bold; + color: #666; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 8px; + } + + .info-value { + font-size: 32px; + font-weight: bold; + color: #2196F3; + } + + .next-piece-container { + background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%); + padding: 20px; + border-radius: 10px; + border: 2px solid #999; + } + + .next-piece-label { + font-size: 12px; + font-weight: bold; + color: #666; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 10px; + text-align: center; + } + + .next-piece-canvas { + border: 2px solid #333; + background: #1a1a1a; + display: block; + margin: 0 auto; + } + + .controls { + background: #f9f9f9; + padding: 20px; + border-radius: 10px; + border: 2px solid #ddd; + font-size: 12px; + color: #555; + } + + .controls h3 { + margin-bottom: 10px; + color: #333; + } + + .controls p { + margin: 5px 0; + line-height: 1.5; + } + + .game-over-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: none; + justify-content: center; + align-items: center; + z-index: 1000; + } + + .game-over-box { + background: white; + padding: 40px; + border-radius: 15px; + text-align: center; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + } + + .game-over-box h1 { + font-size: 48px; + color: #e74c3c; + margin-bottom: 20px; + } + + .game-over-box p { + font-size: 24px; + color: #333; + margin-bottom: 10px; + } + + .game-over-box button { + margin-top: 30px; + padding: 15px 40px; + font-size: 18px; + background: #2196F3; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background 0.3s; + } + + .game-over-box button:hover { + background: #1976D2; + } + </style> +</head> +<body> + <div class="game-over-overlay" id="gameOverOverlay"> + <div class="game-over-box"> + <h1>GAME OVER</h1> + <p>Final Score: <span id="finalScore">0</span></p> + <p>Lines: <span id="finalLines">0</span></p> + <p>Level: <span id="finalLevel">0</span></p> + <button onclick="location.reload()">Play Again</button> + </div> + </div> + + <div class="container"> + <div class="game-area"> + <div class="game-title">TETRIS</div> + <canvas id="gameCanvas" width="300" height="600"></canvas> + </div> + <div class="sidebar"> + <div class="info-box"> + <div class="info-label">Score</div> + <div class="info-value" id="scoreDisplay">0</div> + </div> + <div class="info-box"> + <div class="info-label">Level</div> + <div class="info-value" id="levelDisplay">1</div> + </div> + <div class="info-box"> + <div class="info-label">Lines</div> + <div class="info-value" id="linesDisplay">0</div> + </div> + <div class="next-piece-container"> + <div class="next-piece-label">Next Piece</div> + <canvas id="nextPieceCanvas" width="120" height="120"></canvas> + </div> + <div class="controls"> + <h3>Controls</h3> + <p>← → Move</p> + <p>↓ Soft Drop</p> + <p>↑ Rotate CW</p> + <p>Z Rotate CCW</p> + <p>Space Hard Drop</p> + </div> + </div> + </div> + + <script> + // ===== Game Constants ===== + const GRID_WIDTH = 10; + const GRID_HEIGHT = 20; + const CELL_SIZE = 30; + + const COLORS = { + I: '#00f0f0', // Cyan + O: '#f0f000', // Yellow + T: '#a000f0', // Purple + S: '#00f000', // Green + Z: '#f00000', // Red + J: '#0000f0', // Blue + L: '#f0a000', // Orange + empty: '#1a1a1a' + }; + + const PIECE_SHAPES = { + I: [ + [[1, 1, 1, 1]], + ], + O: [ + [[1, 1], [1, 1]] + ], + T: [ + [[0, 1, 0], [1, 1, 1]], + [[1, 0], [1, 1], [1, 0]], + [[1, 1, 1], [0, 1, 0]], + [[0, 1], [1, 1], [0, 1]] + ], + S: [ + [[0, 1, 1], [1, 1, 0]], + [[1, 0], [1, 1], [0, 1]] + ], + Z: [ + [[1, 1, 0], [0, 1, 1]], + [[0, 1], [1, 1], [1, 0]] + ], + J: [ + [[1, 0, 0], [1, 1, 1]], + [[1, 1], [1, 0], [1, 0]], + [[1, 1, 1], [0, 0, 1]], + [[1, 0], [1, 0], [1, 1]] + ], + L: [ + [[0, 0, 1], [1, 1, 1]], + [[1, 0], [1, 0], [1, 1]], + [[1, 1, 1], [1, 0, 0]], + [[1, 1], [0, 1], [0, 1]] + ] + }; + + // ===== Game State ===== + class GameState { + constructor() { + this.grid = Array(GRID_HEIGHT).fill(null).map(() => Array(GRID_WIDTH).fill(null)); + this.currentPiece = null; + this.currentX = 0; + this.currentY = 0; + this.nextPiece = null; + this.score = 0; + this.lines = 0; + this.level = 1; + this.gameOver = false; + this.lastDropTime = Date.now(); + this.dropInterval = this.getDropInterval(); + } + + getDropInterval() { + // Start at 800ms, decrease with level (minimum 100ms) + return Math.max(100, 800 - (this.level - 1) * 50); + } + + updateLevel() { + const newLevel = Math.floor(this.lines / 10) + 1; + if (newLevel !== this.level) { + this.level = newLevel; + this.dropInterval = this.getDropInterval(); + } + } + } + + // ===== Tetromino Class ===== + class Tetromino { + constructor(type) { + this.type = type; + this.rotationIndex = 0; + this.shapes = PIECE_SHAPES[type]; + } + + getCurrentShape() { + return this.shapes[this.rotationIndex]; + } + + rotate(direction) { + const newRotationIndex = direction === 1 + ? (this.rotationIndex + 1) % this.shapes.length + : (this.rotationIndex - 1 + this.shapes.length) % this.shapes.length; + const oldRotationIndex = this.rotationIndex; + this.rotationIndex = newRotationIndex; + return oldRotationIndex !== newRotationIndex; + } + + resetRotation() { + this.rotationIndex = oldRotationIndex; + } + } + + // ===== Game Logic ===== + class TetrisGame { + constructor() { + this.state = new GameState(); + this.canvas = document.getElementById('gameCanvas'); + this.nextPieceCanvas = document.getElementById('nextPieceCanvas'); + this.ctx = this.canvas.getContext('2d'); + this.nextCtx = this.nextPieceCanvas.getContext('2d'); + + this.spawnNewPiece(); + this.render(); + this.setupEventListeners(); + this.gameLoop(); + } + + spawnNewPiece() { + if (this.state.nextPiece === null) { + this.state.nextPiece = this.getRandomPiece(); + } + + this.state.currentPiece = this.state.nextPiece; + this.state.nextPiece = this.getRandomPiece(); + this.state.currentX = Math.floor(GRID_WIDTH / 2) - 1; + this.state.currentY = 0; + + if (!this.canPlacePiece(this.state.currentX, this.state.currentY, this.state.currentPiece)) { + this.state.gameOver = true; + } + } + + getRandomPiece() { + const types = Object.keys(PIECE_SHAPES); + const randomType = types[Math.floor(Math.random() * types.length)]; + return new Tetromino(randomType); + } + + canPlacePiece(x, y, piece) { + const shape = piece.getCurrentShape(); + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col]) { + const gridX = x + col; + const gridY = y + row; + + if (gridX < 0 || gridX >= GRID_WIDTH || gridY >= GRID_HEIGHT) { + return false; + } + + if (gridY >= 0 && this.state.grid[gridY][gridX] !== null) { + return false; + } + } + } + } + return true; + } + + placePiece() { + const shape = this.state.currentPiece.getCurrentShape(); + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col]) { + const gridX = this.state.currentX + col; + const gridY = this.state.currentY + row; + if (gridY >= 0) { + this.state.grid[gridY][gridX] = this.state.currentPiece.type; + } + } + } + } + this.clearLines(); + } + + clearLines() { + let linesCleared = 0; + const linesToRemove = []; + + for (let row = 0; row < GRID_HEIGHT; row++) { + if (this.state.grid[row].every(cell => cell !== null)) { + linesToRemove.push(row); + linesCleared++; + } + } + + if (linesCleared > 0) { + linesToRemove.forEach(row => { + this.state.grid.splice(row, 1); + this.state.grid.unshift(Array(GRID_WIDTH).fill(null)); + }); + + this.state.lines += linesCleared; + this.state.updateLevel(); + + const points = { + 1: 100, + 2: 300, + 3: 500, + 4: 800 + }; + const basePoints = points[linesCleared] || 0; + this.state.score += basePoints * this.state.level; + } + } + + moveLeft() { + if (this.canPlacePiece(this.state.currentX - 1, this.state.currentY, this.state.currentPiece)) { + this.state.currentX--; + } + } + + moveRight() { + if (this.canPlacePiece(this.state.currentX + 1, this.state.currentY, this.state.currentPiece)) { + this.state.currentX++; + } + } + + softDrop() { + if (this.canPlacePiece(this.state.currentX, this.state.currentY + 1, this.state.currentPiece)) { + this.state.currentY++; + } else { + this.placePiece(); + this.spawnNewPiece(); + } + } + + hardDrop() { + while (this.canPlacePiece(this.state.currentX, this.state.currentY + 1, this.state.currentPiece)) { + this.state.currentY++; + } + this.placePiece(); + this.spawnNewPiece(); + } + + rotate(direction) { + if (this.state.currentPiece.type === 'O') return; // O piece doesn't rotate + + this.state.currentPiece.rotate(direction); + + // Try basic placement + if (this.canPlacePiece(this.state.currentX, this.state.currentY, this.state.currentPiece)) { + return; + } + + // Wall kick: try offset positions + const offsets = [[-1, 0], [1, 0], [0, -1]]; + for (const [dx, dy] of offsets) { + if (this.canPlacePiece(this.state.currentX + dx, this.state.currentY + dy, this.state.currentPiece)) { + this.state.currentX += dx; + this.state.currentY += dy; + return; + } + } + + // Revert rotation if no valid position found + this.state.currentPiece.rotate(direction === 1 ? -1 : 1); + } + + setupEventListeners() { + document.addEventListener('keydown', (e) => { + if (this.state.gameOver) return; + + switch (e.key) { + case 'ArrowLeft': + e.preventDefault(); + this.moveLeft(); + break; + case 'ArrowRight': + e.preventDefault(); + this.moveRight(); + break; + case 'ArrowDown': + e.preventDefault(); + this.softDrop(); + break; + case 'ArrowUp': + e.preventDefault(); + this.rotate(1); + break; + case 'z': + case 'Z': + e.preventDefault(); + this.rotate(-1); + break; + case ' ': + e.preventDefault(); + this.hardDrop(); + break; + } + }); + } + + autoDropPiece() { + const now = Date.now(); + if (now - this.state.lastDropTime > this.state.dropInterval) { + this.softDrop(); + this.state.lastDropTime = now; + } + } + + gameLoop() { + this.autoDropPiece(); + this.updateDisplay(); + this.render(); + + if (this.state.gameOver) { + this.showGameOver(); + return; + } + + requestAnimationFrame(() => this.gameLoop()); + } + + updateDisplay() { + document.getElementById('scoreDisplay').textContent = this.state.score; + document.getElementById('levelDisplay').textContent = this.state.level; + document.getElementById('linesDisplay').textContent = this.state.lines; + } + + showGameOver() { + document.getElementById('finalScore').textContent = this.state.score; + document.getElementById('finalLines').textContent = this.state.lines; + document.getElementById('finalLevel').textContent = this.state.level; + document.getElementById('gameOverOverlay').style.display = 'flex'; + } + + render() { + // Clear canvas + this.ctx.fillStyle = COLORS.empty; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + + // Draw grid + this.ctx.strokeStyle = '#333'; + this.ctx.lineWidth = 0.5; + for (let i = 0; i <= GRID_WIDTH; i++) { + this.ctx.beginPath(); + this.ctx.moveTo(i * CELL_SIZE, 0); + this.ctx.lineTo(i * CELL_SIZE, this.canvas.height); + this.ctx.stroke(); + } + for (let i = 0; i <= GRID_HEIGHT; i++) { + this.ctx.beginPath(); + this.ctx.moveTo(0, i * CELL_SIZE); + this.ctx.lineTo(this.canvas.width, i * CELL_SIZE); + this.ctx.stroke(); + } + + // Draw placed blocks + for (let row = 0; row < GRID_HEIGHT; row++) { + for (let col = 0; col < GRID_WIDTH; col++) { + if (this.state.grid[row][col] !== null) { + this.drawCell(col, row, COLORS[this.state.grid[row][col]]); + } + } + } + + // Draw current piece + if (this.state.currentPiece) { + const shape = this.state.currentPiece.getCurrentShape(); + const color = COLORS[this.state.currentPiece.type]; + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col]) { + const gridX = this.state.currentX + col; + const gridY = this.state.currentY + row; + if (gridY >= 0) { + this.drawCell(gridX, gridY, color); + } + } + } + } + } + } + + drawCell(col, row, color) { + const x = col * CELL_SIZE; + const y = row * CELL_SIZE; + + // Fill cell + this.ctx.fillStyle = color; + this.ctx.fillRect(x + 1, y + 1, CELL_SIZE - 2, CELL_SIZE - 2); + + // Add border/highlight for 3D effect + this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; + this.ctx.lineWidth = 2; + this.ctx.strokeRect(x + 1, y + 1, CELL_SIZE - 2, CELL_SIZE - 2); + } + + renderNextPiece() { + const SIZE = 30; + this.nextCtx.fillStyle = '#1a1a1a'; + this.nextCtx.fillRect(0, 0, this.nextPieceCanvas.width, this.nextPieceCanvas.height); + + if (this.state.nextPiece) { + const shape = this.state.nextPiece.getCurrentShape(); + const color = COLORS[this.state.nextPiece.type]; + const offsetX = (this.nextPieceCanvas.width - shape[0].length * SIZE) / 2; + const offsetY = (this.nextPieceCanvas.height - shape.length * SIZE) / 2; + + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col]) { + const x = offsetX + col * SIZE; + const y = offsetY + row * SIZE; + + this.nextCtx.fillStyle = color; + this.nextCtx.fillRect(x + 1, y + 1, SIZE - 2, SIZE - 2); + + this.nextCtx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; + this.nextCtx.lineWidth = 1; + this.nextCtx.strokeRect(x + 1, y + 1, SIZE - 2, SIZE - 2); + } + } + } + } + } + + gameLoop() { + this.autoDropPiece(); + this.updateDisplay(); + this.render(); + this.renderNextPiece(); + + if (this.state.gameOver) { + this.showGameOver(); + return; + } + + requestAnimationFrame(() => this.gameLoop()); + } + } + + // ===== Initialize Game ===== + let game; + window.addEventListener('DOMContentLoaded', () => { + game = new TetrisGame(); + }); + </script> +</body> +</html> diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/FEATURES.md b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/FEATURES.md @@ -0,0 +1,190 @@ +# 🎮 Tetris Game - Complete Feature List + +## ✅ Core Game Mechanics Implemented + +### Piece System +- [x] **All 7 Tetromino pieces**: I-piece, O-piece, T-piece, S-piece, Z-piece, J-piece, L-piece +- [x] **Piece rotation**: Full 360-degree rotation with multiple rotation states per piece +- [x] **Wall kick system**: Automatic adjustment when pieces hit walls during rotation +- [x] **Piece preview**: Next piece shown in side panel +- [x] **Spawn system**: New pieces spawn at top center + +### Movement Controls +- [x] **Horizontal movement**: Left/Right arrow keys or A/D +- [x] **Soft drop**: Down arrow or S key (controlled descent) +- [x] **Hard drop**: SPACE bar (instant placement) +- [x] **Smooth collision detection**: Prevents pieces from passing through walls or other blocks +- [x] **Natural falling**: Automatic gravity pulling pieces down at increasing speeds + +### Board & Grid +- [x] **10×20 standard board**: Classic Tetris dimensions +- [x] **Block locking**: Pieces become permanent when reaching bottom or other blocks +- [x] **Visual grid**: Clear cell-based rendering +- [x] **Color-coded pieces**: Each tetromino has unique color for easy recognition + +### Line Clearing +- [x] **Full line detection**: Automatically detects when a horizontal line is complete +- [x] **Line removal**: Deletes completed lines +- [x] **Cascading**: Upper blocks fall down after line clears +- [x] **Multiple line clearing**: Supports clearing 1-4 lines simultaneously +- [x] **Bonus scoring**: Extra points for clearing multiple lines at once (Tetris = 800 points!) + +### Scoring System +- [x] **Points calculation**: Based on lines cleared and current level + - 1 line: 100 × level + - 2 lines: 300 × level + - 3 lines: 500 × level + - 4 lines: 800 × level (Tetris bonus!) +- [x] **Score tracking**: Real-time score display +- [x] **Final score**: Shown on game over screen + +### Level & Difficulty +- [x] **Level progression**: New level every 10 lines cleared +- [x] **Speed scaling**: Drop speed increases with level + - Level 1: 800ms/drop + - Level 2: 750ms/drop + - Continues until minimum speed (100ms) +- [x] **Line counter**: Tracks total lines cleared +- [x] **Level display**: Shows current level in UI + +### Game State Management +- [x] **Game over detection**: Detects when pieces stack above board +- [x] **Game over screen**: Shows final score with option to restart +- [x] **Pause functionality**: P key to pause/resume +- [x] **Pause overlay**: Shows "PAUSED" message +- [x] **Game initialization**: Automatic start on page load + +## 🎨 Visual Features + +### UI Elements +- [x] **Game board display**: 10×20 grid with visible cells +- [x] **Score display**: Large, clear score number +- [x] **Level indicator**: Shows current level +- [x] **Lines counter**: Total lines cleared +- [x] **Next piece preview**: 4×4 grid showing upcoming piece +- [x] **Control guide**: On-screen control reference +- [x] **Game title**: "TETRIS" header +- [x] **Game over modal**: Professional overlay with final score +- [x] **Pause modal**: Clear pause state indicator + +### Visual Design +- [x] **Color scheme**: Purple gradient background +- [x] **Dark board**: #1a1a1a background with grid pattern +- [x] **Piece colors**: + - I-piece: Cyan (#00BCD4) + - O-piece: Yellow (#FFEB3B) + - T-piece: Purple (#9C27B0) + - S-piece: Green (#4CAF50) + - Z-piece: Red (#FF5252) + - J-piece: Blue (#2196F3) + - L-piece: Orange (#FF9800) +- [x] **Responsive design**: Works on different screen sizes +- [x] **Smooth animations**: Transitions and visual feedback +- [x] **Professional styling**: Box shadows, borders, gradients + +## ⌨️ Input Controls + +### Keyboard Bindings +``` +Arrow Up or W → Rotate piece clockwise +Arrow Left or A → Move piece left +Arrow Right or D → Move piece right +Arrow Down or S → Soft drop (faster descent) +SPACE → Hard drop (instant placement) +P → Pause/Resume game +``` + +### Input Features +- [x] **Multiple key bindings**: Arrow keys and WASD support +- [x] **Key detection**: Real-time keyboard input +- [x] **Prevention of default behavior**: Custom key handling +- [x] **Responsive controls**: Immediate piece response to input +- [x] **No input lag**: Direct action on key press + +## 🛠️ Technical Implementation + +### Architecture +- [x] **TypeScript source**: Fully typed game code +- [x] **Class-based design**: `TetrisGame` class managing all state +- [x] **Type interfaces**: Typed piece and board structures +- [x] **Modular methods**: Separate functions for each mechanic +- [x] **Event-driven**: Keyboard events trigger game logic + +### Compilation +- [x] **TypeScript → JavaScript**: Compiled to ES2020 +- [x] **No external dependencies**: Pure vanilla JavaScript in browser +- [x] **Small bundle**: Minimal file sizes (~11 KB JS, 8 KB HTML) +- [x] **Ready to deploy**: No build process needed after compilation + +### Performance +- [x] **60 FPS rendering**: Frame rate limiting in render function +- [x] **Efficient DOM updates**: Only update changed elements +- [x] **Game loop optimization**: setInterval for timing +- [x] **Collision detection**: O(n) algorithm checking cells +- [x] **Memory efficiency**: Reuses piece objects + +### Code Quality +- [x] **Well-commented**: Clear explanations in code +- [x] **Consistent naming**: Clear variable and function names +- [x] **Separated concerns**: Game logic separate from rendering +- [x] **Error handling**: Game over detection and handling +- [x] **Type safety**: TypeScript provides compile-time checking + +## 📊 Game Features Summary + +| Feature | Status | Details | +|---------|--------|---------| +| Piece Rotation | ✅ | Full 4-directional with wall kick | +| Line Clearing | ✅ | 1-4 simultaneous lines | +| Scoring | ✅ | Multiplied by level | +| Level System | ✅ | Every 10 lines, speed increases | +| Speed Increase | ✅ | Reaches minimum cap at high levels | +| Pause/Resume | ✅ | P key with overlay indicator | +| Game Over | ✅ | Detection and score display | +| Next Piece Preview | ✅ | Shows upcoming piece in side panel | +| Keyboard Controls | ✅ | Arrow keys + WASD + Space + P | +| Collision Detection | ✅ | Walls, floor, other pieces | +| Board Reset | ✅ | New game on reload | +| Sound Effects | ❌ | Not implemented (can be added) | +| Touch Controls | ❌ | Not implemented (can be added) | +| High Score Save | ❌ | Not implemented (can be added) | +| Multiplayer | ❌ | Not implemented (can be added) | + +## 🎯 Game Quality Metrics + +- **Code Coverage**: Core gameplay 100% +- **Bug-Free**: No known issues +- **Playability**: Fully playable and fun +- **Performance**: Smooth 60 FPS gameplay +- **Compatibility**: Works on modern browsers +- **Documentation**: Complete with examples + +## 📚 Documentation Provided + +- [x] **README.md**: Complete user guide +- [x] **IMPLEMENTATION.md**: Technical architecture details +- [x] **QUICK_START.txt**: Getting started guide +- [x] **FEATURES.md**: This file - complete feature list +- [x] **Inline comments**: Code is well-commented +- [x] **TSConfig**: Compiler configuration documented + +## 🚀 Ready for Deployment + +The game is production-ready and can be: +- [x] Deployed to static hosting (GitHub Pages, Netlify, etc.) +- [x] Run locally with any HTTP server +- [x] Embedded in websites +- [x] Modified and extended easily +- [x] Used as educational material for game development + +## 🎓 Educational Value + +This implementation teaches: +- TypeScript fundamentals and type systems +- Game development core concepts (collision, rendering, game loop) +- DOM manipulation and event handling +- Algorithm design (collision detection, line clearing) +- Software architecture and class design +- Browser-based game development + +Enjoy! 🎮 diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/IMPLEMENTATION.md b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/IMPLEMENTATION.md @@ -0,0 +1,266 @@ +# Tetris Game - Implementation Details + +## Architecture Overview + +This Tetris game is built with a clean TypeScript implementation featuring: +- **Class-based OOP design** with `TetrisGame` class +- **Type-safe interfaces** for game pieces +- **Event-driven control** system +- **Real-time rendering** with DOM updates +- **Game state management** (score, level, lines) + +## File Breakdown + +### 1. **index.html** (8.0 KB) +The complete HTML structure and styling: +- **Game board**: 10×20 cell grid +- **Info panel**: Score, level, lines, next piece preview +- **Game overlays**: Game Over and Pause screens +- **Responsive design**: Works on desktop and mobile +- **Color scheme**: Purple gradient background with grid-based styling + +### 2. **tetris.ts** (12 KB - TypeScript Source) +Main game logic in TypeScript: + +#### Core Components: +- **Piece Definitions**: All 7 tetromino shapes with rotations +- **TetrisGame Class**: Main game controller +- **Constants**: Board dimensions (10×20), drop speeds, scoring values + +#### Key Methods: +```typescript +// Game State Management +createEmptyBoard() // Initialize empty 10×20 grid +spawnNextPiece() // Spawn piece from queue + +// Piece Movement & Rotation +movePiece(dx, dy) // Move piece (collision detection) +rotatePiece() // Rotate piece (with wall kick) +hardDrop() // Instant drop to bottom +lockPiece() // Place piece on board + +// Game Logic +clearLines() // Check and clear completed lines +drop() // Auto-drop (called by timer) +isPositionValid() // Collision detection algorithm + +// Rendering +render() // Update game board display +renderNextPiece() // Show preview of next piece + +// Input Handling +handleKeyDown() // Keyboard event processing +togglePause() // Pause/resume game +``` + +### 3. **tetris.js** (11 KB - Compiled JavaScript) +Compiled output from TypeScript, ready for browser execution: +- ES2020 target with DOM library +- No external dependencies +- Fully self-contained + +### 4. **server.js** (1.2 KB) +Simple Node.js HTTP server: +- Serves HTML, JS, CSS files +- Content-type detection +- Security: Directory traversal prevention +- Runs on port 8080 + +### 5. **tsconfig.json** (301 bytes) +TypeScript compiler configuration: +- Target: ES2020 +- Libraries: ES2020, DOM +- Strict type checking: off (for compatibility) +- Output in same directory + +## Game Mechanics + +### Piece System +- **7 Tetrominoes**: I, O, T, S, Z, J, L +- **Rotation states**: 2-4 rotations per piece +- **Spawn location**: Top center (column 4-5) +- **Collision detection**: Against walls, ground, other pieces + +### Scoring Algorithm +``` +Lines Cleared | Base Points | Final Score + 1 | 100 | 100 × Level + 2 | 300 | 300 × Level + 3 | 500 | 500 × Level + 4 | 800 | 800 × Level (Tetris!) +``` + +### Level System +- **Level threshold**: Every 10 lines cleared +- **Speed progression**: + - Level 1: 800ms/drop + - Level 2: 750ms/drop + - Level N: 800ms - (N-1)×50ms + - Minimum: 100ms/drop + +### Game Over Condition +- Piece cannot spawn due to board obstruction +- Triggered when `isPositionValid()` returns false for new piece + +## Keyboard Controls Implementation + +```typescript +handleKeyDown(key: string) { + switch(key) { + case 'arrowup' | 'w' → rotatePiece() + case 'arrowleft' | 'a' → movePiece(-1, 0) + case 'arrowright' | 'd' → movePiece(1, 0) + case 'arrowdown' | 's' → drop() / softDrop() + case ' ' → hardDrop() + case 'p' → togglePause() + } +} +``` + +## Collision Detection Algorithm + +The core collision detection checks three conditions: +1. **Out of bounds**: X < 0 or X >= 10, Y >= 20 +2. **Occupied cell**: board[Y][X] !== null +3. **Below valid area**: Y < 0 allowed (piece can spawn above board) + +```typescript +isPositionValid(piece: Piece): boolean { + const shape = this.getShape(piece); + + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col] === 0) continue; // Skip empty cells + + const x = piece.x + col; + const y = piece.y + row; + + // Check boundaries + if (x < 0 || x >= BOARD_WIDTH || y >= BOARD_HEIGHT) return false; + + // Check occupied cells + if (y >= 0 && board[y][x] !== null) return false; + } + } + + return true; +} +``` + +## Rotation & Wall Kick System + +When rotating fails (collision), the system tries "wall kicks": +- Offset by -1, +1, -2, +2 positions +- If any offset succeeds, piece rotates with offset +- If all fail, rotation is cancelled + +```typescript +rotatePiece() { + const oldRotation = this.currentPiece.rotation; + this.currentPiece.rotation = (rotation + 1) % shapes.length; + + if (!isPositionValid(this.currentPiece)) { + // Try wall kicks: [-1, 1, -2, 2] + for (const kick of [-1, 1, -2, 2]) { + this.currentPiece.x += kick; + if (isPositionValid(this.currentPiece)) return; // Success! + } + + // All failed, revert + this.currentPiece.rotation = oldRotation; + } +} +``` + +## Line Clearing Algorithm + +```typescript +clearLines() { + const linesToClear = []; + + // Find complete lines + for (let row = 0; row < BOARD_HEIGHT; row++) { + if (board[row].every(cell => cell !== null)) { + linesToClear.push(row); + } + } + + // Remove and add new empty lines + for (let i = linesToClear.length - 1; i >= 0; i--) { + board.splice(linesToClear[i], 1); + board.unshift(Array(BOARD_WIDTH).fill(null)); + } + + // Update score and level + lines += linesToClear.length; + score += points[linesToClear.length] * level; +} +``` + +## Rendering Pipeline + +The render function updates the DOM in this order: +1. **Clear board**: Remove all child elements +2. **Draw locked pieces**: Iterate through board array, create cells +3. **Draw current piece**: Overlay current piece on top +4. **Update UI**: Score, level, lines, next piece preview +5. **Frame limiting**: ~60 FPS using requestAnimationFrame concept + +## Performance Optimizations + +1. **Frame rate limiting**: Only render if 16ms+ passed (60 FPS cap) +2. **Selective DOM updates**: Only when game state changes +3. **Event delegation**: Single event listener for keyboard +4. **Timer optimization**: setInterval for game loop (not recursive) + +## Browser Compatibility + +- **ES2020 features used**: + - Arrow functions + - Template literals + - Spread operator + - Destructuring + - Class syntax + - DOM APIs: querySelector, addEventListener, innerHTML + +- **Tested on**: + - Chrome/Edge (Chromium-based) ✓ + - Firefox ✓ + - Safari ✓ + +## Future Enhancement Ideas + +1. **Touch controls** for mobile +2. **Sound effects** for line clears +3. **Animation** for falling pieces +4. **Local storage** for high scores +5. **Multiplayer** via WebSocket +6. **Themes** for visual customization +7. **Gravity** physics simulation +8. **Undo** system for learning + +## Code Statistics + +| File | Lines | Type | +|------|-------|------| +| index.html | 220 | HTML + CSS | +| tetris.ts | 305 | TypeScript | +| tetris.js | 380 | JavaScript (compiled) | +| server.js | 30 | Node.js | +| **Total** | **~335** | **Active Code** | + +## Deployment + +To run the game: +```bash +node server.js +# Open http://localhost:8080 +``` + +Or with Python: +```bash +python3 -m http.server 8080 +# Open http://localhost:8080 +``` + +The game requires no build step beyond TypeScript compilation, making it easy to deploy anywhere that serves static files. diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/QUICK_START.txt b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/QUICK_START.txt @@ -0,0 +1,105 @@ +╔════════════════════════════════════════════════════════════════════════════╗ +║ 🎮 TETRIS GAME - QUICK START GUIDE ║ +╚════════════════════════════════════════════════════════════════════════════╝ + +✓ STATUS: Server is running at http://localhost:8080 + +📋 FILES CREATED: + • index.html - Game UI (HTML + CSS) + • tetris.ts - Game logic (TypeScript source) + • tetris.js - Compiled JavaScript (ready for browser) + • server.js - HTTP server to serve files + • tsconfig.json - TypeScript compiler config + • README.md - Full documentation + • IMPLEMENTATION.md - Technical details + +🎮 HOW TO PLAY: + +1. Open your browser and go to: http://localhost:8080 + +2. Game automatically starts with a falling piece + +3. Controls: + ↑ or W = Rotate piece clockwise + ← or A = Move piece left + → or D = Move piece right + ↓ or S = Soft drop (piece falls faster) + SPACE = Hard drop (instant placement) + P = Pause/Resume game + +⭐ GAME MECHANICS: + +• Lines: Clear complete horizontal lines for points +• Scoring: 100-800 points per line depending on count +• Levels: New level every 10 lines cleared +• Speed: Game gets faster as you level up +• Game Over: When pieces stack to the top + +🏆 SCORING BREAKDOWN: + +Single Line (1): 100 × level points +Double Lines (2): 300 × level points +Triple Lines (3): 500 × level points +Tetris (4 lines): 800 × level points (best!) + +⚙️ TECHNICAL DETAILS: + +• Built with TypeScript (compiled to ES2020 JavaScript) +• No external dependencies (pure vanilla JS) +• 10×20 board (standard Tetris dimensions) +• 7 piece types (I, O, T, S, Z, J, L) +• Wall kick system for rotation +• Collision detection algorithm +• Real-time rendering with DOM updates + +📁 PROJECT STRUCTURE: + +loop-bench-3VKhwb/ +├── index.html (Game UI) +├── tetris.ts (TypeScript source) +├── tetris.js (Compiled JavaScript) +├── server.js (Node.js server) +├── tsconfig.json (TS config) +├── README.md (Documentation) +├── IMPLEMENTATION.md (Technical docs) +└── QUICK_START.txt (This file) + +🔧 IF YOU WANT TO MODIFY THE GAME: + +1. Edit tetris.ts +2. Recompile: tsc tetris.ts --target es2020 --lib es2020,dom +3. Refresh your browser + +💡 TIPS FOR PLAYING: + +• Watch the "Next Piece" preview to plan ahead +• Clear lines regularly to avoid topping out +• Use hard drop (SPACE) for faster gameplay +• I-pieces are best for clearing multiple lines +• Try to maintain gaps in your structure + +🐛 TROUBLESHOOTING: + +If the game doesn't load: + 1. Make sure server is running: node server.js + 2. Check browser console (F12) for errors + 3. Try a different browser (Chrome, Firefox, Safari) + 4. Clear browser cache (Ctrl+Shift+Delete) + +If pieces move strangely: + • This is normal in the first few seconds as the game initializes + • The collision detection will stabilize immediately + +🎯 CHALLENGE MODES: + +Try these while playing: + • Score 1000 points + • Reach level 5 + • Clear a Tetris (4 lines at once) + • Last 30 seconds without piece overlaps + +═══════════════════════════════════════════════════════════════════════════════ + +Ready to play? Open http://localhost:8080 in your browser! + +Good luck! 🍀 diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/README.md b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/README.md @@ -0,0 +1,166 @@ +# 🎮 Tetris Game - TypeScript Browser Implementation + +A fully functional, playable Tetris game built with TypeScript and compiled to JavaScript for browser play. + +## Features + +### Core Mechanics +- ✅ **All 7 Tetromino Pieces** (I, O, T, S, Z, J, L) +- ✅ **Piece Rotation** with wall kick detection +- ✅ **Line Clearing** with appropriate scoring +- ✅ **Soft Drop** (slow descent) +- ✅ **Hard Drop** (instant placement) +- ✅ **Game Speed Progression** - increases with level +- ✅ **Pause/Resume** functionality +- ✅ **Game Over Detection** + +### Scoring System +- Single line: 100 × level points +- Double line: 300 × level points +- Triple line: 500 × level points +- Tetris (4 lines): 800 × level points + +### Level Progression +- New level every 10 lines cleared +- Drop speed increases with each level +- Minimum speed cap to ensure playability + +### Visual Features +- Color-coded pieces (each tetromino has unique color) +- Smooth animations +- Next piece preview +- Real-time score, level, and line count display +- Game over overlay with final score +- Pause overlay + +## Controls + +| Key | Action | +|-----|--------| +| ↑ / W | Rotate piece | +| ← / A | Move left | +| → / D | Move right | +| ↓ / S | Soft drop (faster descent) | +| SPACE | Hard drop (instant placement) | +| P | Pause/Resume | + +## Installation & Running + +### Option 1: Using Node.js Server +```bash +cd /tmp/loop-bench-3VKhwb +node server.js +``` +Then open your browser to `http://localhost:8080` + +### Option 2: Using Python Server +```bash +cd /tmp/loop-bench-3VKhwb +python3 -m http.server 8080 +``` +Then open your browser to `http://localhost:8080` + +### Option 3: Using Live Server (VS Code Extension) +1. Install the Live Server extension in VS Code +2. Right-click on `index.html` and select "Open with Live Server" + +## Project Structure + +``` +├── index.html - Main HTML structure and styling +├── tetris.ts - TypeScript source code (game logic) +├── tetris.js - Compiled JavaScript (compiled from TypeScript) +├── tsconfig.json - TypeScript configuration +├── server.js - Simple Node.js HTTP server +└── README.md - This file +``` + +## Technical Details + +### TypeScript Implementation +- **Class-based design** with `TetrisGame` class managing all game state +- **Type safety** with TypeScript interfaces for game pieces +- **Modular functions** for collision detection, piece locking, line clearing + +### Game Loop +- **Drop timer**: Updates game state at regular intervals +- **Render function**: Updates DOM to reflect current game state +- **Event listeners**: Handle keyboard input for player actions + +### Piece System +- Each piece has 7 rotations defined in `TETRIS_PIECES` +- Rotation validation with wall kick system +- Collision detection for walls, ground, and other pieces + +### Board System +- 10×20 grid (standard Tetris dimensions) +- Board stored as 2D array with null/type values +- Current piece rendered on top of locked pieces + +## Compilation + +The TypeScript source has been compiled to JavaScript: + +```bash +tsc tetris.ts --target es2020 --lib es2020,dom +``` + +The compiled `tetris.js` is fully self-contained and requires no build tools to run. + +## Browser Support + +- Chrome/Chromium (recommended) +- Firefox +- Safari +- Edge +- Any modern browser with ES2020 support + +## Game Mechanics + +### How Pieces Spawn +- Pieces appear at the top center of the board +- Game over if a new piece can't be spawned (collision) + +### How Lines Clear +- A line clears when all 10 cells are filled +- Multiple lines can clear simultaneously +- Score multiplier applies based on lines cleared + +### How Speed Increases +- Level 1: 800ms drop interval +- Each level: -50ms (minimum 100ms) +- Drop speed increases difficulty over time + +## Tips for Playing + +1. **Plan ahead** - Watch the next piece in the preview +2. **Build efficiently** - Try to clear lines regularly +3. **Use hard drop** - SPACE bar places pieces instantly for faster gameplay +4. **Wall kick saves** - Rotation has wall kick, meaning pieces can rotate near walls +5. **Build gaps** - Leave strategic gaps for I-pieces (they clear 4 lines easily) + +## Development + +To modify and recompile the TypeScript: + +```bash +# Make changes to tetris.ts +tsc tetris.ts --target es2020 --lib es2020,dom + +# Then refresh your browser to see changes +``` + +### Key Classes and Methods + +- `TetrisGame`: Main game class + - `drop()`: Move piece down + - `rotatePiece()`: Rotate current piece + - `lockPiece()`: Lock piece to board + - `clearLines()`: Handle line clearing and scoring + - `handleKeyDown()`: Process keyboard input + +## License + +Free to use and modify. + +Enjoy the game! 🎮 diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/index.html b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/index.html @@ -0,0 +1,305 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Tetris Game</title> + <style> + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + font-family: 'Arial', sans-serif; + } + + .game-container { + display: flex; + gap: 30px; + padding: 20px; + } + + .game-board-wrapper { + display: flex; + flex-direction: column; + gap: 15px; + } + + .game-title { + color: white; + font-size: 36px; + font-weight: bold; + text-align: center; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); + } + + .game-board { + display: grid; + grid-template-columns: repeat(10, 30px); + grid-template-rows: repeat(20, 30px); + gap: 1px; + background: #1a1a1a; + padding: 10px; + border: 3px solid #fff; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + } + + .cell { + width: 30px; + height: 30px; + background: #222; + border: 1px solid #333; + transition: background-color 0.1s; + } + + .cell.empty { + background: #222; + } + + .cell.filled { + background: #4CAF50; + box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.5); + } + + .cell.i { background: #00BCD4; } + .cell.o { background: #FFEB3B; } + .cell.t { background: #9C27B0; } + .cell.s { background: #4CAF50; } + .cell.z { background: #FF5252; } + .cell.j { background: #2196F3; } + .cell.l { background: #FF9800; } + + .info-panel { + display: flex; + flex-direction: column; + gap: 20px; + color: white; + } + + .info-section { + background: rgba(0, 0, 0, 0.3); + padding: 15px; + border-radius: 8px; + border: 2px solid rgba(255, 255, 255, 0.2); + } + + .info-label { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 2px; + color: #aaa; + margin-bottom: 8px; + } + + .info-value { + font-size: 28px; + font-weight: bold; + color: #fff; + font-family: 'Courier New', monospace; + } + + .next-piece { + display: grid; + grid-template-columns: repeat(4, 25px); + grid-template-rows: repeat(4, 25px); + gap: 2px; + padding: 10px; + background: #1a1a1a; + border-radius: 4px; + } + + .next-piece .cell { + width: 25px; + height: 25px; + border: 1px solid #333; + } + + .controls { + margin-top: 15px; + padding: 15px; + background: rgba(0, 0, 0, 0.3); + border-radius: 8px; + border: 2px solid rgba(255, 255, 255, 0.2); + } + + .controls-title { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 2px; + color: #aaa; + margin-bottom: 10px; + } + + .control-item { + color: #ddd; + font-size: 13px; + line-height: 1.8; + } + + .control-key { + background: #333; + padding: 3px 8px; + border-radius: 3px; + font-weight: bold; + font-family: 'Courier New', monospace; + margin-right: 8px; + } + + .game-over { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.95); + color: white; + padding: 40px; + border-radius: 10px; + text-align: center; + z-index: 1000; + display: none; + min-width: 300px; + box-shadow: 0 0 40px rgba(0, 0, 0, 0.8); + } + + .game-over.show { + display: block; + } + + .game-over h2 { + font-size: 36px; + margin-bottom: 20px; + color: #ff5252; + } + + .game-over p { + font-size: 18px; + margin-bottom: 10px; + } + + .game-over .final-score { + font-size: 32px; + font-weight: bold; + color: #4CAF50; + margin: 20px 0; + } + + .game-over button { + background: #4CAF50; + color: white; + border: none; + padding: 12px 30px; + font-size: 16px; + border-radius: 5px; + cursor: pointer; + margin-top: 20px; + transition: background 0.3s; + } + + .game-over button:hover { + background: #45a049; + } + + .paused { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.95); + color: white; + padding: 40px; + border-radius: 10px; + text-align: center; + z-index: 999; + display: none; + min-width: 300px; + } + + .paused.show { + display: block; + } + + .paused h2 { + font-size: 32px; + margin-bottom: 20px; + color: #FFD700; + } + + @media (max-width: 768px) { + .game-container { + flex-direction: column; + gap: 20px; + } + + .game-board { + grid-template-columns: repeat(10, 25px); + grid-template-rows: repeat(20, 25px); + } + + .cell { + width: 25px; + height: 25px; + } + } + </style> +</head> +<body> + <div class="game-container"> + <div class="game-board-wrapper"> + <h1 class="game-title">TETRIS</h1> + <div class="game-board" id="gameBoard"></div> + </div> + + <div class="info-panel"> + <div class="info-section"> + <div class="info-label">Score</div> + <div class="info-value" id="score">0</div> + </div> + + <div class="info-section"> + <div class="info-label">Level</div> + <div class="info-value" id="level">1</div> + </div> + + <div class="info-section"> + <div class="info-label">Lines</div> + <div class="info-value" id="lines">0</div> + </div> + + <div class="info-section"> + <div class="info-label">Next Piece</div> + <div class="next-piece" id="nextPiece"></div> + </div> + + <div class="controls"> + <div class="controls-title">Controls</div> + <div class="control-item"><span class="control-key">↑</span> Rotate</div> + <div class="control-item"><span class="control-key">←</span><span class="control-key">→</span> Move</div> + <div class="control-item"><span class="control-key">↓</span> Soft Drop</div> + <div class="control-item"><span class="control-key">SPACE</span> Hard Drop</div> + <div class="control-item"><span class="control-key">P</span> Pause</div> + </div> + </div> + </div> + + <div class="game-over" id="gameOverOverlay"> + <h2>GAME OVER</h2> + <p>Your game has ended</p> + <div class="final-score" id="finalScore">0</div> + <button onclick="location.reload()">Play Again</button> + </div> + + <div class="paused" id="pausedOverlay"> + <h2>PAUSED</h2> + <p>Press P to resume</p> + </div> + + <script src="tetris.js"></script> +</body> +</html> diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/package-lock.json b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/package-lock.json @@ -0,0 +1,984 @@ +{ + "name": "loop-bench-3vkhwb", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "loop-bench-3vkhwb", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.59.1", + "@types/node": "^25.5.2", + "eslint": "^10.1.0", + "typescript": "^6.0.2" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/package.json b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/package.json @@ -0,0 +1,20 @@ +{ + "name": "loop-bench-3vkhwb", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.59.1", + "@types/node": "^25.5.2", + "eslint": "^10.1.0", + "typescript": "^6.0.2" + } +} diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/server.js b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/server.js @@ -0,0 +1,44 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const PORT = 8080; + +const server = http.createServer((req, res) => { + let filePath = path.join(__dirname, req.url === '/' ? 'index.html' : req.url); + + // Security: prevent directory traversal + if (!filePath.startsWith(__dirname)) { + res.statusCode = 403; + res.end('Forbidden'); + return; + } + + const ext = path.extname(filePath); + const contentTypes = { + '.html': 'text/html', + '.js': 'application/javascript', + '.json': 'application/json', + '.css': 'text/css' + }; + + const contentType = contentTypes[ext] || 'text/plain'; + + fs.readFile(filePath, (err, content) => { + if (err) { + res.statusCode = 404; + res.setHeader('Content-Type', 'text/plain'); + res.end('404 Not Found'); + return; + } + + res.statusCode = 200; + res.setHeader('Content-Type', contentType); + res.end(content); + }); +}); + +server.listen(PORT, () => { + console.log(`🎮 Tetris Game Server running at http://localhost:${PORT}`); + console.log('Press Ctrl+C to stop the server'); +}); diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/tetris.js b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/tetris.js @@ -0,0 +1,339 @@ +// Tetris Game Implementation in TypeScript +const TETRIS_PIECES = { + i: [ + [[1, 1, 1, 1]], + [[1], [1], [1], [1]] + ], + o: [ + [[1, 1], [1, 1]] + ], + t: [ + [[0, 1, 0], [1, 1, 1]], + [[1, 0], [1, 1], [1, 0]], + [[1, 1, 1], [0, 1, 0]], + [[0, 1], [1, 1], [0, 1]] + ], + s: [ + [[0, 1, 1], [1, 1, 0]], + [[1, 0], [1, 1], [0, 1]] + ], + z: [ + [[1, 1, 0], [0, 1, 1]], + [[0, 1], [1, 1], [1, 0]] + ], + j: [ + [[1, 0, 0], [1, 1, 1]], + [[1, 1], [1, 0], [1, 0]], + [[1, 1, 1], [0, 0, 1]], + [[0, 1], [0, 1], [1, 1]] + ], + l: [ + [[0, 0, 1], [1, 1, 1]], + [[1, 0], [1, 0], [1, 1]], + [[1, 1, 1], [1, 0, 0]], + [[1, 1], [0, 1], [0, 1]] + ] +}; +const BOARD_WIDTH = 10; +const BOARD_HEIGHT = 20; +const INITIAL_DROP_SPEED = 800; +const MIN_DROP_SPEED = 100; +class TetrisGame { + constructor() { + this.currentPiece = null; + this.nextPiece = null; + this.score = 0; + this.lines = 0; + this.level = 1; + this.dropSpeed = INITIAL_DROP_SPEED; + this.gameOver = false; + this.paused = false; + this.dropTimer = null; + this.lastRenderTime = 0; + this.board = this.createEmptyBoard(); + this.nextPiece = this.createRandomPiece(); + this.spawnNextPiece(); + this.startGame(); + } + createEmptyBoard() { + return Array(BOARD_HEIGHT) + .fill(null) + .map(() => Array(BOARD_WIDTH).fill(null)); + } + createRandomPiece() { + const types = ['i', 'o', 't', 's', 'z', 'j', 'l']; + const type = types[Math.floor(Math.random() * types.length)]; + const shapes = TETRIS_PIECES[type]; + return { + type, + x: Math.floor(BOARD_WIDTH / 2) - 1, + y: 0, + rotation: 0, + shape: shapes[0] + }; + } + spawnNextPiece() { + this.currentPiece = this.nextPiece; + this.nextPiece = this.createRandomPiece(); + if (this.currentPiece && !this.isPositionValid(this.currentPiece)) { + this.endGame(); + } + this.render(); + } + getShape(piece) { + const shapes = TETRIS_PIECES[piece.type]; + return shapes[piece.rotation % shapes.length]; + } + isPositionValid(piece) { + const shape = this.getShape(piece); + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col] === 0) + continue; + const x = piece.x + col; + const y = piece.y + row; + if (x < 0 || x >= BOARD_WIDTH || y >= BOARD_HEIGHT) { + return false; + } + if (y >= 0 && this.board[y][x] !== null) { + return false; + } + } + } + return true; + } + movePiece(dx, dy) { + if (!this.currentPiece) + return false; + const newPiece = { ...this.currentPiece, x: this.currentPiece.x + dx, y: this.currentPiece.y + dy }; + if (this.isPositionValid(newPiece)) { + this.currentPiece = newPiece; + return true; + } + return false; + } + rotatePiece() { + if (!this.currentPiece) + return; + const shapes = TETRIS_PIECES[this.currentPiece.type]; + if (shapes.length === 1) + return; // Can't rotate square piece + const oldRotation = this.currentPiece.rotation; + this.currentPiece.rotation = (this.currentPiece.rotation + 1) % shapes.length; + if (!this.isPositionValid(this.currentPiece)) { + // Try wall kick + const kicks = [-1, 1, -2, 2]; + let rotated = false; + for (const kick of kicks) { + this.currentPiece.x += kick; + if (this.isPositionValid(this.currentPiece)) { + rotated = true; + break; + } + } + if (!rotated) { + this.currentPiece.rotation = oldRotation; + } + } + } + hardDrop() { + if (!this.currentPiece) + return; + while (this.movePiece(0, 1)) { + // Keep moving down until collision + } + this.lockPiece(); + } + lockPiece() { + if (!this.currentPiece) + return; + const shape = this.getShape(this.currentPiece); + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col] === 0) + continue; + const x = this.currentPiece.x + col; + const y = this.currentPiece.y + row; + if (y >= 0) { + this.board[y][x] = this.currentPiece.type; + } + } + } + this.clearLines(); + this.spawnNextPiece(); + } + clearLines() { + const linesToClear = []; + for (let row = 0; row < BOARD_HEIGHT; row++) { + if (this.board[row].every(cell => cell !== null)) { + linesToClear.push(row); + } + } + if (linesToClear.length === 0) + return; + // Remove cleared lines + for (let i = linesToClear.length - 1; i >= 0; i--) { + this.board.splice(linesToClear[i], 1); + this.board.unshift(Array(BOARD_WIDTH).fill(null)); + } + // Update score + const lineCount = linesToClear.length; + this.lines += lineCount; + const points = [0, 100, 300, 500, 800]; + this.score += points[Math.min(lineCount, 4)] * this.level; + // Update level + const newLevel = Math.floor(this.lines / 10) + 1; + if (newLevel > this.level) { + this.level = newLevel; + this.dropSpeed = Math.max(MIN_DROP_SPEED, INITIAL_DROP_SPEED - (this.level - 1) * 50); + } + } + drop() { + if (!this.movePiece(0, 1)) { + this.lockPiece(); + } + } + startGame() { + this.dropTimer = window.setInterval(() => { + if (!this.paused && !this.gameOver) { + this.drop(); + this.render(); + } + }, this.dropSpeed); + } + render() { + const now = Date.now(); + if (now - this.lastRenderTime < 16) + return; // ~60 FPS + this.lastRenderTime = now; + const boardElement = document.getElementById('gameBoard'); + if (!boardElement) + return; + // Clear board + boardElement.innerHTML = ''; + // Create visual grid + for (let row = 0; row < BOARD_HEIGHT; row++) { + for (let col = 0; col < BOARD_WIDTH; col++) { + const cell = document.createElement('div'); + cell.className = 'cell'; + const piece = this.board[row][col]; + if (piece) { + cell.classList.add('filled', piece); + } + else { + cell.classList.add('empty'); + } + boardElement.appendChild(cell); + } + } + // Draw current piece + if (this.currentPiece) { + const shape = this.getShape(this.currentPiece); + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col] === 0) + continue; + const x = this.currentPiece.x + col; + const y = this.currentPiece.y + row; + if (y >= 0 && y < BOARD_HEIGHT && x >= 0 && x < BOARD_WIDTH) { + const index = y * BOARD_WIDTH + x; + const cell = boardElement.children[index]; + if (cell) { + cell.classList.remove('empty'); + cell.classList.add('filled', this.currentPiece.type); + } + } + } + } + } + // Update next piece preview + this.renderNextPiece(); + // Update UI + document.getElementById('score').textContent = this.score.toString(); + document.getElementById('level').textContent = this.level.toString(); + document.getElementById('lines').textContent = this.lines.toString(); + } + renderNextPiece() { + const preview = document.getElementById('nextPiece'); + if (!preview || !this.nextPiece) + return; + preview.innerHTML = ''; + const shape = this.getShape(this.nextPiece); + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + const cell = document.createElement('div'); + cell.className = 'cell'; + const hasBlock = shape[row] && shape[row][col]; + if (hasBlock) { + cell.classList.add('filled', this.nextPiece.type); + } + else { + cell.classList.add('empty'); + } + preview.appendChild(cell); + } + } + } + endGame() { + this.gameOver = true; + if (this.dropTimer !== null) { + clearInterval(this.dropTimer); + } + const gameOverOverlay = document.getElementById('gameOverOverlay'); + const finalScore = document.getElementById('finalScore'); + if (gameOverOverlay && finalScore) { + finalScore.textContent = this.score.toString(); + gameOverOverlay.classList.add('show'); + } + } + togglePause() { + this.paused = !this.paused; + const pausedOverlay = document.getElementById('pausedOverlay'); + if (pausedOverlay) { + if (this.paused) { + pausedOverlay.classList.add('show'); + } + else { + pausedOverlay.classList.remove('show'); + } + } + this.render(); + } + handleKeyDown(key) { + if (this.gameOver) + return; + switch (key.toLowerCase()) { + case 'arrowup': + case 'w': + this.rotatePiece(); + break; + case 'arrowleft': + case 'a': + this.movePiece(-1, 0); + break; + case 'arrowright': + case 'd': + this.movePiece(1, 0); + break; + case 'arrowdown': + case 's': + this.drop(); + break; + case ' ': + this.hardDrop(); + break; + case 'p': + this.togglePause(); + return; + } + this.render(); + } +} +// Initialize game +let game; +document.addEventListener('DOMContentLoaded', () => { + game = new TetrisGame(); + document.addEventListener('keydown', (e) => { + game.handleKeyDown(e.key); + }); +}); diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/tetris.ts b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/tetris.ts @@ -0,0 +1,404 @@ +// Tetris Game Implementation in TypeScript + +// Piece definitions +type PieceType = 'i' | 'o' | 't' | 's' | 'z' | 'j' | 'l'; + +interface Piece { + type: PieceType; + x: number; + y: number; + rotation: number; + shape: number[][]; +} + +const TETRIS_PIECES: Record<PieceType, number[][][]> = { + i: [ + [[1, 1, 1, 1]], + [[1], [1], [1], [1]] + ], + o: [ + [[1, 1], [1, 1]] + ], + t: [ + [[0, 1, 0], [1, 1, 1]], + [[1, 0], [1, 1], [1, 0]], + [[1, 1, 1], [0, 1, 0]], + [[0, 1], [1, 1], [0, 1]] + ], + s: [ + [[0, 1, 1], [1, 1, 0]], + [[1, 0], [1, 1], [0, 1]] + ], + z: [ + [[1, 1, 0], [0, 1, 1]], + [[0, 1], [1, 1], [1, 0]] + ], + j: [ + [[1, 0, 0], [1, 1, 1]], + [[1, 1], [1, 0], [1, 0]], + [[1, 1, 1], [0, 0, 1]], + [[0, 1], [0, 1], [1, 1]] + ], + l: [ + [[0, 0, 1], [1, 1, 1]], + [[1, 0], [1, 0], [1, 1]], + [[1, 1, 1], [1, 0, 0]], + [[1, 1], [0, 1], [0, 1]] + ] +}; + +const BOARD_WIDTH = 10; +const BOARD_HEIGHT = 20; +const INITIAL_DROP_SPEED = 800; +const MIN_DROP_SPEED = 100; + +class TetrisGame { + private board: (PieceType | null)[][]; + private currentPiece: Piece | null = null; + private nextPiece: Piece | null = null; + private score: number = 0; + private lines: number = 0; + private level: number = 1; + private dropSpeed: number = INITIAL_DROP_SPEED; + private gameOver: boolean = false; + private paused: boolean = false; + private dropTimer: number | null = null; + private lastRenderTime: number = 0; + + constructor() { + this.board = this.createEmptyBoard(); + this.nextPiece = this.createRandomPiece(); + this.spawnNextPiece(); + this.startGame(); + } + + private createEmptyBoard(): (PieceType | null)[][] { + return Array(BOARD_HEIGHT) + .fill(null) + .map(() => Array(BOARD_WIDTH).fill(null)); + } + + private createRandomPiece(): Piece { + const types: PieceType[] = ['i', 'o', 't', 's', 'z', 'j', 'l']; + const type = types[Math.floor(Math.random() * types.length)]; + const shapes = TETRIS_PIECES[type]; + return { + type, + x: Math.floor(BOARD_WIDTH / 2) - 1, + y: 0, + rotation: 0, + shape: shapes[0] + }; + } + + private spawnNextPiece(): void { + this.currentPiece = this.nextPiece; + this.nextPiece = this.createRandomPiece(); + + if (this.currentPiece && !this.isPositionValid(this.currentPiece)) { + this.endGame(); + } + + this.render(); + } + + private getShape(piece: Piece): number[][] { + const shapes = TETRIS_PIECES[piece.type]; + return shapes[piece.rotation % shapes.length]; + } + + private isPositionValid(piece: Piece): boolean { + const shape = this.getShape(piece); + + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col] === 0) continue; + + const x = piece.x + col; + const y = piece.y + row; + + if (x < 0 || x >= BOARD_WIDTH || y >= BOARD_HEIGHT) { + return false; + } + + if (y >= 0 && this.board[y][x] !== null) { + return false; + } + } + } + + return true; + } + + private movePiece(dx: number, dy: number): boolean { + if (!this.currentPiece) return false; + + const newPiece = { ...this.currentPiece, x: this.currentPiece.x + dx, y: this.currentPiece.y + dy }; + + if (this.isPositionValid(newPiece)) { + this.currentPiece = newPiece; + return true; + } + + return false; + } + + private rotatePiece(): void { + if (!this.currentPiece) return; + + const shapes = TETRIS_PIECES[this.currentPiece.type]; + if (shapes.length === 1) return; // Can't rotate square piece + + const oldRotation = this.currentPiece.rotation; + this.currentPiece.rotation = (this.currentPiece.rotation + 1) % shapes.length; + + if (!this.isPositionValid(this.currentPiece)) { + // Try wall kick + const kicks = [-1, 1, -2, 2]; + let rotated = false; + + for (const kick of kicks) { + this.currentPiece.x += kick; + if (this.isPositionValid(this.currentPiece)) { + rotated = true; + break; + } + } + + if (!rotated) { + this.currentPiece.rotation = oldRotation; + } + } + } + + private hardDrop(): void { + if (!this.currentPiece) return; + + while (this.movePiece(0, 1)) { + // Keep moving down until collision + } + + this.lockPiece(); + } + + private lockPiece(): void { + if (!this.currentPiece) return; + + const shape = this.getShape(this.currentPiece); + + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col] === 0) continue; + + const x = this.currentPiece.x + col; + const y = this.currentPiece.y + row; + + if (y >= 0) { + this.board[y][x] = this.currentPiece.type; + } + } + } + + this.clearLines(); + this.spawnNextPiece(); + } + + private clearLines(): void { + const linesToClear: number[] = []; + + for (let row = 0; row < BOARD_HEIGHT; row++) { + if (this.board[row].every(cell => cell !== null)) { + linesToClear.push(row); + } + } + + if (linesToClear.length === 0) return; + + // Remove cleared lines + for (let i = linesToClear.length - 1; i >= 0; i--) { + this.board.splice(linesToClear[i], 1); + this.board.unshift(Array(BOARD_WIDTH).fill(null)); + } + + // Update score + const lineCount = linesToClear.length; + this.lines += lineCount; + + const points = [0, 100, 300, 500, 800]; + this.score += points[Math.min(lineCount, 4)] * this.level; + + // Update level + const newLevel = Math.floor(this.lines / 10) + 1; + if (newLevel > this.level) { + this.level = newLevel; + this.dropSpeed = Math.max(MIN_DROP_SPEED, INITIAL_DROP_SPEED - (this.level - 1) * 50); + } + } + + private drop(): void { + if (!this.movePiece(0, 1)) { + this.lockPiece(); + } + } + + private startGame(): void { + this.dropTimer = window.setInterval(() => { + if (!this.paused && !this.gameOver) { + this.drop(); + this.render(); + } + }, this.dropSpeed); + } + + private render(): void { + const now = Date.now(); + if (now - this.lastRenderTime < 16) return; // ~60 FPS + this.lastRenderTime = now; + + const boardElement = document.getElementById('gameBoard'); + if (!boardElement) return; + + // Clear board + boardElement.innerHTML = ''; + + // Create visual grid + for (let row = 0; row < BOARD_HEIGHT; row++) { + for (let col = 0; col < BOARD_WIDTH; col++) { + const cell = document.createElement('div'); + cell.className = 'cell'; + + const piece = this.board[row][col]; + if (piece) { + cell.classList.add('filled', piece); + } else { + cell.classList.add('empty'); + } + + boardElement.appendChild(cell); + } + } + + // Draw current piece + if (this.currentPiece) { + const shape = this.getShape(this.currentPiece); + for (let row = 0; row < shape.length; row++) { + for (let col = 0; col < shape[row].length; col++) { + if (shape[row][col] === 0) continue; + + const x = this.currentPiece.x + col; + const y = this.currentPiece.y + row; + + if (y >= 0 && y < BOARD_HEIGHT && x >= 0 && x < BOARD_WIDTH) { + const index = y * BOARD_WIDTH + x; + const cell = boardElement.children[index]; + if (cell) { + cell.classList.remove('empty'); + cell.classList.add('filled', this.currentPiece.type); + } + } + } + } + } + + // Update next piece preview + this.renderNextPiece(); + + // Update UI + document.getElementById('score')!.textContent = this.score.toString(); + document.getElementById('level')!.textContent = this.level.toString(); + document.getElementById('lines')!.textContent = this.lines.toString(); + } + + private renderNextPiece(): void { + const preview = document.getElementById('nextPiece'); + if (!preview || !this.nextPiece) return; + + preview.innerHTML = ''; + const shape = this.getShape(this.nextPiece); + + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + const cell = document.createElement('div'); + cell.className = 'cell'; + + const hasBlock = shape[row] && shape[row][col]; + if (hasBlock) { + cell.classList.add('filled', this.nextPiece.type); + } else { + cell.classList.add('empty'); + } + + preview.appendChild(cell); + } + } + } + + private endGame(): void { + this.gameOver = true; + if (this.dropTimer !== null) { + clearInterval(this.dropTimer); + } + + const gameOverOverlay = document.getElementById('gameOverOverlay'); + const finalScore = document.getElementById('finalScore'); + if (gameOverOverlay && finalScore) { + finalScore.textContent = this.score.toString(); + gameOverOverlay.classList.add('show'); + } + } + + public togglePause(): void { + this.paused = !this.paused; + const pausedOverlay = document.getElementById('pausedOverlay'); + if (pausedOverlay) { + if (this.paused) { + pausedOverlay.classList.add('show'); + } else { + pausedOverlay.classList.remove('show'); + } + } + this.render(); + } + + public handleKeyDown(key: string): void { + if (this.gameOver) return; + + switch (key.toLowerCase()) { + case 'arrowup': + case 'w': + this.rotatePiece(); + break; + case 'arrowleft': + case 'a': + this.movePiece(-1, 0); + break; + case 'arrowright': + case 'd': + this.movePiece(1, 0); + break; + case 'arrowdown': + case 's': + this.drop(); + break; + case ' ': + this.hardDrop(); + break; + case 'p': + this.togglePause(); + return; + } + + this.render(); + } +} + +// Initialize game +let game: TetrisGame; + +document.addEventListener('DOMContentLoaded', () => { + game = new TetrisGame(); + + document.addEventListener('keydown', (e) => { + game.handleKeyDown(e.key); + }); +}); diff --git a/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/tsconfig.json b/dashboard/public/artifacts/tetris_context_file=none_effort=high_human_language=en_language=typescript_linter=off_max_budget=low_model=haiku_playwright=off_prompt_style=simple_sub_agents=off_web_search=off_run1/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "lib": ["ES2020", "DOM"], + "outDir": "./", + "rootDir": "./", + "strict": false, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["tetris.ts"], + "exclude": ["node_modules"] +} diff --git a/dashboard/src/components/Grid.tsx b/dashboard/src/components/Grid.tsx @@ -25,18 +25,16 @@ function formatCost(cost: number | null | undefined): string { return "$" + cost.toFixed(2); } -function formatRunId(runId: string): string { - const parts = runId.split("_run"); - const runNum = parts.length > 1 ? `#${parts[parts.length - 1]}` : ""; - const segments = parts[0].split("_"); - const task = segments[0]; - // Only show non-default/interesting config values - const config = segments - .slice(1) - .filter((s) => !s.includes("=off") && !s.includes("=none")) - .map((s) => s.split("=")[1]) - .join(" "); - return `${task} ${config} ${runNum}`.trim(); +function formatRunId(run: Run): React.ReactNode { + const m = run.meta; + return ( + <span style={{ display: "inline-flex", gap: "4px", alignItems: "center", flexWrap: "wrap" }}> + <span className="badge badge-neutral" style={{ fontSize: "0.7rem" }}>{m.task}</span> + <span style={{ color: "var(--text-muted)", fontSize: "0.7rem" }}> + {m.model} {m.prompt_style} {m.language} + </span> + </span> + ); } function formatTime(seconds: number | null | undefined): string { @@ -101,7 +99,7 @@ export default function Grid({ runs, axisValues, tasks }: GridProps) { <tr key={run.meta.run_id}> <td> <a href={`/run/${run.meta.run_id}`} style={{ fontSize: "0.75rem" }}> - {formatRunId(run.meta.run_id)} + {formatRunId(run)} </a> </td> <td>{run.meta.task}</td> diff --git a/dashboard/src/components/RunDetail.tsx b/dashboard/src/components/RunDetail.tsx @@ -20,10 +20,7 @@ const EXIT_CODES: Record<number, string> = { 143: "Terminated (SIGTERM)", }; -const AXIS_CONFIG: Array<{ - key: string; - label: string; -}> = [ +const AXIS_CONFIG: Array<{ key: string; label: string }> = [ { key: "model", label: "Model" }, { key: "effort", label: "Effort" }, { key: "prompt_style", label: "Prompt" }, @@ -43,52 +40,30 @@ const AXIS_CONFIG: Array<{ ]; function ConfigPills({ - axisKey, label, activeValue, allValues, }: { - axisKey: string; label: string; activeValue: string; allValues: string[]; }) { return ( - <div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "6px" }}> - <div - style={{ - width: "90px", - fontSize: "0.7rem", - color: "var(--text-muted)", - textTransform: "uppercase", - letterSpacing: "0.03em", - textAlign: "right", - flexShrink: 0, - }} - > + <div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "4px" }}> + <div style={{ width: "80px", fontSize: "0.65rem", color: "var(--text-muted)", textTransform: "uppercase", letterSpacing: "0.03em", textAlign: "right", flexShrink: 0 }}> {label} </div> - <div style={{ display: "flex", gap: "4px", flexWrap: "wrap" }}> + <div style={{ display: "flex", gap: "3px", flexWrap: "wrap" }}> {allValues.map((val) => ( - <span - key={val} - style={{ - padding: "2px 8px", - borderRadius: "4px", - fontSize: "0.7rem", - fontFamily: "var(--font-mono)", - background: - val === activeValue - ? "rgba(255, 255, 255, 0.1)" - : "transparent", - color: - val === activeValue ? "#fff" : "rgba(255, 255, 255, 0.2)", - border: - val === activeValue - ? "1px solid rgba(255, 255, 255, 0.3)" - : "1px solid rgba(255, 255, 255, 0.05)", - }} - > + <span key={val} style={{ + padding: "1px 6px", + borderRadius: "3px", + fontSize: "0.65rem", + fontFamily: "var(--font-mono)", + background: val === activeValue ? "rgba(255, 255, 255, 0.1)" : "transparent", + color: val === activeValue ? "#fff" : "rgba(255, 255, 255, 0.2)", + border: val === activeValue ? "1px solid rgba(255, 255, 255, 0.3)" : "1px solid rgba(255, 255, 255, 0.05)", + }}> {val} </span> ))} @@ -100,253 +75,126 @@ function ConfigPills({ function ExitCodeBadge({ code }: { code: number | undefined }) { if (code === undefined || code === null) return <span style={{ color: "var(--text-muted)" }}>?</span>; - const label = EXIT_CODES[code] || `Exit ${code}`; const isOk = code === 0; - return ( <div> - <span - style={{ - fontFamily: "var(--font-mono)", - fontWeight: 700, - fontSize: "1.75rem", - color: isOk ? "var(--green)" : "var(--red)", - }} - > + <span style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: "1.75rem", color: isOk ? "var(--green)" : "var(--red)" }}> {code} </span> - <div - style={{ - fontSize: "0.7rem", - color: isOk ? "var(--green)" : "var(--red)", - opacity: 0.8, - }} - > + <div style={{ fontSize: "0.7rem", color: isOk ? "var(--green)" : "var(--red)", opacity: 0.8 }}> {label} </div> </div> ); } -function ScoreBar({ - label, - score, -}: { - label: string; - score: number | null | undefined; -}) { +function ScoreBar({ label, score }: { label: string; score: number | null | undefined }) { if (score === null || score === undefined) { return ( - <div style={{ marginBottom: "8px" }}> - <div - style={{ - display: "flex", - justifyContent: "space-between", - fontSize: "0.8rem", - marginBottom: "4px", - }} - > + <div style={{ marginBottom: "6px" }}> + <div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.75rem", marginBottom: "2px" }}> <span>{label}</span> <span style={{ color: "var(--text-muted)" }}>N/A</span> </div> </div> ); } - const pct = Math.round(score * 100); - const color = - pct >= 70 ? "var(--green)" : pct >= 40 ? "var(--yellow)" : "var(--red)"; - + const color = pct >= 70 ? "var(--green)" : pct >= 40 ? "var(--yellow)" : "var(--red)"; return ( - <div style={{ marginBottom: "8px" }}> - <div - style={{ - display: "flex", - justifyContent: "space-between", - fontSize: "0.8rem", - marginBottom: "4px", - }} - > + <div style={{ marginBottom: "6px" }}> + <div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.75rem", marginBottom: "2px" }}> <span>{label}</span> - <span - style={{ fontFamily: "var(--font-mono)", fontWeight: 600, color }} - > - {pct}% - </span> + <span style={{ fontFamily: "var(--font-mono)", fontWeight: 600, color }}>{pct}%</span> </div> - <div - style={{ - background: "var(--bg)", - borderRadius: "4px", - height: "6px", - overflow: "hidden", - }} - > - <div - style={{ - width: `${pct}%`, - height: "100%", - background: color, - borderRadius: "4px", - }} - /> + <div style={{ background: "var(--bg)", borderRadius: "3px", height: "4px", overflow: "hidden" }}> + <div style={{ width: `${pct}%`, height: "100%", background: color, borderRadius: "3px" }} /> </div> </div> ); } -export default function RunDetail({ - run, - transcriptLines, - axisValues, -}: RunDetailProps) { +export default function RunDetail({ run, transcriptLines, axisValues }: RunDetailProps) { const { meta, eval_results, claude_output } = run; + // Check if this run has an artifact to preview (tetris games, web apps) + const hasArtifact = meta.task === "tetris" || meta.task === "bookmarks-api"; + const artifactUrl = hasArtifact ? `/artifacts/${meta.run_id}/index.html` : null; + return ( - <div style={{ display: "flex", flexDirection: "column", gap: "24px" }}> - {/* Top bar: stats */} - <div className="stats-grid"> - <div className="stat-card"> - <ExitCodeBadge code={meta.exit_code} /> - <div className="stat-label">Exit Code</div> - </div> - <div className="stat-card"> - <div className="stat-value"> - {meta.wall_time_seconds != null - ? meta.wall_time_seconds < 60 - ? `${meta.wall_time_seconds}s` - : `${Math.floor(meta.wall_time_seconds / 60)}m${meta.wall_time_seconds % 60}s` - : "-"} - </div> - <div className="stat-label">Wall Time</div> - </div> - <div className="stat-card"> - <div className="stat-value"> - {claude_output?.total_cost_usd != null - ? `$${claude_output.total_cost_usd.toFixed(2)}` - : "-"} - </div> - <div className="stat-label">Cost</div> - </div> - <div className="stat-card"> - <div className="stat-value">{claude_output?.num_turns ?? "-"}</div> - <div className="stat-label">Turns</div> - </div> - <div className="stat-card"> - <div className="stat-value"> - {claude_output?.usage - ? ( - (claude_output.usage.input_tokens ?? 0) + - (claude_output.usage.output_tokens ?? 0) - ).toLocaleString() - : "-"} + <div style={{ display: "flex", flexDirection: "column", gap: "20px" }}> + {/* Top section: stats + config + scores side by side */} + <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "16px" }}> + {/* Stats */} + <div className="card" style={{ padding: "16px" }}> + <h3 style={{ marginBottom: "12px", fontSize: "0.85rem" }}>Metrics</h3> + <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px" }}> + <div> + <ExitCodeBadge code={meta.exit_code} /> + </div> + <div> + <div style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: "1.75rem" }}> + {meta.wall_time_seconds != null + ? meta.wall_time_seconds < 60 + ? `${meta.wall_time_seconds}s` + : `${Math.floor(meta.wall_time_seconds / 60)}m${meta.wall_time_seconds % 60}s` + : "-"} + </div> + <div style={{ fontSize: "0.7rem", color: "var(--text-muted)" }}>Wall Time</div> + </div> + <div> + <div style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: "1.75rem" }}> + {claude_output?.total_cost_usd != null ? `$${claude_output.total_cost_usd.toFixed(2)}` : "-"} + </div> + <div style={{ fontSize: "0.7rem", color: "var(--text-muted)" }}>Cost</div> + </div> + <div> + <div style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: "1.75rem" }}> + {claude_output?.num_turns ?? "-"} + </div> + <div style={{ fontSize: "0.7rem", color: "var(--text-muted)" }}>Turns</div> + </div> </div> - <div className="stat-label">Total Tokens</div> + {meta.claude_version && ( + <div style={{ marginTop: "12px", fontSize: "0.65rem", color: "var(--text-muted)" }}> + Claude {meta.claude_version} + </div> + )} </div> - </div> - - {/* Config pills */} - <div className="card"> - <h3 style={{ marginBottom: "12px" }}>Configuration</h3> - {AXIS_CONFIG.map(({ key, label }) => { - const active = String( - (meta as Record<string, unknown>)[key] ?? "" - ); - const all = (axisValues as Record<string, string[]>)[key] || [ - active, - ]; - if (!active) return null; - return ( - <ConfigPills - key={key} - axisKey={key} - label={label} - activeValue={active} - allValues={all} - /> - ); - })} - </div> - {/* Two-column: transcript left, scores right */} - <div - style={{ - display: "grid", - gridTemplateColumns: "1fr 360px", - gap: "24px", - alignItems: "start", - }} - > - {/* Transcript */} - <TranscriptViewer lines={transcriptLines} /> + {/* Config */} + <div className="card" style={{ padding: "16px" }}> + <h3 style={{ marginBottom: "10px", fontSize: "0.85rem" }}>Configuration</h3> + {AXIS_CONFIG.map(({ key, label }) => { + const active = String((meta as Record<string, unknown>)[key] ?? ""); + const all = (axisValues as Record<string, string[]>)[key] || [active]; + if (!active) return null; + return <ConfigPills key={key} label={label} activeValue={active} allValues={all} />; + })} + </div> - {/* Scores sidebar */} - <div - style={{ - display: "flex", - flexDirection: "column", - gap: "16px", - position: "sticky", - top: "16px", - }} - > + {/* Scores + checks */} + <div className="card" style={{ padding: "16px" }}> + <h3 style={{ marginBottom: "10px", fontSize: "0.85rem" }}>Evaluation</h3> {eval_results && ( - <div className="card"> - <h3 style={{ marginBottom: "16px" }}>Scores</h3> + <> <ScoreBar label="Overall" score={eval_results.score} /> - <ScoreBar - label="Structural" - score={eval_results.structural?.score} - /> - <ScoreBar - label="Functional" - score={eval_results.functional?.score} - /> + <ScoreBar label="Structural" score={eval_results.structural?.score} /> + <ScoreBar label="Functional" score={eval_results.functional?.score} /> <ScoreBar label="Quality" score={eval_results.quality?.score} /> - </div> + </> )} - {eval_results?.structural?.checks && ( - <div className="card"> - <h4 - style={{ - fontSize: "0.8rem", - color: "var(--text-muted)", - marginBottom: "8px", - }} - > - Structural Checks - </h4> + <div style={{ marginTop: "10px", borderTop: "1px solid var(--border)", paddingTop: "8px" }}> + <div style={{ fontSize: "0.7rem", color: "var(--text-muted)", marginBottom: "4px" }}>Checks</div> {eval_results.structural.checks.map( - ( - check: { pass: boolean; name: string; detail: string }, - i: number - ) => ( - <div - key={i} - style={{ - display: "flex", - gap: "8px", - fontSize: "0.75rem", - marginBottom: "4px", - alignItems: "baseline", - }} - > - <span - style={{ - color: check.pass ? "var(--green)" : "var(--red)", - flexShrink: 0, - }} - > + (check: { pass: boolean; name: string; detail: string }, i: number) => ( + <div key={i} style={{ display: "flex", gap: "6px", fontSize: "0.7rem", marginBottom: "2px" }}> + <span style={{ color: check.pass ? "var(--green)" : "var(--red)", flexShrink: 0 }}> {check.pass ? "+" : "-"} </span> - <span style={{ fontFamily: "var(--font-mono)" }}> - {check.name} - </span> - <span style={{ color: "var(--text-muted)" }}> - {check.detail} - </span> + <span style={{ fontFamily: "var(--font-mono)" }}>{check.name}</span> </div> ) )} @@ -354,6 +202,35 @@ export default function RunDetail({ )} </div> </div> + + {/* Bottom: transcript + artifact preview */} + <div style={{ + display: "grid", + gridTemplateColumns: artifactUrl ? "1fr 1fr" : "1fr", + gap: "20px", + alignItems: "start", + }}> + <TranscriptViewer lines={transcriptLines} /> + + {artifactUrl && ( + <div className="card" style={{ padding: "0", overflow: "hidden", position: "sticky", top: "16px" }}> + <div style={{ padding: "12px 16px", borderBottom: "1px solid var(--border)", fontSize: "0.8rem", fontWeight: 600 }}> + Result Preview + </div> + <iframe + src={artifactUrl} + style={{ + width: "100%", + height: "70vh", + border: "none", + background: "#fff", + }} + sandbox="allow-scripts" + title="Tetris game preview" + /> + </div> + )} + </div> </div> ); } diff --git a/dashboard/src/components/TranscriptViewer.tsx b/dashboard/src/components/TranscriptViewer.tsx @@ -1,151 +1,575 @@ +import { useState, type ReactNode } from "react"; + interface TranscriptViewerProps { lines: string[]; } +// -- Theme tokens (inline, no CSS file needed) -- + +const theme = { + bg: "#0f1117", + bgCard: "#1a1d27", + border: "#2d3045", + text: "#e5e7eb", + textMuted: "#9ca3af", + accent: "#6366f1", + green: "#22c55e", + red: "#ef4444", + yellow: "#eab308", + fontMono: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace", +}; + +// -- Types for parsed transcript events -- + +interface ContentBlockThinking { + type: "thinking"; + thinking: string; +} + +interface ContentBlockText { + type: "text"; + text: string; +} + +interface ContentBlockToolUse { + type: "tool_use"; + id?: string; + name: string; + input: Record<string, unknown>; +} + +type ContentBlock = ContentBlockThinking | ContentBlockText | ContentBlockToolUse; + +interface AssistantMessage { + content: ContentBlock[]; +} + +interface ToolUseResult { + stdout?: string; + stderr?: string; + [key: string]: unknown; +} + interface TranscriptEvent { type?: string; - role?: string; - content?: string; - tool_name?: string; - tool_input?: unknown; + subtype?: string; + // system init + model?: string; + tools?: unknown[]; + // assistant + message?: AssistantMessage; + // user (tool results) + tool_use_result?: ToolUseResult | string; + // result result?: string; + total_cost_usd?: number; + num_turns?: number; + usage?: Record<string, unknown>; [key: string]: unknown; } -function renderEvent(event: TranscriptEvent, index: number) { - const type = event.type || "unknown"; +// -- Minimal markdown renderer -- - if (type === "assistant" || event.role === "assistant") { - return ( - <div - key={index} +function renderMarkdown(text: string): ReactNode[] { + // Split by code blocks first + const parts = text.split(/(```[\s\S]*?```)/g); + const nodes: ReactNode[] = []; + + parts.forEach((part, i) => { + if (part.startsWith("```")) { + // Fenced code block + const match = part.match(/^```(\w*)\n?([\s\S]*?)```$/); + const code = match ? match[2] : part.slice(3, -3); + nodes.push( + <pre + key={i} + style={{ + background: "#12141c", + padding: "10px 12px", + borderRadius: 4, + fontSize: "0.8rem", + fontFamily: theme.fontMono, + overflow: "auto", + margin: "6px 0", + color: theme.text, + border: `1px solid ${theme.border}`, + }} + > + {code} + </pre> + ); + } else { + // Inline formatting + nodes.push(...renderInlineMarkdown(part, i)); + } + }); + + return nodes; +} + +function renderInlineMarkdown(text: string, keyBase: number): ReactNode[] { + // Handle bold, inline code + const parts = text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g); + return parts.map((part, j) => { + const key = `${keyBase}-${j}`; + if (part.startsWith("**") && part.endsWith("**")) { + return ( + <strong key={key} style={{ color: theme.text }}> + {part.slice(2, -2)} + </strong> + ); + } + if (part.startsWith("`") && part.endsWith("`")) { + return ( + <code + key={key} + style={{ + background: "#12141c", + padding: "1px 5px", + borderRadius: 3, + fontSize: "0.8rem", + fontFamily: theme.fontMono, + color: "#c4b5fd", + }} + > + {part.slice(1, -1)} + </code> + ); + } + return <span key={key}>{part}</span>; + }); +} + +// -- Collapsible wrapper -- + +function Collapsible({ + label, + defaultOpen = false, + children, + labelColor, +}: { + label: string; + defaultOpen?: boolean; + children: ReactNode; + labelColor?: string; + muted?: boolean; +}) { + const [open, setOpen] = useState(defaultOpen); + return ( + <div> + <button + onClick={() => setOpen(!open)} style={{ - padding: "12px 16px", - borderLeft: "3px solid #6366f1", - marginBottom: "8px", - background: "rgba(99, 102, 241, 0.05)", + background: "none", + border: "none", + color: labelColor || theme.textMuted, + cursor: "pointer", + fontSize: "0.75rem", + fontFamily: theme.fontMono, + padding: "2px 0", + display: "flex", + alignItems: "center", + gap: 4, }} > - <div + <span style={{ display: "inline-block", width: 12, textAlign: "center", fontSize: "0.65rem" }}> + {open ? "\u25BC" : "\u25B6"} + </span> + {label} + </button> + {open && <div style={{ marginTop: 4 }}>{children}</div>} + </div> + ); +} + +// -- Code block display -- + +function CodeBlock({ + children, + maxHeight, + color, +}: { + children: string; + maxHeight?: string; + color?: string; +}) { + return ( + <pre + style={{ + background: "#12141c", + padding: "8px 12px", + borderRadius: 4, + fontSize: "0.78rem", + fontFamily: theme.fontMono, + overflow: "auto", + maxHeight: maxHeight || "300px", + margin: "4px 0 0", + color: color || theme.textMuted, + border: `1px solid ${theme.border}`, + whiteSpace: "pre-wrap", + wordBreak: "break-word", + }} + > + {children} + </pre> + ); +} + +// -- Tool header (terminal-style bar) -- + +function ToolHeader({ name, detail }: { name: string; detail?: string }) { + return ( + <div + style={{ + display: "flex", + alignItems: "center", + gap: 8, + marginBottom: 4, + }} + > + <span + style={{ + fontSize: "0.7rem", + fontFamily: theme.fontMono, + color: theme.yellow, + fontWeight: 600, + textTransform: "uppercase", + letterSpacing: "0.05em", + }} + > + {name} + </span> + {detail && ( + <span style={{ - fontSize: "0.7rem", - color: "#9ca3af", - marginBottom: "4px", + fontSize: "0.75rem", + fontFamily: theme.fontMono, + color: theme.textMuted, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", }} > - ASSISTANT - </div> - <div style={{ whiteSpace: "pre-wrap", fontSize: "0.875rem" }}> - {event.content || event.result || JSON.stringify(event, null, 2)} - </div> - </div> + {detail} + </span> + )} + </div> + ); +} + +// -- Event card wrapper -- + +function EventCard({ + borderColor, + bgTint, + children, + compact, +}: { + borderColor: string; + bgTint?: string; + children: ReactNode; + compact?: boolean; +}) { + return ( + <div + style={{ + padding: compact ? "6px 12px" : "10px 14px", + borderLeft: `3px solid ${borderColor}`, + marginBottom: 4, + background: bgTint || "transparent", + borderRadius: "0 4px 4px 0", + }} + > + {children} + </div> + ); +} + +// -- Detect file path from heredoc or cat > writes -- + +function extractFileWriteTarget(command: string): string | null { + // cat > filename, cat << ... > filename + const catWrite = command.match(/cat\s+>+\s*([^\s<]+)/); + if (catWrite) return catWrite[1]; + const heredoc = command.match(/cat\s*<<['"\\]*(\w+)['"]*\s*>\s*([^\s]+)/); + if (heredoc) return heredoc[2]; + return null; +} + +// -- Render individual content blocks -- + +function renderContentBlock(block: ContentBlock, index: number): ReactNode { + switch (block.type) { + case "thinking": + return ( + <EventCard key={index} borderColor="#4b5563" bgTint="rgba(75, 85, 99, 0.05)"> + <Collapsible label="thinking..." labelColor="#6b7280"> + <div + style={{ + fontStyle: "italic", + color: theme.textMuted, + fontSize: "0.82rem", + whiteSpace: "pre-wrap", + lineHeight: 1.5, + maxHeight: "300px", + overflow: "auto", + }} + > + {block.thinking} + </div> + </Collapsible> + </EventCard> + ); + + case "text": + return ( + <EventCard key={index} borderColor={theme.accent} bgTint="rgba(99, 102, 241, 0.04)"> + <div style={{ fontSize: "0.875rem", lineHeight: 1.6, color: theme.text, whiteSpace: "pre-wrap" }}> + {renderMarkdown(block.text)} + </div> + </EventCard> + ); + + case "tool_use": + return renderToolUse(block, index); + + default: + return null; + } +} + +function renderToolUse(block: ContentBlockToolUse, index: number): ReactNode { + const name = block.name; + const input = block.input || {}; + + if (name === "Bash") { + const command = (input.command as string) || JSON.stringify(input, null, 2); + const fileTarget = extractFileWriteTarget(command); + return ( + <EventCard key={index} borderColor={theme.yellow} bgTint="rgba(234, 179, 8, 0.04)"> + <ToolHeader name="Bash" detail={fileTarget ? `writing to ${fileTarget}` : undefined} /> + <CodeBlock>{command}</CodeBlock> + </EventCard> + ); + } + + if (name === "Read") { + const filePath = (input.file_path as string) || ""; + return ( + <EventCard key={index} borderColor={theme.yellow} bgTint="rgba(234, 179, 8, 0.04)"> + <ToolHeader name="Read" detail={filePath} /> + </EventCard> + ); + } + + if (name === "Write") { + const filePath = (input.file_path as string) || ""; + const content = (input.content as string) || ""; + return ( + <EventCard key={index} borderColor={theme.yellow} bgTint="rgba(234, 179, 8, 0.04)"> + <ToolHeader name="Write" detail={filePath} /> + {content && <CodeBlock maxHeight="250px">{content}</CodeBlock>} + </EventCard> ); } - if (type === "tool_use" || event.tool_name) { + if (name === "Edit") { + const filePath = (input.file_path as string) || ""; + const oldStr = (input.old_string as string) || ""; + const newStr = (input.new_string as string) || ""; + return ( + <EventCard key={index} borderColor={theme.yellow} bgTint="rgba(234, 179, 8, 0.04)"> + <ToolHeader name="Edit" detail={filePath} /> + {oldStr && ( + <div style={{ marginTop: 4 }}> + <span style={{ fontSize: "0.7rem", color: theme.red, fontFamily: theme.fontMono }}>- old</span> + <CodeBlock maxHeight="150px" color={theme.red}>{oldStr}</CodeBlock> + </div> + )} + {newStr && ( + <div style={{ marginTop: 4 }}> + <span style={{ fontSize: "0.7rem", color: theme.green, fontFamily: theme.fontMono }}>+ new</span> + <CodeBlock maxHeight="150px" color={theme.green}>{newStr}</CodeBlock> + </div> + )} + </EventCard> + ); + } + + // Generic tool + return ( + <EventCard key={index} borderColor={theme.yellow} bgTint="rgba(234, 179, 8, 0.04)"> + <ToolHeader name={name} /> + <CodeBlock maxHeight="200px">{JSON.stringify(input, null, 2)}</CodeBlock> + </EventCard> + ); +} + +// -- Render a full event (top-level JSON line) -- + +function renderEvent(event: TranscriptEvent, index: number): ReactNode { + const type = event.type; + + // System init + if (type === "system" && event.subtype === "init") { + const model = (event.model as string) || "unknown model"; + const toolCount = Array.isArray(event.tools) ? event.tools.length : 0; return ( <div key={index} style={{ - padding: "12px 16px", - borderLeft: "3px solid #eab308", - marginBottom: "8px", - background: "rgba(234, 179, 8, 0.05)", + display: "inline-flex", + alignItems: "center", + gap: 8, + background: "#252836", + borderRadius: 999, + padding: "4px 14px", + marginBottom: 6, + fontSize: "0.75rem", + color: theme.textMuted, + fontFamily: theme.fontMono, }} > - <div - style={{ - fontSize: "0.7rem", - color: "#eab308", - marginBottom: "4px", - fontFamily: "var(--font-mono)", - }} - > - TOOL: {event.tool_name || "unknown"} - </div> - <pre - style={{ - fontSize: "0.75rem", - overflow: "auto", - maxHeight: "200px", - color: "#9ca3af", - }} - > - {typeof event.tool_input === "string" - ? event.tool_input - : JSON.stringify(event.tool_input, null, 2)} - </pre> + <span>{model}</span> + {toolCount > 0 && ( + <> + <span style={{ color: theme.border }}>|</span> + <span>{toolCount} tools</span> + </> + )} </div> ); } - if (type === "tool_result") { + // Assistant message - contains content blocks + if (type === "assistant" && event.message?.content) { + const blocks = event.message.content; + return ( + <div key={index} style={{ marginBottom: 2 }}> + {blocks.map((block, bi) => renderContentBlock(block, bi))} + </div> + ); + } + + // User event (tool results) + if (type === "user") { + const result = event.tool_use_result; + if (!result) return null; + + let stdout = ""; + let stderr = ""; + + if (typeof result === "string") { + stdout = result; + } else { + stdout = (result.stdout as string) || ""; + stderr = (result.stderr as string) || ""; + } + + const content = stdout || stderr; + if (!content) return null; + + const lineCount = content.split("\n").length; + const shouldCollapse = lineCount > 10; + + return ( + <EventCard key={index} borderColor={theme.green} bgTint="rgba(34, 197, 94, 0.03)" compact> + {shouldCollapse ? ( + <Collapsible label={`result (${lineCount} lines)`} labelColor={theme.green}> + {stderr && <CodeBlock maxHeight="200px" color={theme.red}>{stderr}</CodeBlock>} + {stdout && <CodeBlock maxHeight="300px">{stdout}</CodeBlock>} + </Collapsible> + ) : ( + <> + {stderr && <CodeBlock maxHeight="200px" color={theme.red}>{stderr}</CodeBlock>} + {stdout && <CodeBlock maxHeight="200px">{stdout}</CodeBlock>} + </> + )} + </EventCard> + ); + } + + // Final result + if (type === "result") { return ( <div key={index} style={{ - padding: "12px 16px", - borderLeft: "3px solid #22c55e", - marginBottom: "8px", - background: "rgba(34, 197, 94, 0.05)", + background: "rgba(99, 102, 241, 0.08)", + border: `1px solid ${theme.accent}`, + borderRadius: 6, + padding: "14px 16px", + marginTop: 8, }} > <div style={{ fontSize: "0.7rem", - color: "#22c55e", - marginBottom: "4px", + color: theme.accent, + fontFamily: theme.fontMono, + fontWeight: 600, + marginBottom: 8, + textTransform: "uppercase", + letterSpacing: "0.05em", }} > - RESULT + Result </div> - <pre + {event.result && ( + <div style={{ fontSize: "0.875rem", color: theme.text, whiteSpace: "pre-wrap", lineHeight: 1.6 }}> + {renderMarkdown(event.result)} + </div> + )} + <div style={{ + display: "flex", + gap: 16, + marginTop: 10, fontSize: "0.75rem", - overflow: "auto", - maxHeight: "200px", - color: "#9ca3af", + fontFamily: theme.fontMono, + color: theme.textMuted, + flexWrap: "wrap", }} > - {event.content || JSON.stringify(event, null, 2)} - </pre> + {event.total_cost_usd != null && <span>cost: ${event.total_cost_usd.toFixed(4)}</span>} + {event.num_turns != null && <span>turns: {event.num_turns}</span>} + </div> </div> ); } - // Fallback: render raw JSON + // Fallback: render compact raw JSON return ( - <div - key={index} - style={{ - padding: "8px 16px", - marginBottom: "4px", - borderLeft: "3px solid #2d3045", - }} - > + <EventCard key={index} borderColor={theme.border} compact> <pre style={{ fontSize: "0.7rem", - color: "#9ca3af", + color: theme.textMuted, overflow: "auto", - maxHeight: "100px", + maxHeight: "80px", + margin: 0, + fontFamily: theme.fontMono, }} > {JSON.stringify(event, null, 2)} </pre> - </div> + </EventCard> ); } +// -- Main component -- + export default function TranscriptViewer({ lines }: TranscriptViewerProps) { if (lines.length === 0) { return ( <div - className="card" style={{ textAlign: "center", padding: "40px", - color: "var(--text-muted)", + color: theme.textMuted, + background: theme.bgCard, + borderRadius: 8, + border: `1px solid ${theme.border}`, }} > No transcript available for this run. @@ -164,10 +588,34 @@ export default function TranscriptViewer({ lines }: TranscriptViewerProps) { .filter(Boolean); return ( - <div className="card" style={{ maxHeight: "80vh", overflow: "auto" }}> - <h3 style={{ marginBottom: "16px", position: "sticky", top: 0, background: "var(--bg-card)", paddingBottom: "8px", zIndex: 1 }}> - Transcript ({events.length} events) - </h3> + <div + style={{ + maxHeight: "80vh", + overflow: "auto", + background: theme.bgCard, + borderRadius: 8, + border: `1px solid ${theme.border}`, + padding: "14px 16px", + }} + > + <div + style={{ + position: "sticky", + top: 0, + background: theme.bgCard, + paddingBottom: 8, + zIndex: 1, + borderBottom: `1px solid ${theme.border}`, + marginBottom: 10, + }} + > + <h3 style={{ margin: 0, fontSize: "0.95rem", color: theme.text }}> + Transcript + <span style={{ color: theme.textMuted, fontWeight: 400, fontSize: "0.8rem", marginLeft: 8 }}> + {events.length} events + </span> + </h3> + </div> {events.map((event, i) => renderEvent(event, i))} </div> );