From 189ae3d4e51e3a67ae10d56f35d3a84e6487df77 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 26 Apr 2026 23:39:54 +0700 Subject: [PATCH] =?UTF-8?q?feat(game):=20t=C3=A2n=20t=C3=A2n=20l=C3=B4=20t?= =?UTF-8?q?=C3=B4=20=E2=80=94=20align=20master,=205/col,=20ascending,=20dr?= =?UTF-8?q?aw-order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - master board: 11x9 last-digit aligned (col 0 = 1-9, col 8 = 80-90); fixes missing ones-digit-9 row and aligns 1/11/21/.../81 horizontally - master board: each called cell shows 1-based draw order so host can glance across a winning row to verify "Kinh!" - player card: constraint-aware picker guarantees exactly 5 numbers per row AND per column (was loose weighted random allowing 4-6 per col) - player card: numbers within each column now placed ascending top-to-bottom per lô tô hội chợ tân tân convention --- web/src/lib/game-logic.js | 103 ++++++++++++++--------------- web/src/routes/master/+page.svelte | 46 ++++++++++--- 2 files changed, 85 insertions(+), 64 deletions(-) diff --git a/web/src/lib/game-logic.js b/web/src/lib/game-logic.js index 6baeb94..a6891f1 100644 --- a/web/src/lib/game-logic.js +++ b/web/src/lib/game-logic.js @@ -20,41 +20,6 @@ const NUM_ROWS = 9; const NUM_COLS = 9; const NUM_PER_ROW = 5; -/** - * Weighted random selection of a column index. - * @param {number[]} weights - * @returns {number} - */ -function randomANumberInRow(weights) { - const tempWeight = [...weights]; - for (let i = 1; i < tempWeight.length; i++) { - tempWeight[i] += tempWeight[i - 1]; - } - const rand = Math.floor(Math.random() * tempWeight[tempWeight.length - 1]); - for (let i = 0; i < tempWeight.length; i++) { - if (rand < tempWeight[i]) return i; - } - return 0; -} - -/** - * Select NUM_PER_ROW columns for a row using weighted random. Mutates baseWeight. - * @param {number[]} baseWeight - * @returns {number[]} - */ -function randomARow(baseWeight) { - const tempWeight = [...baseWeight]; - /** @type {number[]} */ - const selectedCols = []; - for (let i = 0; i < NUM_PER_ROW; i++) { - const col = randomANumberInRow(tempWeight); - selectedCols.push(col); - tempWeight[col] = 0; - baseWeight[col]--; - } - return selectedCols; -} - /** * Pick `num` random numbers from column `col`'s range. * @param {number} num @@ -64,37 +29,67 @@ function randomARow(baseWeight) { function randomNumbersInCol(num, col) { const arr = [...NUM_IN_COL[col]]; arr.sort(() => 0.5 - Math.random()); - return arr.slice(0, num); + // Pick `num` at random, then return them ascending so they sit + // top-to-bottom in the column (lô tô hội chợ convention). + return arr.slice(0, num).sort((a, b) => a - b); } /** - * Generate a 9x9 lô tô grid. Cell values: 0 = empty, >0 = number. + * Choose which columns are filled in each row so that every row has exactly + * NUM_PER_ROW filled cells AND every column ends up with exactly NUM_PER_ROW + * filled cells. Forces any column whose remaining quota equals the number of + * rows left — otherwise that column could not reach its target — then picks + * the rest at random from columns with quota > 0. The forced set never + * exceeds NUM_PER_ROW because total remaining quota = NUM_PER_ROW * rowsLeft. + * @returns {number[][]} + */ +function pickFilledCols() { + const quota = new Array(NUM_COLS).fill(NUM_PER_ROW); + /** @type {number[][]} */ + const result = []; + for (let row = 0; row < NUM_ROWS; row++) { + const rowsLeft = NUM_ROWS - row; + /** @type {number[]} */ + const forced = []; + /** @type {number[]} */ + const candidates = []; + for (let col = 0; col < NUM_COLS; col++) { + if (quota[col] === rowsLeft) forced.push(col); + else if (quota[col] > 0) candidates.push(col); + } + for (let i = candidates.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [candidates[i], candidates[j]] = [candidates[j], candidates[i]]; + } + const selected = [ + ...forced, + ...candidates.slice(0, NUM_PER_ROW - forced.length), + ].sort((a, b) => a - b); + for (const col of selected) quota[col]--; + result.push(selected); + } + return result; +} + +/** + * Generate a 9x9 lô tô grid with exactly NUM_PER_ROW filled cells per row + * AND per column. Cell values: 0 = empty, >0 = number. * @returns {number[][]} */ export function generateGrid() { const cell = Array.from({ length: NUM_ROWS }, () => new Array(NUM_COLS).fill(0) ); - const countNumPerCol = new Array(NUM_COLS).fill(0); - const baseWeight = new Array(NUM_COLS).fill(6); - - for (let i = 0; i < NUM_ROWS; i++) { - const newRow = randomARow(baseWeight); - newRow.forEach((col) => { - countNumPerCol[col]++; - cell[i][col] = -1; - }); + const colsPerRow = pickFilledCols(); + for (let row = 0; row < NUM_ROWS; row++) { + for (const col of colsPerRow[row]) cell[row][col] = -1; } - - for (let i = 0; i < NUM_COLS; i++) { - const selectedNum = randomNumbersInCol(countNumPerCol[i], i); - for (let j = 0; j < NUM_ROWS; j++) { - if (cell[j][i] === -1) { - cell[j][i] = selectedNum.shift() ?? 0; - } + for (let col = 0; col < NUM_COLS; col++) { + const picked = randomNumbersInCol(NUM_PER_ROW, col); + for (let row = 0; row < NUM_ROWS; row++) { + if (cell[row][col] === -1) cell[row][col] = picked.shift() ?? 0; } } - return cell; } diff --git a/web/src/routes/master/+page.svelte b/web/src/routes/master/+page.svelte index cd0fb46..c854c07 100644 --- a/web/src/routes/master/+page.svelte +++ b/web/src/routes/master/+page.svelte @@ -2,21 +2,32 @@ const STORAGE_KEY = "loto_master"; /** - * Build the 9x10 board: columns 0-8 map to number ranges 1-9, 10-19, ..., 80-90. + * Build the 11x9 board, aligned by last digit so column N row R holds + * the number whose tens-digit is N (col 0 = ones, col 8 = eighties/90) + * and whose ones-digit is R. Row 0 holds the *0 multiples (10..80), + * rows 1-9 hold 1..9, 11..19, ..., 81..89, and row 10 holds 90 alone. + * Empty slots are 0. * @returns {number[][]} */ function buildBoard() { /** @type {number[][]} */ const board = []; - for (let row = 0; row < 10; row++) { + for (let row = 0; row < 11; row++) { /** @type {number[]} */ const cells = []; for (let col = 0; col < 9; col++) { - const num = col === 0 ? row + 1 : col * 10 + row; - if (col === 0 && row === 9) cells.push(0); - else if (col === 8 && row === 9) cells.push(90); - else if (col > 0 && row === 9) cells.push(0); - else cells.push(num); + let num = 0; + if (row === 10) { + // Only 90 sits in this trailing row, last column. + if (col === 8) num = 90; + } else if (row === 0) { + // First col has no *0 number in 1-9 range; others get 10, 20, ..., 80. + if (col > 0) num = col * 10; + } else { + // rows 1..9 hold digit `row`: col 0 -> row, col N -> N*10 + row. + num = col === 0 ? row : col * 10 + row; + } + cells.push(num); } board.push(cells); } @@ -77,7 +88,11 @@ if (state) saveState(state); }); - const calledSet = $derived(new Set(state?.called ?? [])); + // Map number -> 1-based draw order; lets the master grid show "this + // was the Nth call" for fast Kinh! verification. + const callOrder = $derived( + new Map((state?.called ?? []).map((n, i) => [n, i + 1])), + ); function handleNewGame() { if (state && !confirm("Bạn có muốn tạo ván mới không?")) return; @@ -191,7 +206,9 @@
{#each BOARD_FLAT as num, idx (idx)} {@const hasNumber = num > 0} - {@const isCalled = hasNumber && calledSet.has(num)} + {@const order = hasNumber ? callOrder.get(num) : undefined} + {@const isCalled = order !== undefined} + {@const isLast = isCalled && num === lastCalled}
{hasNumber ? num : ""} + {#if isCalled} + + {order} + + {/if}
{/each}