305	/*    pits      – rozsahy sloupců bez podlahy (propasti)              */
306	/*    platforms – [sloupec, řádek, délka]                             */
307	/*    coins / enemies – [sloupec, řádek]  (nepřítel spadne na zem)    */
308	/* ------------------------------------------------------------------ */
309	const LEVELS = [
310	  {
311	    name: 'Rozcvička', width: 56, floorRow: 12,
312	    playerStart: [2,11], goal: [53,11],
313	    pits: [[21,22],[34,35]],
314	    platforms: [[10,9,4],[26,9,3],[30,8,3],[42,9,4]],
315	    coins: [].concat(cr(10,8,4), cr(16,10,3), cr(26,8,3), cr(31,7,2), cr(42,8,4), cr(48,11,2)),
316	    enemies: [[15,10],[40,10]],
317	  },
318	  {
319	    name: 'Mezery', width: 68, floorRow: 12,
320	    playerStart: [2,11], goal: [65,11],
321	    pits: [[14,16],[24,26],[38,40],[48,50],[58,60]],
322	    platforms: [[18,9,3],[30,9,3],[33,7,3],[43,9,3],[52,10,4]],
323	    coins: [].concat(cr(18,8,3), cr(24,10,3), cr(34,6,2), cr(43,8,3), cr(48,10,3), cr(10,11,2), cr(61,11,3)),
324	    enemies: [[8,10],[20,10],[35,10],[45,10],[54,10],[62,10]],
325	  },
326	  {
327	    name: 'Výstup', width: 80, floorRow: 12,
328	    playerStart: [2,11], goal: [77,11],
329	    pits: [[12,14],[19,21],[27,29],[35,37],[43,46],[52,54],[60,63],[70,72]],
330	    platforms: [[16,10,3],[24,9,3],[32,10,3],[44,10,3],[50,9,3],[61,10,3],[66,9,3]],
331	    coins: [].concat(cr(16,9,3), cr(24,8,3), cr(32,9,3), cr(44,9,3), cr(50,8,3), cr(56,11,2), cr(61,9,3), cr(66,8,2), cr(74,11,2)),
332	    enemies: [[7,10],[17,10],[25,10],[33,10],[40,10],[49,10],[57,10],[66,9],[74,10]],
333	  },
334	  {
335	    name: 'Trhliny', width: 96, floorRow: 12,
336	    playerStart: [2,11], goal: [93,11],
337	    pits: [[10,12],[16,18],[22,25],[30,32],[36,39],[44,46],[50,52],[56,59],[64,66],[70,73],[78,80],[85,88]],
338	    platforms: [[14,10,2],[23,10,2],[28,9,3],[37,10,2],[42,9,3],[48,10,2],[57,10,2],[62,9,3],[71,10,2],[76,9,3],[86,10,2]],
339	    coins: [].concat(cr(14,9,2), cr(23,9,2), cr(28,8,3), cr(37,9,2), cr(42,8,3), cr(48,9,2), cr(57,9,2), cr(62,8,3), cr(71,9,2), cr(76,8,3), cr(83,11,2), cr(90,11,2)),
340	    enemies: [[6,10],[20,10],[28,8],[34,10],[42,8],[48,10],[54,10],[62,8],[68,10],[76,8],[82,10],[91,10]],
341	  },
342	];
343
344	function buildLevel(d, idx = 0){

/*    pits      – rozsahy sloupců bez podlahy (propasti)              */
/*    platforms – [sloupec, řádek, délka]                             */
/*    coins / enemies – [sloupec, řádek]  (nepřítel spadne na zem)    */
/* ------------------------------------------------------------------ */
const LEVELS = [
  {
    name: 'Rozcvička', width: 56, floorRow: 12,
    playerStart: [2,11], goal: [53,11],
    pits: [[21,22],[34,35]],
    platforms: [[10,9,4],[26,9,3],[30,8,3],[42,9,4]],
    coins: [].concat(cr(10,8,4), cr(16,10,3), cr(26,8,3), cr(31,7,2), cr(42,8,4), cr(48,11,2)),
    enemies: [[15,10],[40,10]],
  },
  {
    name: 'Mezery', width: 68, floorRow: 12,
    playerStart: [2,11], goal: [65,11],
    pits: [[14,16],[24,26],[38,40],[48,50],[58,60]],
    platforms: [[18,9,3],[30,9,3],[33,7,3],[43,9,3],[52,10,4]],
    coins: [].concat(cr(18,8,3), cr(24,10,3), cr(34,6,2), cr(43,8,3), cr(48,10,3), cr(10,11,2), cr(61,11,3)),
    enemies: [[8,10],[20,10],[35,10],[45,10],[54,10],[62,10]],
  },
  {
    name: 'Výstup', width: 80, floorRow: 12,
    playerStart: [2,11], goal: [77,11],
    pits: [[12,14],[19,21],[27,29],[35,37],[43,46],[52,54],[60,63],[70,72]],
    platforms: [[16,10,3],[24,9,3],[32,10,3],[44,10,3],[50,9,3],[61,10,3],[66,9,3]],
    coins: [].concat(cr(16,9,3), cr(24,8,3), cr(32,9,3), cr(44,9,3), cr(50,8,3), cr(56,11,2), cr(61,9,3), cr(66,8,2), cr(74,11,2)),
    enemies: [[7,10],[17,10],[25,10],[33,10],[40,10],[49,10],[57,10],[66,9],[74,10]],
  },
  {
    name: 'Trhliny', width: 96, floorRow: 12,
    playerStart: [2,11], goal: [93,11],
    pits: [[10,12],[16,18],[22,25],[30,32],[36,39],[44,46],[50,52],[56,59],[64,66],[70,73],[78,80],[85,88]],
    platforms: [[14,10,2],[23,10,2],[28,9,3],[37,10,2],[42,9,3],[48,10,2],[57,10,2],[62,9,3],[71,10,2],[76,9,3],[86,10,2]],
    coins: [].concat(cr(14,9,2), cr(23,9,2), cr(28,8,3), cr(37,9,2), cr(42,8,3), cr(48,9,2), cr(57,9,2), cr(62,8,3), cr(71,9,2), cr(76,8,3), cr(83,11,2), cr(90,11,2)),
    enemies: [[6,10],[20,10],[28,8],[34,10],[42,8],[48,10],[54,10],[62,8],[68,10],[76,8],[82,10],[91,10]],
  },
];

function buildLevel(d, idx = 0){


1)a 2)a 3)menší vzrůst 4)od středního už nepřidávat 5) trochu od každého


