From e4e91ab14ef312a235df9361c750442ccf5c9f42 Mon Sep 17 00:00:00 2001 From: kg0816 <211191696+kg0816@users.noreply.github.com> Date: Sun, 16 Nov 2025 17:47:49 +0900 Subject: [PATCH 1/5] feat: add additional rules drawer for code templates --- src/lib/assets/life-game-rules/lifespan.js | 330 ++++++++++++++++++ .../assets/life-game-rules/probabilistics.js | 293 ++++++++++++++++ src/routes/+page.svelte | 77 +++- 3 files changed, 699 insertions(+), 1 deletion(-) create mode 100644 src/lib/assets/life-game-rules/lifespan.js create mode 100644 src/lib/assets/life-game-rules/probabilistics.js diff --git a/src/lib/assets/life-game-rules/lifespan.js b/src/lib/assets/life-game-rules/lifespan.js new file mode 100644 index 0000000..1adbad8 --- /dev/null +++ b/src/lib/assets/life-game-rules/lifespan.js @@ -0,0 +1,330 @@ +"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 boardSize = 20; //盤面の大きさ(20x20) +const cellSize = 600 / boardSize; //セルの大きさ(px) +const maxLifespan = 2; // セルの最大寿命 + +// around: 周囲の生きたセル数, currentLifespan: 現在の寿命 +function getNextLifespan(around, currentLifespan) { + // セルが生きている場合 + if (currentLifespan > 0) { + // 周囲が 2 か 3 なら寿命を維持(減らさない) + if (2 <= around && around <= 3) { + return currentLifespan; + } + // 条件を満たさない場合は寿命を減らす + return currentLifespan - 1; + } + + // セルが死んでいる場合 + // 周囲が 3 なら誕生 + if (around === 3) { + return maxLifespan; + } + + return 0; +} + +// cellの状態に応じた色を返す関数 +function getStyle(cell, lifespan) { + if (cell === 0) return "white"; + + // cellの値に応じて色を返す場合はここに追加 + if (lifespan === 2) return "black"; + if (lifespan === 1) return "gray"; + if (lifespan === 0) return "white"; + + 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.lifespan = board[i][j] ? maxLifespan : 0; // セルの寿命を設定 + button.style.backgroundColor = getStyle(board[i][j], button.lifespan); //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 (patternShape[r][c]) { + const btn = table.children[boardRow].children[boardCol].children[0]; + btn.lifespan = maxLifespan; + } + } + } + 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.lifespan = board[i][j] ? maxLifespan : 0; + button.style.backgroundColor = getStyle(board[i][j], button.lifespan); + } + }; + button.onmouseenter = () => { + if (isDragging && timer === "stop" && board[i][j] !== dragMode && !isPlacingTemplate) { + board[i][j] = dragMode; + button.lifespan = board[i][j] ? maxLifespan : 0; + button.style.backgroundColor = getStyle(board[i][j], button.lifespan); + } + 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; + const lifespan = cell.lifespan || 0; + cell.style.backgroundColor = getStyle(board[cellPos.row][cellPos.col], lifespan); + }); + 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], button.lifespan); + 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 generationChange(num) { + //現在の世代を表すgenerationFigureを変更し、文章も変更 + generationFigure = num; + window.parent.postMessage( + { + type: "generation_change", + data: generationFigure, + }, + "*", + ); +} + +function progressBoard() { + const newLifespans = Array(boardSize) + .fill(0) + .map(() => Array(boardSize).fill(0)); + + 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) { + const neighborButton = table.children[i + tate[ii]].children[j + yoko[jj]].children[0]; + around += neighborButton.lifespan > 0 ? 1 : 0; + } + } + } + //↑周囲のマスに黒マスが何個あるかを計算(aroundに格納) + + // 前の世代の寿命をボタンから取得し、新しい寿命を計算して保存 + const button = table.children[i].children[j].children[0]; + const currentLifespan = button.lifespan; + newLifespans[i][j] = getNextLifespan(around, currentLifespan); + } + } + + // 新しい寿命を適用 + for (let i = 0; i < boardSize; i++) { + for (let j = 0; j < boardSize; j++) { + const button = table.children[i].children[j].children[0]; + button.lifespan = newLifespans[i][j]; + board[i][j] = button.lifespan > 0 ? 1 : 0; + } + } + + 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); +}; + +on.board_randomize = () => { + //白黒ランダムにBoardを変更 + board = Array.from({ length: boardSize }, () => + Array.from({ length: boardSize }, () => (Math.random() > 0.5 ? 1 : 0)), + ); + renderBoard(); + generationChange(0); +}; + +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(); +}; diff --git a/src/lib/assets/life-game-rules/probabilistics.js b/src/lib/assets/life-game-rules/probabilistics.js new file mode 100644 index 0000000..933dabb --- /dev/null +++ b/src/lib/assets/life-game-rules/probabilistics.js @@ -0,0 +1,293 @@ +"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 boardSize = 20; //盤面の大きさ(20x20) +const cellSize = 600 / boardSize; //セルの大きさ(px) + +// around: 周囲の生きたセル数 self: 自身が生きているかどうか +function isNextAlive(around, self) { + // 自身が生きている & 周囲が 2 か 3 で生存 50%の確率でこのルールに従う + if (self && 2 <= around && around <= 3) { + return Math.random() > 0.5 ? self : !self; + } + // 自身が生きている & 周囲が2,3以外で死亡 50%の確率でこのルールに従う + if (self && (around < 2 || around > 3)) { + return Math.random() > 0.5 ? !self : self; + } + // 自身が死んでいる & 周囲が 3 で誕生 50%の確率でこのルールに従う + if (!self && around === 3) { + return Math.random() > 0.5 ? 1 : 0; + } + // 自身が死んでいる & 周囲が3以外で死亡のまま + 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]; + } + } + 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"; + } + }; + 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 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]); + } + } + 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); +}; + +on.board_randomize = () => { + //白黒ランダムにBoardを変更 + board = Array.from({ length: boardSize }, () => + Array.from({ length: boardSize }, () => (Math.random() > 0.5 ? 1 : 0)), + ); + renderBoard(); + generationChange(0); +}; + +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(); +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 14fdc08..8d11978 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,6 +3,8 @@ import event from "@/iframe/event.js?raw"; import lifeGameHTML from "@/iframe/life-game.html?raw"; import lifeGameJS from "@/iframe/life-game.js?raw"; + import lifespan from "$lib/assets/life-game-rules/lifespan.js?raw"; + import probabilistics from "$lib/assets/life-game-rules/probabilistics.js?raw"; import patterns from "$lib/board-templates"; import * as icons from "$lib/icons/index.ts"; import { BoardManager } from "$lib/models/BoardManager.svelte"; @@ -26,6 +28,7 @@ let isJapanese = $state(true); let resetModalOpen = $state(false); let bottomDrawerOpen = $state(false); + let ruleDrawerOpen = $state(false); let generationFigure = $state(0); let sizeValue = $state(20); @@ -138,6 +141,32 @@ minHeight: "100%", }, }); + + const rulesExplanation = { + lifespan: { + code: lifespan, + ja: "それぞれのいのちに寿命を設定できます", + en: "Set lifespan for each cell", + }, + probabilistics: { + code: probabilistics, + ja: "生死に確率を導入できます", + en: "Introduce probability to life and death", + }, + }; + + type RuleExplanation = { + code: string; + ja: string; + en: string; + }; + + function selectRule(ruleName: keyof typeof rulesExplanation) { + console.log(`Selected rule: ${ruleName}`); + const rule = rulesExplanation[ruleName] as RuleExplanation; + editingCode = rule.code; + appliedCode = rule.code; + } +
+
+
+ {#each Object.entries(rulesExplanation) as [ruleName, description]} + + {/each} +
+
+
@@ -292,7 +347,10 @@
+
From f72f6574d815459f8fb89e88712185df2f301aa4 Mon Sep 17 00:00:00 2001 From: kg0816 <211191696+kg0816@users.noreply.github.com> Date: Sun, 16 Nov 2025 17:51:44 +0900 Subject: [PATCH 2/5] fix lint --- src/routes/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 8d11978..83508e4 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -254,7 +254,7 @@ >
- {#each Object.entries(rulesExplanation) as [ruleName, description]} + {#each Object.entries(rulesExplanation) as [ruleName, description] (ruleName)}
From 115218d390d877f92ad2d8991bb2f2178c42fe53 Mon Sep 17 00:00:00 2001 From: kg0816 <211191696+kg0816@users.noreply.github.com> Date: Mon, 17 Nov 2025 10:37:00 +0900 Subject: [PATCH 4/5] refactor: add localized names to rule selection UI --- src/lib/rules-explanation.ts | 20 ++++++++++++++++---- src/routes/+page.svelte | 11 +++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/lib/rules-explanation.ts b/src/lib/rules-explanation.ts index 4e4d533..e1aa06e 100644 --- a/src/lib/rules-explanation.ts +++ b/src/lib/rules-explanation.ts @@ -9,13 +9,25 @@ export type RuleExplanation = { export const rulesExplanation = { lifespan: { + name: { + ja: "寿命システム", + en: "Lifespan system", + }, + description: { + ja: "それぞれのいのちに寿命を設定できます", + en: "Set lifespan for each cell", + }, code: lifespan, - ja: "それぞれのいのちに寿命を設定できます", - en: "Set lifespan for each cell", }, probabilistics: { + name: { + ja: "確率システム", + en: "Probabilistic system", + }, + description: { + ja: "生死に確率を導入できます", + en: "Introduce probability to life and death", + }, code: probabilistics, - ja: "生死に確率を導入できます", - en: "Introduce probability to life and death", }, }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 575eb4c..63742da 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -15,6 +15,7 @@ import { javascript } from "@codemirror/lang-javascript"; import { oneDark } from "@codemirror/theme-one-dark"; import { EditorView } from "@codemirror/view"; + import { is } from "valibot"; let editingCode = $state(lifeGameJS); let appliedCode = $state(lifeGameJS); @@ -232,18 +233,20 @@ >
- {#each Object.entries(rulesExplanation) as [ruleName, description] (ruleName)} + {#each Object.entries(rulesExplanation) as [ruleName, ruleData] (ruleName)} From 2bafdae5cbdbd642858312353c75d7ade90b8cba Mon Sep 17 00:00:00 2001 From: kg0816 <211191696+kg0816@users.noreply.github.com> Date: Mon, 17 Nov 2025 10:43:20 +0900 Subject: [PATCH 5/5] fix lint --- src/routes/+page.svelte | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 63742da..e902202 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -15,7 +15,6 @@ import { javascript } from "@codemirror/lang-javascript"; import { oneDark } from "@codemirror/theme-one-dark"; import { EditorView } from "@codemirror/view"; - import { is } from "valibot"; let editingCode = $state(lifeGameJS); let appliedCode = $state(lifeGameJS);