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..6f3df50
--- /dev/null
+++ b/src/lib/assets/life-game-rules/score.js
@@ -0,0 +1,364 @@
+"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 = [];
+ showToast(
+ "盤面が途中で変更されたため得点がリセットされました。スコア:" + score,
+ "The score has been reset because the board was changed midway through. Score:" + 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;
+ }
+ showToast(
+ "盤面上に生きているセルがなくなりました 終了! スコア:" + score,
+ "All cells on the board have been cleared. Game over! Score:" + score,
+ );
+ window.parent.postMessage({ type: "timer_change", data: "stopped" }, "*");
+ stop();
+ previousBoard = [];
+ }
+
+ // ループ確認
+ 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 = [];
+ }
+
+ let previousLiveCells = board.flat().reduce((sum, cell) => sum + cell, 0);
+ let currentLiveCells = newBoard.flat().reduce((sum, cell) => sum + cell, 0);
+ if (previousBoard.length === 0) {
+ 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();
+};
+
+function showToast(jMessage, eMessage) {
+ timer = "stop";
+ window.parent.postMessage(
+ {
+ type: "show_toast",
+ data: { japanese: jMessage, english: eMessage },
+ },
+ "*",
+ );
+}
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..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);
@@ -55,6 +54,11 @@
return () => clearInterval(timerId);
});
+ type showToastEvent = {
+ japanese: string;
+ english: string;
+ };
+
type OngoingEvent =
| "play"
| "pause"
@@ -66,7 +70,13 @@
| "request_sync"
| "progress";
- type IncomingEvent = "generation_change" | "sync" | "Size shortage" | "save_board";
+ 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) {
@@ -93,6 +103,15 @@
boardManager.openSaveModal(event.data.data as number[][], appliedCode as string);
break;
}
+ case "show_toast": {
+ const sentence = event.data.data as showToastEvent;
+ toast.show(isJapanese ? sentence.japanese : sentence.english, "info");
+ break;
+ }
+ case "timer_change": {
+ timer = event.data.data as "stopped" | "running";
+ break;
+ }
default: {
event.data.type satisfies never;
break;
@@ -395,17 +414,16 @@
@@ -417,7 +435,6 @@