<total_tokens>14993721 tokens left</total_tokens>


362	    if (g >= ROWS) return;
363	    if (tier === 2 && g < fr) tier = 1;            // velká jen na hlavní podlaze
364	    w.enemies.push(mkEnemy(c*TILE + (TILE - ETIER[tier].w) / 2, g*TILE - ETIER[tier].h, tier));
365	  };
366
367	  // ručně rozmístěné příšerky – druh podle postupu v příběhu
368	  for (const [c,r] of (d.enemies || [])) addEnemy(c, r, rollTier(prog));
369
370	  // příšerky navíc – čím dál v příběhu, tím víc
371	  const extra = idx * 2;
372	  for (let k = 0; k < extra; k++){
373	    const c = 6 + Math.floor(Math.random() * Math.max(1, d.width - 12));
374	    if (Math.abs(c - d.playerStart[0]) < 6) continue;
375	    if (d.goal && Math.abs(c - d.goal[0]) < 4) continue;
376	    if ((d.pits || []).some(([a,b]) => c >= a - 1 && c <= b + 1)) continue;
377	    addEnemy(c, fr, rollTier(prog));
378	  }
379
380	  const ps = w.firstSolidRow(d.playerStart[0], d.playerStart[1]);
381	  w.playerStart = { x: d.playerStart[0]*TILE, y: ps*TILE - player.h };
382
383	  const gg = w.firstSolidRow(d.goal[0], d.goal[1]);
384	  w.goal = { x: d.goal[0]*TILE, y: gg*TILE };
385	  return w;

    if (g >= ROWS) return;
    if (tier === 2 && g < fr) tier = 1;            // velká jen na hlavní podlaze
    w.enemies.push(mkEnemy(c*TILE + (TILE - ETIER[tier].w) / 2, g*TILE - ETIER[tier].h, tier));
  };

  // ručně rozmístěné příšerky – druh podle postupu v příběhu
  for (const [c,r] of (d.enemies || [])) addEnemy(c, r, rollTier(prog));

  // příšerky navíc – čím dál v příběhu, tím víc
  const extra = idx * 2;
  for (let k = 0; k < extra; k++){
    const c = 6 + Math.floor(Math.random() * Math.max(1, d.width - 12));
    if (Math.abs(c - d.playerStart[0]) < 6) continue;
    if (d.goal && Math.abs(c - d.goal[0]) < 4) continue;
    if ((d.pits || []).some(([a,b]) => c >= a - 1 && c <= b + 1)) continue;
    addEnemy(c, fr, rollTier(prog));
  }

  const ps = w.firstSolidRow(d.playerStart[0], d.playerStart[1]);
  w.playerStart = { x: d.playerStart[0]*TILE, y: ps*TILE - player.h };

  const gg = w.firstSolidRow(d.goal[0], d.goal[1]);
  w.goal = { x: d.goal[0]*TILE, y: gg*TILE };
  return w;


