Skip to content

Commit 601507e

Browse files
authored
Merge pull request #44 from ut-code/score-template
スコアテンプレートの実装
2 parents 767e792 + 99914dd commit 601507e

File tree

3 files changed

+366
-1
lines changed

3 files changed

+366
-1
lines changed
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
"use strict";
2+
3+
//ルール
4+
//スコア計算は生きているセルの総数が増加した時、増加分だけ加算されます
5+
//減少した時は、スコアは変化しません
6+
//スコアが計算され始めてからセルを直接変化させるとスコアがリセットされます
7+
//生きているセルがなくなるか、ループが起きたら計算終了です
8+
9+
let generationFigure = 0;
10+
let isDragging = false;
11+
let dragMode = 0; // 1: 黒にする, 0: 白にする
12+
let isPlacingTemplate = false;
13+
let patternShape = [];
14+
let patternHeight = 0;
15+
let patternWidth = 0;
16+
let previewCells = [];
17+
let previousBoard = [];
18+
let score = 0;
19+
20+
//変数設定
21+
let boardSize = 20; //盤面の大きさ(20x20)
22+
const cellSize = 450 / boardSize; //セルの大きさ(px)
23+
24+
// around: 周囲の生きたセル数 self: 自身が生きているかどうか
25+
function isNextAlive(around, self) {
26+
// 自身が生きている & 周囲が 2 か 3 で生存
27+
if (self && 2 <= around && around <= 3) {
28+
return self;
29+
}
30+
// 自身が死んでいる & 周囲が 3 で誕生
31+
if (!self && around === 3) {
32+
return 1;
33+
}
34+
return 0;
35+
}
36+
37+
// cellの状態に応じた色を返す関数
38+
function getStyle(cell) {
39+
if (cell === 0) return "white";
40+
// cellの値に応じて色を返す場合はここに追加
41+
return "black"; // デフォルトは黒
42+
}
43+
44+
//Boardの初期化
45+
let board = Array.from({ length: boardSize }, () => Array.from({ length: boardSize }, () => 0));
46+
const table = document.getElementById("game-board");
47+
48+
//盤面をBoardに従って変更する関数達(Boardを変更したら実行する)
49+
function renderBoard() {
50+
// bodyを中央配置に設定
51+
document.body.style.display = "flex";
52+
document.body.style.justifyContent = "center";
53+
document.body.style.alignItems = "center";
54+
document.body.style.minHeight = "100vh";
55+
document.body.style.margin = "0";
56+
document.body.style.padding = "0";
57+
58+
// 初回の盤面生成
59+
table.innerHTML = "";
60+
for (let i = 0; i < boardSize; i++) {
61+
const tr = document.createElement("tr");
62+
tr.style.padding = "0";
63+
for (let j = 0; j < boardSize; j++) {
64+
const td = document.createElement("td");
65+
td.style.padding = "0";
66+
const button = document.createElement("button");
67+
button.style.backgroundColor = board[i][j] ? "black" : "white"; //Boardの対応する値によって色を変更
68+
// ボードが大きいときは border をつけない
69+
if (boardSize >= 50) {
70+
button.style.border = "none";
71+
table.style.border = "1px solid black";
72+
} else {
73+
button.style.border = "0.5px solid black";
74+
}
75+
button.style.width = `${cellSize}px`;
76+
button.style.height = `${cellSize}px`;
77+
button.style.padding = "0"; //cellSizeが小さいとき、セルが横長になることを防ぐ
78+
button.style.display = "block"; //cellSizeが小さいとき、行間が空きすぎるのを防ぐ
79+
button.onclick = () => {
80+
if (isPlacingTemplate) {
81+
clearPreview();
82+
isPlacingTemplate = false;
83+
if (i + patternHeight < boardSize + 1 && j + patternWidth < boardSize + 1) {
84+
for (let r = 0; r < patternHeight; r++) {
85+
for (let c = 0; c < patternWidth; c++) {
86+
const boardRow = i + r;
87+
const boardCol = j + c;
88+
board[boardRow][boardCol] = patternShape[r][c];
89+
}
90+
}
91+
if (generationFigure !== 0) {
92+
scoreReset();
93+
}
94+
rerender();
95+
generationChange(0);
96+
} else {
97+
window.parent.postMessage(
98+
{
99+
type: "Size shortage",
100+
data: {},
101+
},
102+
"*",
103+
);
104+
}
105+
}
106+
};
107+
button.onmousedown = (e) => {
108+
e.preventDefault();
109+
if (!isPlacingTemplate) {
110+
isDragging = true;
111+
board[i][j] = board[i][j] ? 0 : 1;
112+
dragMode = board[i][j];
113+
button.style.backgroundColor = board[i][j] ? "black" : "white";
114+
if (generationFigure > 1) {
115+
scoreReset();
116+
}
117+
}
118+
};
119+
button.onmouseenter = () => {
120+
if (isDragging && board[i][j] !== dragMode && !isPlacingTemplate) {
121+
board[i][j] = dragMode;
122+
button.style.backgroundColor = board[i][j] ? "black" : "white";
123+
}
124+
if (isPlacingTemplate) {
125+
drawPreview(i, j);
126+
}
127+
};
128+
td.appendChild(button);
129+
tr.appendChild(td);
130+
}
131+
table.appendChild(tr);
132+
}
133+
}
134+
135+
table.onmouseleave = () => {
136+
if (isPlacingTemplate) {
137+
clearPreview();
138+
}
139+
};
140+
141+
function drawPreview(row, col) {
142+
clearPreview();
143+
for (let r = 0; r < patternHeight; r++) {
144+
for (let c = 0; c < patternWidth; c++) {
145+
if (patternShape[r][c] === 1) {
146+
const boardRow = row + r;
147+
const boardCol = col + c;
148+
if (boardRow < boardSize && boardCol < boardSize) {
149+
const cell = table.rows[boardRow].cells[boardCol].firstChild;
150+
cell.style.backgroundColor = "gray";
151+
previewCells.push({ row: boardRow, col: boardCol });
152+
}
153+
}
154+
}
155+
}
156+
}
157+
158+
function clearPreview() {
159+
previewCells.forEach((cellPos) => {
160+
const cell = table.rows[cellPos.row].cells[cellPos.col].firstChild;
161+
cell.style.backgroundColor = board[cellPos.row][cellPos.col] ? "black" : "white";
162+
});
163+
previewCells = [];
164+
}
165+
166+
function rerender() {
167+
// 2回目以降の盤面生成
168+
for (let i = 0; i < boardSize; i++) {
169+
for (let j = 0; j < boardSize; j++) {
170+
const button = table.children[i].children[j].children[0];
171+
172+
// 色の更新
173+
const currentCellColor = button.style.backgroundColor;
174+
const expectedCellColor = getStyle(board[i][j]);
175+
if (currentCellColor !== expectedCellColor) {
176+
button.style.backgroundColor = expectedCellColor;
177+
}
178+
}
179+
}
180+
}
181+
182+
document.addEventListener("mouseup", () => {
183+
isDragging = false;
184+
});
185+
186+
renderBoard();
187+
188+
function scoreReset() {
189+
if (score === 0) return;
190+
previousBoard = [];
191+
showToast(
192+
"盤面が途中で変更されたため得点がリセットされました。スコア:" + score,
193+
"The score has been reset because the board was changed midway through. Score:" + score,
194+
);
195+
score = 0;
196+
}
197+
198+
function generationChange(num) {
199+
//現在の世代を表すgenerationFigureを変更し、文章も変更
200+
generationFigure = num;
201+
window.parent.postMessage(
202+
{
203+
type: "generation_change",
204+
data: generationFigure,
205+
},
206+
"*",
207+
);
208+
}
209+
210+
function progressBoard() {
211+
const newBoard = structuredClone(board);
212+
for (let i = 0; i < boardSize; i++) {
213+
for (let j = 0; j < boardSize; j++) {
214+
//周囲のマスに黒マスが何個あるかを計算(aroundに格納)↓
215+
let around = 0;
216+
let tate, yoko;
217+
if (i === 0) {
218+
tate = [0, 1];
219+
} else if (i === boardSize - 1) {
220+
tate = [0, -1];
221+
} else {
222+
tate = [-1, 0, 1];
223+
}
224+
if (j === 0) {
225+
yoko = [0, 1];
226+
} else if (j === boardSize - 1) {
227+
yoko = [0, -1];
228+
} else {
229+
yoko = [-1, 0, 1];
230+
}
231+
for (let ii = 0; ii < tate.length; ii++) {
232+
for (let jj = 0; jj < yoko.length; jj++) {
233+
if (tate[ii] !== 0 || yoko[jj] !== 0) {
234+
around += board[i + tate[ii]][j + yoko[jj]] !== 0 ? 1 : 0;
235+
}
236+
}
237+
}
238+
//↑周囲のマスに黒マスが何個あるかを計算(aroundに格納)
239+
newBoard[i][j] = isNextAlive(around, board[i][j]);
240+
}
241+
}
242+
243+
// すべて白マスでないか確認
244+
const isAllWhite = newBoard.every((row) => row.every((cell) => cell === 0));
245+
if (isAllWhite) {
246+
// generationFigure が 0 の場合は、初期状態なのでメッセージを送らない
247+
if (generationFigure === 0) {
248+
return;
249+
}
250+
showToast(
251+
"盤面上に生きているセルがなくなりました 終了! スコア:" + score,
252+
"All cells on the board have been cleared. Game over! Score:" + score,
253+
);
254+
window.parent.postMessage({ type: "timer_change", data: false }, "*");
255+
previousBoard = [];
256+
}
257+
258+
// ループ確認
259+
const newBoardString = JSON.stringify(newBoard);
260+
if (previousBoard.some((prevBoard) => JSON.stringify(prevBoard) === newBoardString)) {
261+
showToast("ループ発生 終了! スコア:" + score, "Loop detected! End! Score:" + score);
262+
window.parent.postMessage({ type: "timer_change", data: false }, "*");
263+
previousBoard = [];
264+
}
265+
266+
let previousLiveCells = board.flat().reduce((sum, cell) => sum + cell, 0);
267+
let currentLiveCells = newBoard.flat().reduce((sum, cell) => sum + cell, 0);
268+
if (previousBoard.length === 0) {
269+
score = 0;
270+
} else {
271+
score += previousLiveCells > currentLiveCells ? previousLiveCells - currentLiveCells : 0;
272+
}
273+
274+
previousBoard.push(board);
275+
if (previousBoard.length > 120) {
276+
previousBoard.shift(); // 120個までに制限
277+
}
278+
board = newBoard;
279+
generationChange(generationFigure + 1);
280+
rerender();
281+
}
282+
283+
//イベント
284+
285+
on.progress = () => {
286+
progressBoard();
287+
};
288+
289+
on.board_reset = () => {
290+
//すべて白にBoardを変更
291+
board = Array.from({ length: boardSize }, () => Array.from({ length: boardSize }, () => 0));
292+
renderBoard();
293+
generationChange(0);
294+
scoreReset();
295+
};
296+
297+
on.board_randomize = () => {
298+
//白黒ランダムにBoardを変更
299+
board = Array.from({ length: boardSize }, () =>
300+
Array.from({ length: boardSize }, () => (Math.random() > 0.5 ? 1 : 0)),
301+
);
302+
renderBoard();
303+
generationChange(0);
304+
scoreReset();
305+
};
306+
307+
on.get_boardsize = () => {
308+
window.parent.postMessage({ type: "get_boardsize", data: boardSize }, "*");
309+
};
310+
311+
on.place_template = (template) => {
312+
patternShape = template;
313+
patternHeight = patternShape.length;
314+
patternWidth = patternShape[0].length;
315+
isPlacingTemplate = true;
316+
table.style.cursor = "crosshair";
317+
};
318+
319+
on.save_board = async () => {
320+
window.parent.postMessage({ type: "save_board", data: board }, "*");
321+
};
322+
323+
on.apply_board = (newBoard) => {
324+
board = newBoard;
325+
renderBoard();
326+
generationChange(0);
327+
scoreReset();
328+
};
329+
330+
function showToast(jMessage, eMessage) {
331+
window.parent.postMessage(
332+
{
333+
type: "show_toast",
334+
data: { japanese: jMessage, english: eMessage },
335+
},
336+
"*",
337+
);
338+
}

