From 65de6db4f887c3b09bfa57e7480ab5f656236d9a Mon Sep 17 00:00:00 2001 From: TKHR-Shiu Date: Thu, 20 Nov 2025 22:32:12 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Score=E3=83=86=E3=83=B3=E3=83=97=E3=83=AC?= =?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=AE=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/assets/life-game-rules/score.js | 366 ++++++++++++++++++++++++ src/lib/rules-explanation.ts | 12 + src/routes/+page.svelte | 41 ++- 3 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 src/lib/assets/life-game-rules/score.js diff --git a/src/lib/assets/life-game-rules/score.js b/src/lib/assets/life-game-rules/score.js new file mode 100644 index 0000000..c734d15 --- /dev/null +++ b/src/lib/assets/life-game-rules/score.js @@ -0,0 +1,366 @@ +"use strict"; + +//ルール +//スコア計算は生きているセルの総数が増加した時、増加分だけ加算されます +//減少した時は、スコアは変化しません +//スコアが計算され始めてからセルを直接変化させるとエラーが出てしまいます +//生きているセルがなくなるか、ループが起きたら計算終了です + +let timer = "stop"; +let generationFigure = 0; +let isDragging = false; +let dragMode = 0; // 1: 黒にする, 0: 白にする +let isPlacingTemplate = false; +let patternShape = []; +let patternHeight = 0; +let patternWidth = 0; +let previewCells = []; +let previousBoard = []; +let score = 0; + +//変数設定 +let boardSize = 20; //盤面の大きさ(20x20) +const cellSize = 600 / boardSize; //セルの大きさ(px) + +// around: 周囲の生きたセル数 self: 自身が生きているかどうか +function isNextAlive(around, self) { + // 自身が生きている & 周囲が 2 か 3 で生存 + if (self && 2 <= around && around <= 3) { + return self; + } + // 自身が死んでいる & 周囲が 3 で誕生 + if (!self && around === 3) { + return 1; + } + return 0; +} + +// cellの状態に応じた色を返す関数 +function getStyle(cell) { + if (cell === 0) return "white"; + // cellの値に応じて色を返す場合はここに追加 + return "black"; // デフォルトは黒 +} + +//Boardの初期化 +let board = Array.from({ length: boardSize }, () => Array.from({ length: boardSize }, () => 0)); +const table = document.getElementById("game-board"); + +//盤面をBoardに従って変更する関数達(Boardを変更したら実行する) +function renderBoard() { + // 初回の盤面生成 + table.innerHTML = ""; + for (let i = 0; i < boardSize; i++) { + const tr = document.createElement("tr"); + tr.style.padding = "0"; + for (let j = 0; j < boardSize; j++) { + const td = document.createElement("td"); + td.style.padding = "0"; + const button = document.createElement("button"); + button.style.backgroundColor = board[i][j] ? "black" : "white"; //Boardの対応する値によって色を変更 + // ボードが大きいときは border をつけない + if (boardSize >= 50) { + button.style.border = "none"; + table.style.border = "1px solid black"; + } else { + button.style.border = "0.5px solid black"; + } + button.style.width = `${cellSize}px`; + button.style.height = `${cellSize}px`; + button.style.padding = "0"; //cellSizeが小さいとき、セルが横長になることを防ぐ + button.style.display = "block"; //cellSizeが小さいとき、行間が空きすぎるのを防ぐ + button.onclick = () => { + if (isPlacingTemplate) { + clearPreview(); + isPlacingTemplate = false; + if (i + patternHeight < boardSize + 1 && j + patternWidth < boardSize + 1) { + for (let r = 0; r < patternHeight; r++) { + for (let c = 0; c < patternWidth; c++) { + const boardRow = i + r; + const boardCol = j + c; + board[boardRow][boardCol] = patternShape[r][c]; + } + } + if (generationFigure !== 0) { + scoreReset(); + } + rerender(); + generationChange(0); + stop(); + } else { + window.parent.postMessage( + { + type: "Size shortage", + data: {}, + }, + "*", + ); + } + } + }; + button.onmousedown = (e) => { + e.preventDefault(); + if (timer === "stop" && !isPlacingTemplate) { + isDragging = true; + board[i][j] = !board[i][j]; + dragMode = board[i][j]; + button.style.backgroundColor = board[i][j] ? "black" : "white"; + if (generationFigure > 1) { + scoreReset(); + } + } + }; + button.onmouseenter = () => { + if (isDragging && timer === "stop" && board[i][j] !== dragMode && !isPlacingTemplate) { + board[i][j] = dragMode; + button.style.backgroundColor = board[i][j] ? "black" : "white"; + } + if (isPlacingTemplate) { + drawPreview(i, j); + } + }; + td.appendChild(button); + tr.appendChild(td); + } + table.appendChild(tr); + } +} + +table.onmouseleave = () => { + if (isPlacingTemplate) { + clearPreview(); + } +}; + +function drawPreview(row, col) { + clearPreview(); + for (let r = 0; r < patternHeight; r++) { + for (let c = 0; c < patternWidth; c++) { + if (patternShape[r][c] === 1) { + const boardRow = row + r; + const boardCol = col + c; + if (boardRow < boardSize && boardCol < boardSize) { + const cell = table.rows[boardRow].cells[boardCol].firstChild; + cell.style.backgroundColor = "gray"; + previewCells.push({ row: boardRow, col: boardCol }); + } + } + } + } +} + +function clearPreview() { + previewCells.forEach((cellPos) => { + const cell = table.rows[cellPos.row].cells[cellPos.col].firstChild; + cell.style.backgroundColor = board[cellPos.row][cellPos.col] ? "black" : "white"; + }); + previewCells = []; +} + +function rerender() { + // 2回目以降の盤面生成 + for (let i = 0; i < boardSize; i++) { + for (let j = 0; j < boardSize; j++) { + const button = table.children[i].children[j].children[0]; + + // 色の更新 + const currentCellColor = button.style.backgroundColor; + const expectedCellColor = getStyle(board[i][j]); + if (currentCellColor !== expectedCellColor) { + button.style.backgroundColor = expectedCellColor; + } + + // セルサイズの更新 + const currentCellsize = button.style.width; + const expectedCellsize = `${cellSize}px`; + if (currentCellsize !== expectedCellsize) { + button.style.width = expectedCellsize; + button.style.height = expectedCellsize; + } + } + } +} + +document.addEventListener("mouseup", () => { + isDragging = false; +}); + +renderBoard(); +progressBoard(); + +function scoreReset() { + if (score === 0) return; + previousBoard = []; + window.parent.postMessage( + { + type: "Score reset", + data: score, + }, + "*", + ); + + score = 0; +} + +function generationChange(num) { + //現在の世代を表すgenerationFigureを変更し、文章も変更 + generationFigure = num; + window.parent.postMessage( + { + type: "generation_change", + data: generationFigure, + }, + "*", + ); +} + +function progressBoard() { + const newBoard = structuredClone(board); + for (let i = 0; i < boardSize; i++) { + for (let j = 0; j < boardSize; j++) { + //周囲のマスに黒マスが何個あるかを計算(aroundに格納)↓ + let around = 0; + let tate, yoko; + if (i === 0) { + tate = [0, 1]; + } else if (i === boardSize - 1) { + tate = [0, -1]; + } else { + tate = [-1, 0, 1]; + } + if (j === 0) { + yoko = [0, 1]; + } else if (j === boardSize - 1) { + yoko = [0, -1]; + } else { + yoko = [-1, 0, 1]; + } + for (let ii = 0; ii < tate.length; ii++) { + for (let jj = 0; jj < yoko.length; jj++) { + if (tate[ii] !== 0 || yoko[jj] !== 0) { + around += board[i + tate[ii]][j + yoko[jj]] !== 0 ? 1 : 0; + } + } + } + //↑周囲のマスに黒マスが何個あるかを計算(aroundに格納) + newBoard[i][j] = isNextAlive(around, board[i][j]); + } + } + + // すべて白マスでないか確認 + const isAllWhite = newBoard.every((row) => row.every((cell) => cell === 0)); + if (isAllWhite) { + // generationFigure が 0 の場合は、初期状態なのでメッセージを送らない + if (generationFigure === 0) { + return; + } + window.parent.postMessage( + { + type: "Score white", + data: score, + }, + "*", + ); + stop(); + timer = "stop"; + previousBoard = []; + } + + // ループ確認 + const newBoardString = JSON.stringify(newBoard); + if (previousBoard.some((prevBoard) => JSON.stringify(prevBoard) === newBoardString)) { + window.parent.postMessage( + { + type: "Score loop", + data: score, + }, + "*", + ); + stop(); + timer = "stop"; + previousBoard = []; + } + + let previousLiveCells = board.flat().reduce((sum, cell) => sum + cell, 0); + let currentLiveCells = newBoard.flat().reduce((sum, cell) => sum + cell, 0); + if (timer == "stop") { + score = 0; + } else { + score += previousLiveCells > currentLiveCells ? previousLiveCells - currentLiveCells : 0; + } + + previousBoard.push(board); + if (previousBoard.length > 120) { + previousBoard.shift(); // 120個までに制限 + } + board = newBoard; + generationChange(generationFigure + 1); + rerender(); +} + +//イベント + +on.progress = () => { + progressBoard(); +}; + +on.play = () => { + timer = "start"; +}; + +on.pause = () => { + timer = "stop"; +}; + +on.board_reset = () => { + //すべて白にBoardを変更 + board = Array.from({ length: boardSize }, () => Array.from({ length: boardSize }, () => 0)); + renderBoard(); + generationChange(0); + scoreReset(); +}; + +on.board_randomize = () => { + //白黒ランダムにBoardを変更 + board = Array.from({ length: boardSize }, () => + Array.from({ length: boardSize }, () => (Math.random() > 0.5 ? 1 : 0)), + ); + renderBoard(); + generationChange(0); + scoreReset(); +}; + +on.request_sync = () => { + window.parent.postMessage( + { + type: "sync", + data: { + generationFigure: generationFigure, + boardSize: boardSize, + }, + }, + "*", + ); + console.log("generationFigure:", generationFigure, "boardSize:", boardSize); +}; + +on.place_template = (template) => { + patternShape = template; + patternHeight = patternShape.length; + patternWidth = patternShape[0].length; + isPlacingTemplate = true; + table.style.cursor = "crosshair"; + stop(); +}; + +on.save_board = async () => { + window.parent.postMessage({ type: "save_board", data: board }, "*"); +}; + +on.apply_board = (newBoard) => { + board = newBoard; + renderBoard(); + generationChange(0); + stop(); + scoreReset(); +}; diff --git a/src/lib/rules-explanation.ts b/src/lib/rules-explanation.ts index bf9dcd7..a80d8ad 100644 --- a/src/lib/rules-explanation.ts +++ b/src/lib/rules-explanation.ts @@ -1,5 +1,6 @@ import lifespan from "$lib/assets/life-game-rules/lifespan.js?raw"; import probabilistics from "$lib/assets/life-game-rules/probabilistics.js?raw"; +import score from "$lib/assets/life-game-rules/score.js?raw"; export type RuleExplanation = { name: { @@ -35,4 +36,15 @@ export const rulesExplanation = { }, code: probabilistics, }, + score: { + name: { + ja: "得点システム", + en: "Score system", + }, + description: { + ja: "生きたセルの数で得点を競えます", + en: "Compete for points based on the number of living cells", + }, + code: score, + }, }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2abb85e..9abf77b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -66,7 +66,14 @@ | "request_sync" | "progress"; - type IncomingEvent = "generation_change" | "sync" | "Size shortage" | "save_board"; + type IncomingEvent = + | "generation_change" + | "sync" + | "Size shortage" + | "save_board" + | "Score reset" + | "Score loop" + | "Score white"; function handleMessage(event: MessageEvent<{ type: IncomingEvent; data: unknown }>) { switch (event.data.type) { @@ -93,6 +100,38 @@ boardManager.openSaveModal(event.data.data as number[][], appliedCode as string); break; } + case "Score reset": { + toast.show( + isJapanese + ? "盤面が途中で変更されたため得点がリセットされました。スコア:" + event.data.data + : "The score has been reset because the board was changed midway through. Score:" + + event.data.data, + "error", + ); + break; + } + case "Score loop": { + toast.show( + isJapanese + ? "ループ発生 終了! スコア:" + event.data.data + : "Loop detected! End! Score:" + event.data.data, + "info", + ); + timer = "stopped"; + isProgress = false; + break; + } + case "Score white": { + toast.show( + isJapanese + ? "盤面上に生きているセルがなくなりました 終了! スコア:" + event.data.data + : "All cells on the board have been cleared. Game over! Score:" + event.data.data, + "info", + ); + timer = "stopped"; + isProgress = false; + break; + } default: { event.data.type satisfies never; break; From eb585eed7172b05832f6a809e774e5d21d4c32d5 Mon Sep 17 00:00:00 2001 From: TKHR-Shiu Date: Fri, 21 Nov 2025 00:19:47 +0900 Subject: [PATCH 2/4] =?UTF-8?q?showToast=E9=96=A2=E6=95=B0=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/assets/life-game-rules/score.js | 42 +++++++++++------------ src/routes/+page.svelte | 45 +++++-------------------- 2 files changed, 28 insertions(+), 59 deletions(-) diff --git a/src/lib/assets/life-game-rules/score.js b/src/lib/assets/life-game-rules/score.js index c734d15..ca5f270 100644 --- a/src/lib/assets/life-game-rules/score.js +++ b/src/lib/assets/life-game-rules/score.js @@ -191,14 +191,10 @@ progressBoard(); function scoreReset() { if (score === 0) return; previousBoard = []; - window.parent.postMessage( - { - type: "Score reset", - data: score, - }, - "*", + showToast( + "盤面が途中で変更されたため得点がリセットされました。スコア:" + score, + "The score has been reset because the board was changed midway through. Score:" + score, ); - score = 0; } @@ -254,36 +250,25 @@ function progressBoard() { if (generationFigure === 0) { return; } - window.parent.postMessage( - { - type: "Score white", - data: score, - }, - "*", + showToast( + "盤面上に生きているセルがなくなりました 終了! スコア:" + score, + "All cells on the board have been cleared. Game over! Score:" + score, ); stop(); - timer = "stop"; previousBoard = []; } // ループ確認 const newBoardString = JSON.stringify(newBoard); if (previousBoard.some((prevBoard) => JSON.stringify(prevBoard) === newBoardString)) { - window.parent.postMessage( - { - type: "Score loop", - data: score, - }, - "*", - ); + showToast("ループ発生 終了! スコア:" + score, "Loop detected! End! Score:" + score); stop(); - timer = "stop"; previousBoard = []; } let previousLiveCells = board.flat().reduce((sum, cell) => sum + cell, 0); let currentLiveCells = newBoard.flat().reduce((sum, cell) => sum + cell, 0); - if (timer == "stop") { + if (previousBoard.length === 0) { score = 0; } else { score += previousLiveCells > currentLiveCells ? previousLiveCells - currentLiveCells : 0; @@ -364,3 +349,14 @@ on.apply_board = (newBoard) => { stop(); scoreReset(); }; + +function showToast(jMessage, eMessage) { + timer = "stop"; + window.parent.postMessage( + { + type: "show_toast", + data: { japanese: jMessage, english: eMessage }, + }, + "*", + ); +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 9abf77b..4271f92 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -55,6 +55,11 @@ return () => clearInterval(timerId); }); + type showToastEvent = { + japanese: string; + english: string; + }; + type OngoingEvent = | "play" | "pause" @@ -66,14 +71,7 @@ | "request_sync" | "progress"; - type IncomingEvent = - | "generation_change" - | "sync" - | "Size shortage" - | "save_board" - | "Score reset" - | "Score loop" - | "Score white"; + type IncomingEvent = "generation_change" | "sync" | "Size shortage" | "save_board" | "show_toast"; function handleMessage(event: MessageEvent<{ type: IncomingEvent; data: unknown }>) { switch (event.data.type) { @@ -100,34 +98,9 @@ boardManager.openSaveModal(event.data.data as number[][], appliedCode as string); break; } - case "Score reset": { - toast.show( - isJapanese - ? "盤面が途中で変更されたため得点がリセットされました。スコア:" + event.data.data - : "The score has been reset because the board was changed midway through. Score:" + - event.data.data, - "error", - ); - break; - } - case "Score loop": { - toast.show( - isJapanese - ? "ループ発生 終了! スコア:" + event.data.data - : "Loop detected! End! Score:" + event.data.data, - "info", - ); - timer = "stopped"; - isProgress = false; - break; - } - case "Score white": { - toast.show( - isJapanese - ? "盤面上に生きているセルがなくなりました 終了! スコア:" + event.data.data - : "All cells on the board have been cleared. Game over! Score:" + event.data.data, - "info", - ); + case "show_toast": { + const sentence = event.data.data as showToastEvent; + toast.show(isJapanese ? sentence.japanese : sentence.english, "info"); timer = "stopped"; isProgress = false; break; From 34be369f3e04a11b1ddbbfae72cba7d2e1af03fb Mon Sep 17 00:00:00 2001 From: TKHR-Shiu Date: Fri, 21 Nov 2025 18:03:12 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=9E=E3=83=BC?= =?UTF-8?q?=E3=82=92iframe=E3=81=8B=E3=82=89=E5=A4=89=E6=9B=B4=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/assets/life-game-rules/score.js | 2 ++ src/routes/+page.svelte | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/lib/assets/life-game-rules/score.js b/src/lib/assets/life-game-rules/score.js index ca5f270..6f3df50 100644 --- a/src/lib/assets/life-game-rules/score.js +++ b/src/lib/assets/life-game-rules/score.js @@ -254,6 +254,7 @@ function progressBoard() { "盤面上に生きているセルがなくなりました 終了! スコア:" + score, "All cells on the board have been cleared. Game over! Score:" + score, ); + window.parent.postMessage({ type: "timer_change", data: "stopped" }, "*"); stop(); previousBoard = []; } @@ -262,6 +263,7 @@ function progressBoard() { const newBoardString = JSON.stringify(newBoard); if (previousBoard.some((prevBoard) => JSON.stringify(prevBoard) === newBoardString)) { showToast("ループ発生 終了! スコア:" + score, "Loop detected! End! Score:" + score); + window.parent.postMessage({ type: "timer_change", data: "stopped" }, "*"); stop(); previousBoard = []; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 4271f92..a37ba66 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -71,7 +71,13 @@ | "request_sync" | "progress"; - type IncomingEvent = "generation_change" | "sync" | "Size shortage" | "save_board" | "show_toast"; + type IncomingEvent = + | "generation_change" + | "sync" + | "Size shortage" + | "save_board" + | "show_toast" + | "timer_change"; function handleMessage(event: MessageEvent<{ type: IncomingEvent; data: unknown }>) { switch (event.data.type) { @@ -101,8 +107,11 @@ case "show_toast": { const sentence = event.data.data as showToastEvent; toast.show(isJapanese ? sentence.japanese : sentence.english, "info"); - timer = "stopped"; - isProgress = false; + break; + } + case "timer_change": { + timer = event.data.data as "stopped" | "running"; + isProgress = timer === "running"; break; } default: { @@ -516,6 +525,7 @@ class="btn btn-ghost hover:bg-[rgb(220,220,220)] text-black" onclick={() => { isProgress = false; + timer = "stopped"; sendEvent("pause"); ruleDrawerOpen = !ruleDrawerOpen; bottomDrawerOpen = false; From b833410638758fa7cccbfd050e23b22978f53dd6 Mon Sep 17 00:00:00 2001 From: TKHR-Shiu Date: Fri, 21 Nov 2025 18:20:47 +0900 Subject: [PATCH 4/4] =?UTF-8?q?isProgress=E3=82=92timer=E3=81=AB=E7=B5=B1?= =?UTF-8?q?=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/+page.svelte | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index a37ba66..15c4038 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -23,7 +23,6 @@ let showEditor = $state(true); let preview_iframe: HTMLIFrameElement | undefined = $state(); - let isProgress = $state(false); let isJapanese = $state(true); let resetModalOpen = $state(false); let bottomDrawerOpen = $state(false); @@ -111,7 +110,6 @@ } case "timer_change": { timer = event.data.data as "stopped" | "running"; - isProgress = timer === "running"; break; } default: { @@ -416,17 +414,16 @@ @@ -438,7 +435,6 @@