<total_tokens>14989894 tokens left</total_tokens>


Edit

  // příšerky navíc – čím dál v příběhu, tím víc
  const extra = idx * 2;

  // příšerky navíc – jen kde to level povolí (autoEnemies); od „Střední“ dál 0
  const extra = d.autoEnemies ?? 0;


The file /home/tonda/hra.html has been updated successfully. (file state is current in your context — no need to Read it back)

  // příšerky navíc – čím dál v příběhu, tím víc
  const extra = idx * 2;

  // příšerky navíc – jen kde to level povolí (autoEnemies); od „Střední“ dál 0
  const extra = d.autoEnemies ?? 0;

// ručně rozmístěné příšerky – druh podle postupu v příběhu

for (const [c,r] of (d.enemies || [])) addEnemy(c, r, rollTier(prog));

-  // příšerky navíc – čím dál v příběhu, tím víc

+  // příšerky navíc – jen kde to level povolí (autoEnemies); od „Střední“ dál 0

const c = 6 + Math.floor(Math.random() * Math.max(1, d.width - 12));

if (Math.abs(c - d.playerStart[0]) < 6) continue;


<total_tokens>14989060 tokens left</total_tokens>


-    name: 'Rozcvička', width: 56, floorRow: 12,

+    // úplná rozehřívačka – široké nízké plošinky, 2dlaždicové propasti, spousta mincí, jediná malá příšerka

+    name: 'Rozcvička', width: 48, floorRow: 12,

-    name: 'Mezery', width: 68, floorRow: 12,

+    // Lehký – užší plošinky, propasti 2–3, pár malých příšerek

+    name: 'Lehký', width: 62, floorRow: 12,

-    name: 'Výstup', width: 80, floorRow: 12,

+    // Střední – samé 3dlaždicové propasti, plošinky ob jednu, žádné příšerky

+    name: 'Střední', width: 74, floorRow: 12,

-    name: 'Trhliny', width: 96, floorRow: 12,

+    // Těžký – propasti až 4 dlaždice, doskoky 3–4, minimum plošinek, bez příšerek

+    name: 'Těžký', width: 84, floorRow: 12,

+    // Ultra těžký – nepřetržité 4dlaždicové propasti, doskoky přesně 3, drobné plošinky navíc

+    name: 'Ultra těžký', width: 96, floorRow: 12,

+    // Nemožný – 12 propastí za sebou, každá 5 dlaždic (maximální možný skok), doskoky 3, nic navíc

+    name: 'Nemožný', width: 108, floorRow: 12,


<total_tokens>14985839 tokens left</total_tokens>


Bash

python3 - <<'EOF'
GRAV=0.62; MAXF=16; TILE=36
PW,PH=26,32

def solid_cols_for_pit(pit_a,pit_b):
    # floor everywhere except pit columns; also columns <0 treat solid start
    def isSolid(c,r):
        if r<12 or r>14: return False
        if pit_a<=c<=pit_b: return False
        return True
    return isSolid