src/lib/rules-explanation.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import lifespan from "$lib/assets/life-game-rules/lifespan.js?raw";
22
import probabilistics from "$lib/assets/life-game-rules/probabilistics.js?raw";
33
import wolframcode from "$lib/assets/life-game-rules/wolframcode.js?raw";
4+
import score from "$lib/assets/life-game-rules/score.js?raw";
45

56
export type RuleExplanation = {
67
name: {
@@ -47,4 +48,15 @@ export const rulesExplanation = {
4748
},
4849
code: wolframcode,
4950
},
51+
score: {
52+
name: {
53+
ja: "得点システム",
54+
en: "Score system",
55+
},
56+
description: {
57+
ja: "生きたセルの増加数で得点を競えます",
58+
en: "Compete for points based on the number of live cells gained",
59+
},
60+
code: score,
61+
},
5062
};

src/routes/+page.svelte

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,13 @@
6565
| "get_boardsize"
6666
| "progress";
6767
68-
type IncomingEvent = "generation_change" | "get_boardsize" | "Size shortage" | "save_board";
68+
type IncomingEvent =
69+
| "generation_change"
70+
| "get_boardsize"
71+
| "Size shortage"
72+
| "save_board"
73+
| "show_toast"
74+
| "timer_change";
6975
7076
function handleMessage(event: MessageEvent<{ type: IncomingEvent; data: unknown }>) {
7177
switch (event.data.type) {
@@ -91,6 +97,15 @@
9197
boardManager.openSaveModal(event.data.data as number[][], appliedCode as string);
9298
break;
9399
}
100+
case "show_toast": {
101+
const sentence = event.data.data as { japanese: string; english: string };
102+
toast.show(isJapanese ? sentence.japanese : sentence.english, "info");
103+
break;
104+
}
105+
case "timer_change": {
106+
timerIsRunnning = event.data.data as boolean;
107+
break;
108+
}
94109
default: {
95110
event.data.type satisfies never;
96111
break;

0 commit comments

Comments
 (0)