loop-benchmarking

Controlled experiments across agentic coding configurations. Same task, one variable, what actually works.
git clone https://git.shiptheloop.com/loop-benchmarking.git
Log | Files | Refs | README

tetris.spec.ts (16027B)


      1 import { test, expect, type Page } from "@playwright/test";
      2 
      3 // Try common entry points until one loads successfully.
      4 async function loadGame(page: Page) {
      5   const candidates = [
      6     "/",
      7     "/index.html",
      8     "/dist/index.html",
      9     "/public/index.html",
     10     "/build/index.html",
     11   ];
     12 
     13   for (const path of candidates) {
     14     try {
     15       const resp = await page.goto(path, { timeout: 5000 });
     16       if (resp?.ok()) return;
     17     } catch {
     18       continue;
     19     }
     20   }
     21 }
     22 
     23 // Find the game surface: canvas or a grid-like DOM container.
     24 function gameBoard(page: Page) {
     25   return page.locator(
     26     [
     27       "canvas",
     28       '[class*="board"]',
     29       '[class*="grid"]',
     30       '[class*="game-area"]',
     31       '[class*="field"]',
     32       '[id*="board"]',
     33       '[id*="grid"]',
     34       '[id*="game"]',
     35       '[id*="field"]',
     36       "table",
     37     ].join(", ")
     38   );
     39 }
     40 
     41 // Click the board area to make sure it has focus, then try common
     42 // start interactions in case the game waits for user action.
     43 async function ensureGameStarted(page: Page) {
     44   const board = gameBoard(page);
     45   const count = await board.count();
     46   if (count > 0) {
     47     try {
     48       await board.first().click({ timeout: 2000 });
     49     } catch {
     50       // click failed, continue anyway
     51     }
     52   }
     53 
     54   // Some games need a key press or button click to start
     55   const startButton = page.locator(
     56     'button:has-text("start"), button:has-text("Start"), button:has-text("play"), button:has-text("Play"), [class*="start"], [id*="start"]'
     57   );
     58   if ((await startButton.count()) > 0) {
     59     try {
     60       await startButton.first().click({ timeout: 2000 });
     61     } catch {
     62       // ignore
     63     }
     64   }
     65 
     66   // Press Enter/Space as a fallback start trigger
     67   await page.keyboard.press("Enter");
     68   await page.waitForTimeout(300);
     69   await page.keyboard.press("Space");
     70   await page.waitForTimeout(500);
     71 }
     72 
     73 test.describe("Tetris Game", () => {
     74   test.beforeEach(async ({ page }) => {
     75     await loadGame(page);
     76     await page.waitForLoadState("domcontentloaded");
     77     await page.waitForTimeout(1000);
     78     await ensureGameStarted(page);
     79   });
     80 
     81   // ---- 1. Page loads without errors ----
     82   test("page loads without console errors", async ({ page }) => {
     83     const errors: string[] = [];
     84     page.on("pageerror", (err) => errors.push(err.message));
     85     await page.waitForTimeout(2000);
     86     expect(errors).toEqual([]);
     87   });
     88 
     89   // ---- 2. Game board is visible ----
     90   test("game board is visible", async ({ page }) => {
     91     const board = gameBoard(page);
     92     const count = await board.count();
     93 
     94     expect(
     95       count,
     96       "Expected a <canvas> or a container with board/grid/game/field in its class or id"
     97     ).toBeGreaterThan(0);
     98 
     99     // The board element should have meaningful dimensions
    100     const box = await board.first().boundingBox();
    101     expect(box, "Game board should be visible on screen").not.toBeNull();
    102     expect(box!.width).toBeGreaterThan(50);
    103     expect(box!.height).toBeGreaterThan(50);
    104   });
    105 
    106   // ---- 3. Game starts automatically or via interaction ----
    107   test("game starts", async ({ page }) => {
    108     // After beforeEach, the game should be running. Verify by checking that
    109     // the page is not static: take two screenshots separated by time.
    110     const shot1 = await page.screenshot();
    111     await page.waitForTimeout(2500);
    112     const shot2 = await page.screenshot();
    113 
    114     expect(
    115       Buffer.from(shot1).equals(Buffer.from(shot2)),
    116       "Expected the game to show visual activity after starting"
    117     ).toBe(false);
    118   });
    119 
    120   // ---- 4. Piece falls automatically (auto-drop) ----
    121   test("piece falls automatically", async ({ page }) => {
    122     // Take screenshots at intervals without pressing any keys.
    123     // A falling piece should cause visual changes.
    124     const shot1 = await page.screenshot();
    125     await page.waitForTimeout(2000);
    126     const shot2 = await page.screenshot();
    127     await page.waitForTimeout(2000);
    128     const shot3 = await page.screenshot();
    129 
    130     const buf1 = Buffer.from(shot1);
    131     const buf2 = Buffer.from(shot2);
    132     const buf3 = Buffer.from(shot3);
    133 
    134     // At least one pair should differ (piece is moving down)
    135     const changed = !buf1.equals(buf2) || !buf2.equals(buf3);
    136     expect(changed, "Expected piece to fall over time without input").toBe(
    137       true
    138     );
    139   });
    140 
    141   // ---- 5. Left arrow moves piece left ----
    142   test("left arrow moves piece", async ({ page }) => {
    143     const errors: string[] = [];
    144     page.on("pageerror", (err) => errors.push(err.message));
    145 
    146     const shot1 = await page.screenshot();
    147     await page.keyboard.press("ArrowLeft");
    148     await page.waitForTimeout(200);
    149     await page.keyboard.press("ArrowLeft");
    150     await page.waitForTimeout(200);
    151     await page.keyboard.press("ArrowLeft");
    152     await page.waitForTimeout(300);
    153     const shot2 = await page.screenshot();
    154 
    155     // The piece should have moved, so the screenshots should differ
    156     expect(
    157       Buffer.from(shot1).equals(Buffer.from(shot2)),
    158       "Expected visual change after pressing left arrow"
    159     ).toBe(false);
    160     expect(errors).toEqual([]);
    161   });
    162 
    163   // ---- 6. Right arrow moves piece right ----
    164   test("right arrow moves piece", async ({ page }) => {
    165     const errors: string[] = [];
    166     page.on("pageerror", (err) => errors.push(err.message));
    167 
    168     const shot1 = await page.screenshot();
    169     await page.keyboard.press("ArrowRight");
    170     await page.waitForTimeout(200);
    171     await page.keyboard.press("ArrowRight");
    172     await page.waitForTimeout(200);
    173     await page.keyboard.press("ArrowRight");
    174     await page.waitForTimeout(300);
    175     const shot2 = await page.screenshot();
    176 
    177     expect(
    178       Buffer.from(shot1).equals(Buffer.from(shot2)),
    179       "Expected visual change after pressing right arrow"
    180     ).toBe(false);
    181     expect(errors).toEqual([]);
    182   });
    183 
    184   // ---- 7. Down arrow moves piece down faster ----
    185   test("down arrow accelerates piece", async ({ page }) => {
    186     const errors: string[] = [];
    187     page.on("pageerror", (err) => errors.push(err.message));
    188 
    189     const shot1 = await page.screenshot();
    190     for (let i = 0; i < 10; i++) {
    191       await page.keyboard.press("ArrowDown");
    192       await page.waitForTimeout(50);
    193     }
    194     await page.waitForTimeout(200);
    195     const shot2 = await page.screenshot();
    196 
    197     expect(
    198       Buffer.from(shot1).equals(Buffer.from(shot2)),
    199       "Expected visual change after pressing down arrow repeatedly"
    200     ).toBe(false);
    201     expect(errors).toEqual([]);
    202   });
    203 
    204   // ---- 8. Up arrow (or Z) rotates piece ----
    205   test("rotation changes the piece", async ({ page }) => {
    206     const errors: string[] = [];
    207     page.on("pageerror", (err) => errors.push(err.message));
    208 
    209     const shot1 = await page.screenshot();
    210     await page.keyboard.press("ArrowUp");
    211     await page.waitForTimeout(300);
    212     const shot2 = await page.screenshot();
    213 
    214     expect(
    215       Buffer.from(shot1).equals(Buffer.from(shot2)),
    216       "Expected visual change after pressing rotate key"
    217     ).toBe(false);
    218     expect(errors).toEqual([]);
    219   });
    220 
    221   // ---- 9. Space bar hard-drops piece ----
    222   test("space bar hard-drops piece", async ({ page }) => {
    223     const errors: string[] = [];
    224     page.on("pageerror", (err) => errors.push(err.message));
    225 
    226     const shot1 = await page.screenshot();
    227     await page.keyboard.press("Space");
    228     await page.waitForTimeout(500);
    229     const shot2 = await page.screenshot();
    230 
    231     expect(
    232       Buffer.from(shot1).equals(Buffer.from(shot2)),
    233       "Expected visual change after pressing space (hard drop)"
    234     ).toBe(false);
    235     expect(errors).toEqual([]);
    236   });
    237 
    238   // ---- 10. Pieces lock at the bottom ----
    239   test("pieces lock at the bottom", async ({ page }) => {
    240     // Hard-drop several pieces and check that the bottom of the board
    241     // accumulates filled cells (the visual should change cumulatively).
    242     const shots: Buffer[] = [];
    243 
    244     shots.push(Buffer.from(await page.screenshot()));
    245 
    246     for (let i = 0; i < 3; i++) {
    247       await page.keyboard.press("Space");
    248       await page.waitForTimeout(800);
    249     }
    250 
    251     shots.push(Buffer.from(await page.screenshot()));
    252 
    253     // After 3 hard drops, the board should look different from the start
    254     // because pieces have stacked up at the bottom.
    255     expect(
    256       shots[0].equals(shots[1]),
    257       "Expected pieces to stack up at the bottom after hard drops"
    258     ).toBe(false);
    259   });
    260 
    261   // ---- 11. New piece spawns after lock ----
    262   test("new piece spawns after locking", async ({ page }) => {
    263     // Hard-drop to lock a piece, then wait and verify the game is still
    264     // showing activity (a new piece should be falling).
    265     await page.keyboard.press("Space");
    266     await page.waitForTimeout(1000);
    267 
    268     const shot1 = await page.screenshot();
    269     await page.waitForTimeout(2000);
    270     const shot2 = await page.screenshot();
    271 
    272     // If a new piece spawned and is falling, the screen should change
    273     expect(
    274       Buffer.from(shot1).equals(Buffer.from(shot2)),
    275       "Expected a new piece to spawn and fall after the previous one locked"
    276     ).toBe(false);
    277   });
    278 
    279   // ---- 12. Multiple different pieces appear ----
    280   test("multiple different pieces appear", async ({ page }) => {
    281     // Play through several pieces and capture screenshots. Different piece
    282     // shapes should produce visually distinct patterns.
    283     const shots: Buffer[] = [];
    284 
    285     for (let i = 0; i < 6; i++) {
    286       // Move each piece to a different column so they don't overlap identically
    287       if (i % 2 === 0) {
    288         await page.keyboard.press("ArrowLeft");
    289         await page.waitForTimeout(100);
    290         await page.keyboard.press("ArrowLeft");
    291         await page.waitForTimeout(100);
    292       } else {
    293         await page.keyboard.press("ArrowRight");
    294         await page.waitForTimeout(100);
    295         await page.keyboard.press("ArrowRight");
    296         await page.waitForTimeout(100);
    297       }
    298       await page.keyboard.press("Space");
    299       await page.waitForTimeout(600);
    300       shots.push(Buffer.from(await page.screenshot()));
    301     }
    302 
    303     // At least some consecutive screenshots should differ (different piece shapes)
    304     let differences = 0;
    305     for (let i = 1; i < shots.length; i++) {
    306       if (!shots[i - 1].equals(shots[i])) differences++;
    307     }
    308 
    309     expect(
    310       differences,
    311       "Expected to see visual differences between consecutive pieces (different shapes)"
    312     ).toBeGreaterThanOrEqual(2);
    313   });
    314 
    315   // ---- 13. Completed line clears ----
    316   test("completed line clears", async ({ page }) => {
    317     // Fill a row by dropping many pieces. Observe whether any row disappears.
    318     // We can detect this by tracking the total filled area -- after a line clear,
    319     // the board should have less filled content than just before the clear.
    320     const pageText = async () =>
    321       (await page.evaluate(() => document.body.innerText)) || "";
    322 
    323     // Drop many pieces rapidly to fill rows
    324     for (let i = 0; i < 30; i++) {
    325       // Vary positions to try to complete a row
    326       const moves = (i % 5) - 2; // -2, -1, 0, 1, 2
    327       for (let m = 0; m < Math.abs(moves); m++) {
    328         await page.keyboard.press(
    329           moves < 0 ? "ArrowLeft" : "ArrowRight"
    330         );
    331         await page.waitForTimeout(50);
    332       }
    333       await page.keyboard.press("Space");
    334       await page.waitForTimeout(300);
    335     }
    336 
    337     // Check if a score or lines counter changed (common indicators of line clears)
    338     const text = await pageText();
    339     const numbers = (text.match(/\d+/g) || []).map(Number);
    340     const hasNonZero = numbers.some((n) => n > 0);
    341 
    342     // Also check visual: the board should not be completely static
    343     const shot1 = await page.screenshot();
    344     await page.waitForTimeout(1000);
    345     const shot2 = await page.screenshot();
    346 
    347     // Either: score/lines increased, or game is still active (meaning lines cleared
    348     // and made room for more pieces instead of game over)
    349     const stillActive = !Buffer.from(shot1).equals(Buffer.from(shot2));
    350 
    351     expect(
    352       hasNonZero || stillActive,
    353       "Expected evidence of line clearing (score > 0 or game still active after many drops)"
    354     ).toBe(true);
    355   });
    356 
    357   // ---- 14. Score increases during play ----
    358   test("score increases during play", async ({ page }) => {
    359     // Look for a score display on the page
    360     const getNumbers = async () => {
    361       const text = (await page.evaluate(() => document.body.innerText)) || "";
    362       return (text.match(/\d+/g) || []).map(Number);
    363     };
    364 
    365     const numbersBefore = await getNumbers();
    366 
    367     // Play for a while: drop several pieces
    368     for (let i = 0; i < 15; i++) {
    369       const offset = (i % 5) - 2;
    370       for (let m = 0; m < Math.abs(offset); m++) {
    371         await page.keyboard.press(offset < 0 ? "ArrowLeft" : "ArrowRight");
    372         await page.waitForTimeout(50);
    373       }
    374       await page.keyboard.press("Space");
    375       await page.waitForTimeout(300);
    376     }
    377 
    378     const numbersAfter = await getNumbers();
    379 
    380     // At least one number on the page should have increased
    381     // (score, lines counter, level, etc.)
    382     let anyIncreased = false;
    383     const maxLen = Math.min(numbersBefore.length, numbersAfter.length);
    384     for (let i = 0; i < maxLen; i++) {
    385       if (numbersAfter[i] > numbersBefore[i]) {
    386         anyIncreased = true;
    387         break;
    388       }
    389     }
    390 
    391     // Also accept if new numbers appeared
    392     if (!anyIncreased && numbersAfter.length > numbersBefore.length) {
    393       anyIncreased = true;
    394     }
    395 
    396     // Also accept if the max number increased
    397     if (!anyIncreased) {
    398       const maxBefore = numbersBefore.length > 0 ? Math.max(...numbersBefore) : 0;
    399       const maxAfter = numbersAfter.length > 0 ? Math.max(...numbersAfter) : 0;
    400       if (maxAfter > maxBefore) anyIncreased = true;
    401     }
    402 
    403     expect(
    404       anyIncreased,
    405       "Expected at least one numeric value on the page to increase during play (score, lines, level)"
    406     ).toBe(true);
    407   });
    408 
    409   // ---- 15. Game over when pieces reach top ----
    410   test("game over when pieces reach top", async ({ page }) => {
    411     // Stack pieces in the center until the game ends.
    412     // Drop as many pieces as possible straight down.
    413     for (let i = 0; i < 50; i++) {
    414       await page.keyboard.press("Space");
    415       await page.waitForTimeout(200);
    416     }
    417 
    418     await page.waitForTimeout(2000);
    419 
    420     // After stacking to overflow, the game should show some game-over indicator:
    421     // - text saying "game over", "you lose", "try again", "restart", "end"
    422     // - or the game stops updating (static screen)
    423     const text = ((await page.evaluate(() => document.body.innerText)) || "").toLowerCase();
    424     const gameOverText =
    425       text.includes("game over") ||
    426       text.includes("gameover") ||
    427       text.includes("you lose") ||
    428       text.includes("try again") ||
    429       text.includes("restart") ||
    430       text.includes("play again") ||
    431       text.includes("ended");
    432 
    433     // Check if the game stopped (screen is static)
    434     const shot1 = await page.screenshot();
    435     await page.waitForTimeout(2000);
    436     const shot2 = await page.screenshot();
    437     const isStatic = Buffer.from(shot1).equals(Buffer.from(shot2));
    438 
    439     expect(
    440       gameOverText || isStatic,
    441       "Expected game-over text or the game to stop after stacking pieces to the top"
    442     ).toBe(true);
    443   });
    444 
    445   // ---- 16. Game runs for 30 seconds without crashing ----
    446   test("game runs for 30 seconds without crashing", async ({ page }) => {
    447     const errors: string[] = [];
    448     page.on("pageerror", (err) => errors.push(err.message));
    449 
    450     // Simulate varied gameplay for 30 seconds
    451     const keys = [
    452       "ArrowLeft",
    453       "ArrowRight",
    454       "ArrowDown",
    455       "ArrowUp",
    456       "Space",
    457     ];
    458     const start = Date.now();
    459 
    460     while (Date.now() - start < 30_000) {
    461       const key = keys[Math.floor(Math.random() * keys.length)];
    462       await page.keyboard.press(key);
    463       await page.waitForTimeout(150 + Math.random() * 200);
    464     }
    465 
    466     // The page should still be alive (not crashed, not blank)
    467     const text = await page.evaluate(() => document.body.innerText);
    468     expect(text, "Page body should not be empty after 30s of play").toBeTruthy();
    469     expect(
    470       errors.length,
    471       `Expected no console errors during 30s of play, got: ${errors.join("; ")}`
    472     ).toBe(0);
    473   });
    474 });

Impressum · Datenschutz