def sim(pit_a,pit_b, takeoff_x, hold=True):
    isSolid=solid_cols_for_pit(pit_a,pit_b)
    x=takeoff_x; y=400.0; vy=-13.5; vx=4.4
    onground=False
    for f in range(120):
        # jump already applied (vy=-13.5 at f0)
        if not hold and vy<-4: vy=-4
        vy=min(vy+GRAV,MAXF)
        # move x then resolve x (no walls above floor here), move y then resolve y
        x+=vx
        y+=vy
        # resolve y: check columns spanned
        c1=int(x//TILE); c2=int((x+PW-1)//TILE)
        r1=int(y//TILE); r2=int((y+PH-1)//TILE)
        landed=False
        for r in range(r1,r2+1):
            for c in range(c1,c2+1):
                if isSolid(c,r) and vy>0:
                    y=r*TILE-PH; vy=0; landed=True
        if landed:
            return f, x, y  # landed frame
    return None

for (a,b),label in [((8,12),'Nemožný 5-wide'),((8,11),'Ultra 4-wide'),((23,26),'Těžký 4-wide')]:
    gapw=b-a+1
    # takeoff: right edge at a*TILE -> x = a*TILE - PW ... but must be grounded on col a-1
    tx = a*TILE - PW
    res=sim(a,b,tx,hold=True)
    print(f"{label}: gap {gapw} tiles, takeoff x={tx} (right edge {tx+PW}={a*TILE}), landed:", res,
          "-> lands on col", int((res[1])//TILE) if res else None, "to", int((res[1]+PW-1)//TILE) if res else None,
          "(far solid starts col", b+1, ")")
EOF

Simulate max-distance jumps


Nemožný 5-wide: gap 5 tiles, takeoff x=262 (right edge 288=288), landed: (42, 451.199999999999, 400) -> lands on col 12 to 13 (far solid starts col 13 )
Ultra 4-wide: gap 4 tiles, takeoff x=262 (right edge 288=288), landed: (42, 451.199999999999, 400) -> lands on col 12 to 13 (far solid starts col 12 )
Těžký 4-wide: gap 4 tiles, takeoff x=802 (right edge 828=828), landed: (42, 991.199999999999, 400) -> lands on col 27 to 28 (far solid starts col 27 )


<total_tokens>14983734 tokens left</total_tokens>


python3 - <<'EOF'
GRAV=0.62; MAXF=16; TILE=36
PW,PH=26,32
def isSolid(c,r,a,b):
    if r<12 or r>14: return False
    if a<=c<=b: return False
    return True
def sim(a,b, x0, coyote_frames=0, hold=True):
    x=x0; y=400.0; vy=0.0; vx=4.4
    # coyote: fall a few frames before jump
    for _ in range(coyote_frames):
        vy=min(vy+GRAV,MAXF); y+=vy; x+=vx
    vy=-13.5  # jump
    for f in range(120):
        if not hold and vy<-4: vy=-4
        vy=min(vy+GRAV,MAXF); x+=vx; y+=vy
        c1=int(x//TILE); c2=int((x+PW-1)//TILE)
        r1=int(y//TILE); r2=int((y+PH-1)//TILE)
        for r in range(r1,r2+1):
            for c in range(c1,c2+1):
                if isSolid(c,r,a,b) and vy>0:
                    return coyote_frames, f, round(x,1), round(x+PW,1), b+1  # far solid col
    return coyote_frames, None, None, None, b+1
a,b=8,12
for cf in range(0,5):
    for x0 in (a*TILE-PW, a*TILE-PW-4, a*TILE-PW+2):
        r=sim(a,b,x0,cf)
        need_x = (b+1)*TILE
        ok = r[1] is not None
        print(f"coyote={cf} x0={x0}: landed={ok} rightEdge={r[3]} need>= {need_x}")
EOF

Test 5-wide jump tolerance


coyote=0 x0=262: landed=True rightEdge=477.2 need>= 468
coyote=0 x0=258: landed=True rightEdge=473.2 need>= 468
coyote=0 x0=264: landed=True rightEdge=479.2 need>= 468
coyote=1 x0=262: landed=True rightEdge=481.6 need>= 468
coyote=1 x0=258: landed=True rightEdge=477.6 need>= 468
coyote=1 x0=264: landed=True rightEdge=483.6 need>= 468
coyote=2 x0=262: landed=True rightEdge=486.0 need>= 468
coyote=2 x0=258: landed=True rightEdge=482.0 need>= 468
coyote=2 x0=264: landed=True rightEdge=488.0 need>= 468
coyote=3 x0=262: landed=True rightEdge=490.4 need>= 468
coyote=3 x0=258: landed=True rightEdge=486.4 need>= 468
coyote=3 x0=264: landed=True rightEdge=492.4 need>= 468
coyote=4 x0=262: landed=True rightEdge=494.8 need>= 468
coyote=4 x0=258: landed=True rightEdge=490.8 need>= 468
coyote=4 x0=264: landed=True rightEdge=496.8 need>= 468


<total_tokens>14979667 tokens left</total_tokens>


python3 - <<'EOF'
import re
s=open('/home/tonda/hra.html').read()
js=re.search(r'<script>(.*?)</script>',s,re.S).group(1)
for op,cl in [('(',')'),('{','}'),('[',']')]:
    assert js.count(op)==js.count(cl),(op,js.count(op),js.count(cl))
print('brackets ok')
# quick check: each pit list, landing >=1, no overlap, within width
for m in re.finditer(r"name: '([^']+)', width: (\d+),[\s\S]*?pits: (\[\[[^\]]*\][^\n]*\])", js):
    name,w=m.group(1),int(m.group(2))
    pits=eval(m.group(3))
    w_ok=all(0<a<=b<w for a,b in pits)
    lands=[pits[i+1][0]-pits[i][1]-1 for i in range(len(pits)-1)]
    print(f"{name:12s} w={w} pits={len(pits)} widest={max(b-a+1 for a,b in pits)} minland={min(lands) if lands else '-'} inbounds={w_ok}")
EOF

Validate level geometry


brackets ok
Rozcvička    w=48 pits=2 widest=2 minland=11 inbounds=True
Lehký        w=62 pits=5 widest=3 minland=6 inbounds=True
Střední      w=74 pits=7 widest=3 minland=5 inbounds=True
Těžký        w=84 pits=10 widest=4 minland=3 inbounds=True
Ultra těžký  w=96 pits=12 widest=4 minland=3 inbounds=True
Nemožný      w=108 pits=12 widest=5 minland=3 inbounds=True


<total_tokens>14976638 tokens left</total_tokens>


Edit

Aktivně se dál ladí (2026-08: dvojskok do obchodu + vylepšení za 7 mincí, cena života 1,5×, generování mezer podle max. doskoku). Neměnit bez vyžádání.

Aktivně se dál ladí (2026-08: dvojskok do obchodu + vylepšení za 7 mincí, cena života 1,5×, generování mezer podle max. doskoku, příběhové levely přepsané na 6 s rostoucí obtížností: Rozcvička → Lehký → Střední → Těžký → Ultra těžký → Nemožný, příšerky jen v prvních dvou, `autoEnemies` per-level). Neměnit bez vyžádání.


The file /home/tonda/.claude/projects/-home-tonda/memory/games-projects.md has been updated successfully.

---
name: games-projects
description: "Herní projekty uživatele v /home/tonda — HTML5 hry, jak se spouští a testují"
metadata:
  node_type: memory
  type: project
  originSessionId: ed3caf46-d974-42bc-8521-75e2962ef648
  modified: 2026-08-28T13:19:27.350Z
---

Uživatel staví jednoduché webové hry jako **jeden samostatný `.html` soubor** (canvas + vanilla JS v IIFE, žádné buildy, žádné externí soubory). UI a komentáře **česky**.

Soubory v `/home/tonda/`:
- `hra.html` — „Hranatý svět", 2D plošinovka (příběhové levely + nekonečný běh, obchod, skiny, dotykové ovládání, WebAudio). Aktivně se dál ladí (2026-08: dvojskok do obchodu + vylepšení za 7 mincí, cena života 1,5×, generování mezer podle max. doskoku). Neměnit bez vyžádání.
- `svet.html` — „Svět", zjednodušený Minecraft shora (pohled top-down). Prototyp rozjetý 2026-08-27: seedovaný svět, těžba (držení levého = kop, pravé = polož), tiery krumpáče, crafting menu + verpánek + pec, den/noc, mobové v noci (zombie/kostlivec/pavouk), luk/kuš, život 20 / hlad 10, smrt = drop věcí + respawn, localStorage save. **Další krok: jeskyně z boku** (vstupy `T.cave` už se generují jako značky).
- `neon-drop.html` — starší hra.

Spuštění serveru: `./hra-server.sh` (spustí `python3 -m http.server 8000` na 0.0.0.0, pokud neběží). Testuje se v prohlížeči na `http://192.168.5.32:8000/<soubor>.html`. Claude nemá prohlížeč — po úpravě dát uživateli URL a nechat ho otestovat.

Není tu git. Prostředí: server jako uživatel `tonda`, uživatelův desktop je jiný stroj (`/home/zema/...` cesty ke mně nedosáhnou, `file://` odkazy nefungují).

-- `hra.html` — „Hranatý svět", 2D plošinovka (příběhové levely + nekonečný běh, obchod, skiny, dotykové ovládání, WebAudio). Aktivně se dál ladí (2026-08: dvojskok do obchodu + vylepšení za 7 mincí, cena života 1,5×, generování mezer podle max. doskoku). Neměnit bez vyžádání.

+- `hra.html` — „Hranatý svět", 2D plošinovka (příběhové levely + nekonečný běh, obchod, skiny, dotykové ovládání, WebAudio). Aktivně se dál ladí (2026-08: dvojskok do obchodu + vylepšení za 7 mincí, cena života 1,5×, generování mezer podle max. doskoku, příběhové levely přepsané na 6 s rostoucí obtížností: Rozcvička → Lehký → Střední → Těžký → Ultra těžký → Nemožný, příšerky jen v prvních dvou, `autoEnemies` per-level). Neměnit bez vyžádání.


<total_tokens>14975342 tokens left</total_tokens>


Bash

curl -s -o /dev/null -w "http %{http_code}\n" http://localhost:8000/hra.html

Confirm server serving


http 200


<total_tokens>14975123 tokens left</total_tokens>


Otestuj na **http://192.168.5.32:8000/hra.html** → Příběhové levely.

**Nová struktura – 6 levelů s čistou křivkou** (staré Mezery / Výstup / Trhliny nahrazeny):

| # | Level | Šířka | Propasti | Nejširší | Doskoky | Příšerky |
|---|-------|-------|----------|----------|---------|----------|
| 1 | **Rozcvička** | 48 | 2 | 2 dl. | 11+ dl. | 1 malá |
| 2 | **Lehký** | 62 | 5 | 3 dl. | 6 dl. | 3 + 1 náhodná |
| 3 | **Střední** | 74 | 7 | 3 dl. | 5 dl. | žádné |
| 4 | **Těžký** | 84 | 10 | 4 dl. | 3 dl. | žádné |
| 5 | **Ultra těžký** | 96 | 12 | 4 dl. | 3 dl. | žádné |
| 6 | **Nemožný** | 108 | 12 | **5 dl.** | 3 dl. | žádné |

**Detaily:**
- **Rozcvička zlehčena** „trochu od každého": jen 2 propasti (2 dlaždice místo 2–3), široké nízké plošinky, jediná malá příšerka, hodně mincí jako vodítko na startu.
- **Menší vzrůst délky** (48 → 62 → 74 → 84 → 96 → 108, +~12/level).
- **Nepřátelé jen v Rozcvičce a Lehkém.** Přidal jsem per-level pole `autoEnemies` a nahradil jím dřívější `idx * 2` automatické příšerky (jinak by je Střední+ dostávalo taky).
- **Nemožný** = 12 propastí za sebou, každá **5 dlaždic** = přesně maximální možný skok (ověřeno simulací fyziky: doskok ~189 px na 180 px mezeru, ~9 px na hraně). Dokončitelné bez jediné chyby, žádné plošinky ani pomoc. Dvojskok z obchodu ho zásadně zlidští.
- Ultra těžký a Nemožný se liší jen šířkou propastí (4 vs 5) — jinak stejný nemilosrdný rytmus.


We're editing the game Hranatý svět (hra.html). Done this session: double-jump shop item (20 coins, +1/level upgrade for 7), life price now 1.5x, gap generation capped to max jump distance, and story levels rewritten to 6 (Rozcvička through Nemožný). Next: you test at http://192.168.5.32:8000/hra.html.