Sztuczna inteligencja i analiza danych opierają się na bardzo prostej zasadzie: na podstawie ograniczonej liczby przykładów (danych treningowych) chcemy wyciągać wnioski o zjawiskach, których nie widzimy w całości. Dane treningowe zawierają szum, błędy pomiarowe, przypadkowe odchylenia, braki oraz zależności, których często nie potrafimy bezpośrednio zaobserwować. Dlatego analiza danych nie jest jedynie rachunkiem na tabelach liczb. Jest próbą rozumowania w warunkach niepewności. Rachunek prawdopodobieństwa dostarcza języka, w którym tę niepewność można precyzyjnie opisać.
Mimo że poniższe notatki stanowią materiał do standardowego kursu z teorii prawdopodobieństwa postaramy się dobrze umotywować główny kierunek wykładu. Zaczniemy od przykładu pokazującego że losowe zaburzenia nie są problemem, a narzędziem, które można wykorzystać na swoją korzyść.
1.1 Boarding samolotu
Zaczniemy od zagadnienia wpuszczania pasażerów na pokład samolotu. Poniższa symulacja porównuje trzy kolejności wpuszczania pasażerów: uporządkowani zgodnie z numerami siedzeń, losowo oraz metodą z pracy Steffen (2008).
boarding = {const ROWS =24;const COLS = ["A","B","C","D","E","F"];const AISLE_AFTER =2;const COLORS = {empty:"#d8dee8",seated:"#22c55e",walking:"#2563eb",stowing:"#f59e0b",waiting:"#ef4444",aisle:"#eef2f7",outline:"#7b8798",text:"#344054" };// Powtarzalny generator liczb pseudolosowych.functionrng(seed) {let x = seed >>>0;return () => ((x = (1664525* x +1013904223) >>>0) /4294967296); }functionshuffled(xs, random) {const a = xs.slice();for (let i = a.length-1; i >0;--i) {const j =Math.floor(random() * (i +1)); [a[i], a[j]] = [a[j], a[i]]; }return a; }functionpassengers() {returnArray.from({length: ROWS}, (_, r) => COLS.map((seat, c) => ({id:`${r +1}${seat}`,row:r,col:c, seat})) ).flat(); }functionorderFor(method, random) {const all =passengers();if (method ==="random") returnshuffled(all, random);if (method ==="back") {const ans = [];for (let r = ROWS -1; r >=0;--r) ans.push(...shuffled(all.filter(p => p.row=== r), random));return ans; }// Metoda Steffena: najpierw okna, potem środek, na końcu przejście;// w każdej grupie naprzemienne rzędy od tyłu i obie strony kabiny.const ans = [];for (const pair of [[0,5],[1,4],[2,3]]) {for (const parity of [1,0]) {for (let r = ROWS -1; r >=0;--r) {if (r %2=== parity)for (const c of pair) ans.push(all.find(p => p.row=== r && p.col=== c)); } } }return ans; }functionmakeSim(method, seed) {const random =rng(seed);return { method, random,queue:orderFor(method, random),active:[],seated:newSet(),time:0,done:false,simultaneous:0 }; }functionseatDelay(p, seated) {// 3 jednostki na bagaż oraz dodatkowy czas, gdy trzeba przepuścić// osobę siedzącą bliżej przejścia.let delay =3;if (p.col===0) delay += (seated.has(`${p.row}-2`) ?2:0) + (seated.has(`${p.row}-1`) ?2:0);if (p.col===1) delay += seated.has(`${p.row}-2`) ?2:0;if (p.col===5) delay += (seated.has(`${p.row}-3`) ?2:0) + (seated.has(`${p.row}-4`) ?2:0);if (p.col===4) delay += seated.has(`${p.row}-3`) ?2:0;return delay; }functionstep(sim) {if (sim.done) return; sim.time++;const blocked =newSet(sim.active.filter(p => p.state==="stowing").map(p => p.pos));const occupied =newSet(sim.active.filter(p => p.state!=="seated").map(p => p.pos));// Aktualizujemy od najdalszego pasażera, aby nikt nie przeszedł przez inną osobę.const moving = sim.active.slice().sort((a,b) => b.pos- a.pos);for (const p of moving) {if (p.state==="stowing") { p.wait--;if (p.wait<=0) { p.state="seated"; sim.seated.add(`${p.row}-${p.col}`); occupied.delete(p.pos); } } elseif (p.state==="walking") {if (p.pos=== p.row) { p.state="stowing"; p.wait=seatDelay(p, sim.seated); } else {const next = p.pos+1;if (!occupied.has(next) &&!blocked.has(next)) { occupied.delete(p.pos); p.pos= next; occupied.add(p.pos); } } } } sim.active= sim.active.filter(p => p.state!=="seated");// Jedna osoba może wejść w każdej jednostce czasu, jeśli początek przejścia jest wolny.if (sim.queue.length&&!occupied.has(-1) &&!occupied.has(0)) {const p = sim.queue.shift(); sim.active.push({...p,pos:-1,state:"walking",wait:0}); } sim.done= sim.queue.length===0&& sim.active.length===0; }const methods = [ {id:"back",label:"Od tyłu do przodu"}, {id:"random",label:"Kolejność losowa"}, {id:"steffen",label:"Metoda Steffena"} ];constroot=html`<div> <div class="boarding-controls"> <button class="primary" data-action="start">▶ Start</button> <button data-action="pause">Ⅱ Pauza</button> <button data-action="reset">↺ Od początku</button> <label>Tempo <input data-speed type="range" min="1" max="10" value="5"></label> <label>Ziarno <input data-seed type="number" min="1" max="9999" value="2026" style="width:5.5rem"></label> </div> <div class="boarding-grid"></div> <div class="boarding-legend"> <span class="boarding-key"><i class="boarding-dot" style="background:${COLORS.walking}"></i> idzie</span> <span class="boarding-key"><i class="boarding-dot" style="background:${COLORS.stowing}"></i> odkłada bagaż</span> <span class="boarding-key"><i class="boarding-dot" style="background:${COLORS.seated}"></i> siedzi</span> </div> </div>`;const grid =root.querySelector(".boarding-grid");const panels =newMap();for (const m of methods) {const panel =html`<section class="boarding-panel"><h3>${m.label}</h3><div class="boarding-status"></div><svg viewBox="0 0 250 666" role="img" aria-label="Symulacja: ${m.label}"></svg></section>`; grid.append(panel); panels.set(m.id, panel); }let sims, timer =null;functionreset() {pause();const seed =+root.querySelector("[data-seed]").value||2026; sims = methods.map((m,i) =>makeSim(m.id, seed +101* i));render(); }functionstart() {if (timer || sims.every(s => s.done)) return;const loop = () => {for (const s of sims) step(s);render();if (sims.every(s => s.done)) pause(); };const speed =+root.querySelector("[data-speed]").value; timer =setInterval(loop,310-27* speed); }functionpause() { if (timer) clearInterval(timer); timer =null; }functionrender() {for (const sim of sims) {const panel = panels.get(sim.method), svg = panel.querySelector("svg");const y = r =>625- r *24;let marks =`<rect x="64" y="32" width="122" height="630" rx="55" fill="none" stroke="${COLORS.outline}" stroke-width="2"/>`; marks +=`<rect x="116" y="42" width="18" height="590" fill="${COLORS.aisle}"/>`;for (let r=0; r<ROWS;++r) { marks +=`<text x="51" y="${y(r)+5}" text-anchor="end" font-size="9" fill="${COLORS.text}">${r+1}</text>`;for (let c=0; c<6;++c) {const x =70+ c*16+ (c>AISLE_AFTER ?18:0);const fill = sim.seated.has(`${r}-${c}`) ? COLORS.seated: COLORS.empty; marks +=`<rect x="${x}" y="${y(r)-8}" width="12" height="16" rx="2" fill="${fill}"/>`; } }for (const p of sim.active) {const cy = p.pos<0?646:y(p.pos);const fill = p.state==="stowing"? COLORS.stowing: COLORS.walking; marks +=`<circle cx="125" cy="${cy}" r="6" fill="${fill}" stroke="white" stroke-width="1"><title>${p.id}</title></circle>`; } svg.innerHTML= marks;const seated = sim.seated.size; panel.querySelector(".boarding-status").textContent= sim.done?`Koniec: ${sim.time} jednostek czasu`:`czas ${sim.time} · siedzi ${seated}/144`; } }root.querySelector("[data-action=start]").onclick= start;root.querySelector("[data-action=pause]").onclick= pause;root.querySelector("[data-action=reset]").onclick= reset;root.querySelector("[data-speed]").onchange= () => { const wasRunning =!!timer;pause();if (wasRunning) start(); };root.querySelector("[data-seed]").onchange= reset; invalidation.then(pause);reset();returnroot;}
Czy potrafisz uzasadnić dlaczego metoda uporządkowana wypada słabiej niż losowa kolejność? Czy potrafisz uzasadnić, że metoda Steffena jest optymalna?
1.2 Półmaraton w Tokio
Kolejny przykład pochodzi z Tokio, gdzie w październiku 2024 roku odbył się Hakone Ekiden Yosenkai - półmaraton (bieg na 21.0975 km) kwalifikacyjny do wyścigu Hakone Ekiden (bieg sztafetowy). Oczywiście spodziewamy się, że znakomita większość zawodników uplasuje się w połowie stawki i kilka najszybszych lub najwolniejszych osób będzie odstawało odpowiednio na jej początku i końcu.
Okazuje się jednak, że dokładny rozkład biegaczy jest bardzo regularny. Zaobserwowana krzywa to funkcja dzwonowa, która jest równa \[\begin{equation*}
\exp \left(-(x-\mu)^2/(2\sigma^2)\right)/\sqrt{2\pi\sigma^2}
\end{equation*}\] dla pewnych parametrów \(\sigma>0\) oraz \(\mu \in \mathbb{R}\). W trakcie wykładu nauczymy się szukać tego typu regularności i zobaczymy dokładnie skąd bierze się zaobserwowany kształt. Parametry \(\mu\) oraz \(\sigma\) są odpowiednio odpowiedzialne za przesunięcie i kształt krzywej.
Rozważmy filmy nakręcone w latach \(2000-2025\), które obejrzało co najmniej \(1000\) osób. Każdy film ma swoją ocenę w serwisie IMDb. Co zobaczymy, gdy zamiast pojedynczych tytułów przyjrzymy się tysiącom filmów jednocześnie? Jedna obserwacja to jeden film i jego ocena w IMDb.
Pokaż kod
imdbRows =FileAttachment("CSV/imdb_filmy_2000_2025_min1000.csv").csv({typed:true})imdbRatings = imdbRows.map(d => d.averageRating).filter(x =>Number.isFinite(x) && x >=1&& x <=10)// Ustalona losowa kolejność: zwiększenie próby dodaje kolejne filmy.imdbOrder = {const a = imdbRatings.slice();let seed =20261006;for (let i = a.length-1; i >0;--i) { seed = (Math.imul(1664525, seed) +1013904223) >>>0;const j =Math.floor((seed /4294967296) * (i +1)); [a[i], a[j]] = [a[j], a[i]]; }return a;}viewof imdbN = Inputs.range([50, imdbOrder.length], {value: imdbOrder.length,step:1,label:"Liczba filmów"})viewof imdbShowNormal = Inputs.toggle({value:true,label:"Pokaż krzywą normalną"})imdbSample = imdbOrder.slice(0, imdbN)imdbStats = {const n = imdbSample.length;const mean = imdbSample.reduce((s, x) => s + x,0) / n;const sd =Math.sqrt(imdbSample.reduce((s, x) => s + (x - mean) **2,0) / n);return {n, mean, sd};}// Każdy przedział obejmuje dwie oceny, np. 6,0 i 6,1.imdbBins = {const bins =Array.from({length:46}, (_, i) => ({left:0.95+0.2* i,right:1.15+0.2* i,count:0 }));for (const x of imdbSample) { bins[Math.floor((Math.round(x *10) -10) /2)].count++; }return bins;}// Gęstość normalna przeskalowana do liczebności słupków.imdbCurve =Array.from({length:601}, (_, i) => {const x =0.8+9.4* i /600;const {n, mean, sd} = imdbStats;const y = n *0.2/ (sd *Math.sqrt(2*Math.PI))*Math.exp(-0.5* ((x - mean) / sd) **2);return {x, y};})md`**Liczba filmów:** ${imdbStats.n.toLocaleString("pl-PL")} ·**Średnia:** ${imdbStats.mean.toLocaleString("pl-PL", {minimumFractionDigits:2,maximumFractionDigits:2})} ·**Odchylenie standardowe:** ${imdbStats.sd.toLocaleString("pl-PL", {minimumFractionDigits:2,maximumFractionDigits:2})}`
Niebieskie słupki przedstawiają rzeczywiste oceny filmów. Pomarańczowa linia to krzywa normalna o tej samej średniej i odchyleniu standardowym co wybrana próba.
Ostateczny histogram przypomina dzwon, ale ma dłuższy lewy ogon. Krzywa normalna jest więc przybliżeniem. Zwiększanie liczby filmów pozwala lepiej zobaczyć rozkład ocen — nie zmienia go automatycznie w rozkład normalny.
1.4 Opóźnienia lotów
Przyjrzyjmy się lotom z nowojorskich lotnisk w 2013 roku. Możemy porównać odchylenia przylotów od rozkładu albo czasy spędzone w powietrzu na jednej trasie: JFK–Los Angeles.
Pokaż kod
flightRows =FileAttachment("CSV/loty_nyc_2013.csv").csv({typed:true})viewof flightMode = Inputs.select(["Czas lotu JFK–Los Angeles","Opóźnienia wszystkich lotów","Przyloty po punktualnym lub wcześniejszym wylocie"], {label:"Wybierz dane"})viewof flightShowNormal = Inputs.toggle({value:true,label:"Pokaż krzywą normalną"})flightConfig = flightMode ==="Czas lotu JFK–Los Angeles"? {column:"air_time",label:"Czas spędzony w powietrzu (min)"}: {column:"arr_delay",label:"Odchylenie przylotu od rozkładu (min)"}flightValues = {let rows = flightRows;if (flightMode ==="Czas lotu JFK–Los Angeles") { rows = rows.filter(d => d.origin==="JFK"&& d.dest==="LAX"); } elseif (flightMode ==="Przyloty po punktualnym lub wcześniejszym wylocie") { rows = rows.filter(d =>Number.isFinite(d.dep_delay) && d.dep_delay<=0); }return rows.map(d => d[flightConfig.column]).filter(Number.isFinite);}// Stała losowa kolejność: zwiększenie próby dodaje kolejne loty.flightOrder = {const a = flightValues.slice();let seed =20261006;for (let i = a.length-1; i >0;--i) { seed = (Math.imul(1664525, seed) +1013904223) >>>0;const j =Math.floor((seed /4294967296) * (i +1)); [a[i], a[j]] = [a[j], a[i]]; }return a;}viewof flightN = Inputs.range([50, flightOrder.length], {value: flightOrder.length,step:1,label:"Liczba lotów"})flightSample = flightOrder.slice(0, flightN)flightStats = {const n = flightSample.length;const mean = flightSample.reduce((s, x) => s + x,0) / n;const sd =Math.sqrt(flightSample.reduce((s, x) => s + (x - mean) **2,0) / n);return {n, mean, sd};}// Oś obejmuje całą wybraną grupę, także przy małej próbie.flightBounds = {let min =Infinity, max =-Infinity;for (const x of flightValues) { min =Math.min(min, x); max =Math.max(max, x); }return {left:5*Math.floor(min /5) -2.5,right:5*Math.ceil(max /5) +2.5,step:5 };}flightBins = {const {left, right, step} = flightBounds;const bins =Array.from({length:Math.round((right - left) / step)}, (_, i) => ({left: left + i * step,right: left + (i +1) * step,count:0 }));for (const x of flightSample) { bins[Math.floor((x - left) / step)].count++; }return bins.map(d => ({...d,percent:100* d.count/ flightStats.n}));}flightCurve =Array.from({length:1001}, (_, i) => {const {left, right, step} = flightBounds;const {mean, sd} = flightStats;const x = left + (right - left) * i /1000;const y =100* step / (sd *Math.sqrt(2*Math.PI))*Math.exp(-0.5* ((x - mean) / sd) **2);return {x, y};})md`**Liczba lotów:** ${flightStats.n.toLocaleString("pl-PL")} ·**Średnia:** ${flightStats.mean.toLocaleString("pl-PL", {minimumFractionDigits:1,maximumFractionDigits:1})} min ·**Odchylenie standardowe:** ${flightStats.sd.toLocaleString("pl-PL", {minimumFractionDigits:1,maximumFractionDigits:1})} min`
Niebieskie słupki przedstawiają rzeczywiste dane w przedziałach szerokości 5 minut. Pomarańczowa linia to krzywa normalna o tej samej średniej i odchyleniu standardowym co wybrana próba.
W wykresach przylotów wartości ujemne oznaczają przybycie przed planowaną godziną. W wariancie z punktualnym wylotem uwzględniamy loty, które wystartowały o czasie lub wcześniej. W każdym wariancie pomijamy brakujące wartości badanej zmiennej.
1.5 Deska Galtona
Kolejny przykład to deska Galtona zaprezentowana poniżej. Kule spadają po kołkach za każdym razem odbijając się losowo w lewo bądź prawo. Poniżej widzimy histogram (znormalizowany wykres słupkowy) liczby kul, które opuściły deskę przez dane miejsce.
Ideą twierdzeń granicznych jest wyodrębnienie deterministycznego stwierdzenia o losowym układzie. Nie każde takie twierdzenie prowadzi do krzywej dzwonowej. W losowych parkietażach zobaczymy okrąg.
Dla naturalnego \(n\) rozważmy diament aztecki rzędu \(n\). Powstaje on z szachownicy \(2n\times 2n\) przez usunięcie czterech schodkowych rogów. Długości kolejnych, wyśrodkowanych wierszy wynoszą
Chcemy pokryć cały diament kostkami domina o wymiarach \(2\times1\) lub \(1\times2\), bez nakładania kostek i bez pozostawiania pustych pól. Diament zawiera \(2n(n+1)\) pól, więc każdy parkietaż składa się z \(n(n+1)\) kostek. Spośród wszystkich dopuszczalnych parkietaży losujemy jeden. Każdy parkietaż wybieramy z jednakowym prawdopodobieństwem.
Pokaż kod
{const MAX =200;const DX = [0,0,-1,1], DY = [-1,1,0,0];const FALLBACK_COLORS = ["#e2e3e5","#9ec5fe"];// Tablice są wielokrotnie używane; brak obiektów dla pojedynczych kostek.class Aztec {constructor(limit, random =Math.random) {this.limit= limit;this.random= random;this.side=2* limit +2;this.offset= limit +1;const capacity = limit * (limit +1);this.map=newInt32Array(this.side*this.side);this.x=newInt16Array(capacity);this.y=newInt16Array(capacity);this.d=newUint8Array(capacity);this.bad=newUint8Array(capacity);this.reset(); }reset() {this.n=this.count=0;this.map.fill(-1); }index(x, y) {return (y +this.offset) *this.side+ x +this.offset; }put(i) {const p =this.index(this.x[i],this.y[i]);this.map[p] =this.map[p + (this.d[i] <2?1:this.side)] = i +1; }add(x, y, d) {const i =this.count++;this.x[i] = x;this.y[i] = y;this.d[i] = d;this.put(i); }step() {if (this.n>=this.limit) returnnull;// Najpierw zaznaczamy WSZYSTKIE kolizje, bez zmiany starego układu.for (let i =0; i <this.count; i++) {const d =this.d[i];const j =this.map[this.index(this.x[i] + DX[d],this.y[i] + DY[d])] -1;this.bad[i] = j >=0&&this.d[j] === (d ^1) &&this.map[this.index(this.x[j] + DX[this.d[j]],this.y[j] + DY[this.d[j]])] === i +1; }// Usuwamy kolizje i przesuwamy pozostałe kostki o jedno pole.let kept =0;for (let i =0; i <this.count; i++) {if (this.bad[i]) continue;const d =this.d[i];this.x[kept] =this.x[i] + DX[d];this.y[kept] =this.y[i] + DY[d];this.d[kept++] = d; }this.count= kept;this.n++;// Czyścimy tylko pola diamentu, nie całą prostokątną tablicę.for (let y =-this.n; y <this.n; y++) {const half =this.n-Math.floor(Math.abs(y +0.5));this.map.fill(0,this.index(-half, y),this.index(half, y)); }for (let i =0; i < kept; i++) this.put(i);// Puste pola tworzą bloki 2×2. W każdym niezależnie rzucamy monetą.for (let y =-this.n; y <this.n; y++) {const half =this.n-Math.floor(Math.abs(y +0.5));let p =this.index(-half, y);for (let x =-half; x < half; x++, p++) {if (this.map[p] !==0) continue;if (this.random() <0.5) {this.add(x, y,0);this.add(x, y +1,1); } else {this.add(x, y,2);this.add(x +1, y,3); } } } } }constroot=document.createElement("div");root.className="aztec-demo";root.innerHTML=` <style> .aztec-demo {max-width:680px;margin:1rem auto} .aztec-demo .aztec-controls,.aztec-demo .aztec-legend { display:flex;flex-wrap:wrap;gap:.5rem;align-items:center;margin:.7rem 0 } .aztec-demo button { font:inherit;color:inherit;background:var(--bs-body-bg,#fff); border:1px solid #94a3b8;border-radius:.3rem;padding:.25rem .65rem;cursor:pointer } .aztec-demo button:disabled {opacity:.45;cursor:default} .aztec-demo label {display:inline-flex;gap:.3rem;align-items:center} .aztec-demo canvas { display:block;width:100%;height:auto;aspect-ratio:1; background:var(--bs-body-bg,#fff) } .aztec-demo .aztec-swatch {display:inline-block;width:.9em;height:.9em;margin-right:.25em} .aztec-demo .aztec-horizontal {background:var(--bs-secondary-bg,#e2e3e5)} .aztec-demo .aztec-vertical {background:var(--bs-primary-border-subtle,#9ec5fe)} .aztec-demo .aztec-legend {font-size:.9em;gap:1rem} .aztec-demo .aztec-info {min-height:1.6em;font-variant-numeric:tabular-nums} </style> <div class="aztec-controls">${[3,4,5,10,50,100,200].map(n =>`<button type="button" data-n="${n}">n=${n}</button>`).join("")} </div> <canvas width="600" height="600" role="img" aria-label="Losowy parkietaż diamentu azteckiego"></canvas> <div class="aztec-legend"> <span><i class="aztec-swatch aztec-horizontal"></i>Kostki poziome</span> <span><i class="aztec-swatch aztec-vertical"></i>Kostki pionowe</span> </div> <div class="aztec-controls"> <button type="button" data-action="random">Nowe losowanie</button> <label><input type="checkbox" data-option="circle" checked> Okrąg graniczny</label> </div> <div class="aztec-info" role="status" aria-live="polite"></div>`;const canvas =root.querySelector("canvas"), ctx = canvas.getContext("2d");const info =root.querySelector(".aztec-info");const circle =root.querySelector('[data-option="circle"]');const randomButton =root.querySelector('[data-action="random"]');const swatches = [...root.querySelectorAll(".aztec-swatch")];const model =newAztec(MAX);let generating =false, alive =true, job =0;functionthemePalette() {const style =getComputedStyle(root);const color = (names, fallback) => {for (const name of names) {const value = style.getPropertyValue(name).trim();if (value) return value; }return fallback; };return {fills: [color(["--bs-secondary-bg","--bs-tertiary-bg"], FALLBACK_COLORS[0]),color(["--bs-primary-border-subtle","--bs-primary-bg-subtle"], FALLBACK_COLORS[1]) ],background:color(["--bs-body-bg"],"#fff"),border:color(["--bs-border-color"],"#adb5bd"),ink:color(["--bs-body-color"],"#212529") }; }functionupdate() { info.textContent=`n = ${model.n} · kostki domina: ${model.count.toLocaleString("pl-PL")}`; canvas.setAttribute("aria-label",`Parkietaż diamentu rzędu ${model.n}, ${model.count} kostek domina`); }functiondraw() {// Rysujemy wyłącznie gotowy parkietaż, również przy zmianie rozmiaru okna.if (generating ||!alive) return;const size =Math.max(1,Math.round(canvas.getBoundingClientRect().width||600));const ratio =Math.min(window.devicePixelRatio||1,2);const pixels =Math.round(size * ratio);if (canvas.width!== pixels) canvas.width= canvas.height= pixels; ctx.setTransform(ratio,0,0, ratio,0,0); ctx.clearRect(0,0, size, size); ctx.translate(size /2, size /2);const scale =Math.max(1, size -24) / (2*Math.max(1, model.n)); ctx.scale(scale, scale);const palette =themePalette(); canvas.style.backgroundColor= palette.background; swatches.forEach((swatch, i) => swatch.style.backgroundColor= palette.fills[i]);// Dwie ścieżki i dwa wypełnienia: osobno kostki poziome i pionowe.for (let orientation =0; orientation <2; orientation++) { ctx.beginPath();for (let i =0; i < model.count; i++) {const horizontal = model.d[i] <2;if (Number(!horizontal) !== orientation) continue; ctx.rect(model.x[i], model.y[i], horizontal ?2:1, horizontal ?1:2); } ctx.fillStyle= palette.fills[orientation]; ctx.fill();if (scale >=5) { ctx.strokeStyle= palette.border; ctx.lineWidth=0.55/ scale; ctx.stroke(); } }if (circle.checked&& model.n>0) { ctx.setLineDash([5/ scale,4/ scale]); ctx.beginPath(); ctx.arc(0,0, model.n/Math.SQRT2,0,2*Math.PI); ctx.strokeStyle= palette.background; ctx.lineWidth=3.5/ scale; ctx.stroke(); ctx.strokeStyle= palette.ink; ctx.lineWidth=1.4/ scale; ctx.stroke(); ctx.setLineDash([]); } }asyncfunctiongenerate(n) {const currentJob =++job; model.reset(); generating =true; randomButton.disabled=true; info.textContent=`Losowanie diamentu rzędu ${n}…`;// Obliczamy w krótkich porcjach, ale nie pokazujemy etapów pośrednich.while (alive && currentJob === job && model.n< n) {const deadline =performance.now() +12;do model.step();while (model.n< n &&performance.now() < deadline);if (model.n< n) awaitnewPromise(resolve =>requestAnimationFrame(resolve)); }if (!alive || currentJob !== job) return; generating =false; randomButton.disabled=false;draw();update(); }root.addEventListener("click",event=> {const button =event.target.closest("button");if (!button || button.disabled) return;if (button.dataset.n) { generate(Number(button.dataset.n));return; }if (button.dataset.action==="random") generate(model.n); });root.addEventListener("change", () =>draw());const observer =newResizeObserver(() =>draw()); observer.observe(canvas);const themeObserver =newMutationObserver(() =>draw()); themeObserver.observe(document.documentElement, {attributes:true}); themeObserver.observe(document.body, {attributes:true}); invalidation.then(() => { alive =false; job++; observer.disconnect(); themeObserver.disconnect(); });generate(4);returnroot;}
Wybierz \(n=100\) lub \(n=200\) i wykonaj kilka nowych losowań. Szczegóły za każdym razem się zmieniają, ale ogólny obraz pozostaje podobny. Przy czterech rogach pojawiają się zamarznięte obszary, w których kostki tworzą regularny wzór. W środku orientacje są wymieszane i tworzą okrąg.