製作の経緯
年初めは運勢占いがたくさんあるよね。
自分がそれを制作したら自分有利の運勢になるやん!!...
ということで、選ばれたスタートを自分の選んだゴールに繋げる盤面を自動で作る仕組みを実装してみました〜
成果物
どの選択肢を選んでも「みたち」に行き着きます。リロードして何回か試してみてください。
スマホ用に作ったのでパソコンだと不恰好かも...
プログラミング
ソースコード
アルゴリズム
目的
ランダムに与えられたスタートとゴールをうまくカモフラージュしながら結ぶ
流れ
- スタートとゴールを最短で行く場合の道を得る。
- 1の途中に寄り道を追加する。例えば2本目から3本目に行く道の前に、2本目から1本目に行く道と1本目から2本目に行く道を追加する。
- 自分が通らないダミーの道を追加する
全体
おおみそかに慌てて作ったので見やすさ絶望的です。
main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.canvas-parent {
width: 100%;
height: 200vh;
}
</style>
</head>
<body>
<div>
<h2 style="text-align: center">今年大事にするべきものあみだ</h2>
<div style="text-align: center">
スタート地点を選択したら<br />「あみだくじ!」をクリック!!
</div>
</div>
<div class="canvas-parent" id="canvasParent">
<canvas id="canvas"></canvas>
</div>
<script>
const FRAMERATE = 60;
let canvasParent = document.getElementById("canvasParent");
let canvas = document.getElementById("canvas");
let context = canvas.getContext("2d");
let x = canvas.width;
let y = canvas.height;
canvas.addEventListener("click", (e) => {
const rect = canvas.getBoundingClientRect();
const point = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
if (!clicked) {
if (
//「あみだくじ!」をクリックしたら
point.y < 155 &&
point.y > 105 &&
point.x > x / 2 - button1.width / 2 &&
point.x < x / 2 + button1.width / 2
) {
//ゴールに表示する5文字を初期化
wordList = shuffle(wordList);
wordList.splice(Math.floor(Math.random() * 5), 0, "みたち");
let index = wordList.indexOf("みたち");
let numList = [];
//自分の動くべき道追加
for (
let i = Math.min(index, checked);
i < Math.max(index, checked) + 1;
i++
) {
numList.push(i);
}
if (index < checked) {
numList.reverse();
}
if (numList.length == 1) {
}
lineList.push([checked]); //-1で取り出せるように2重
//lineListには[[前の位置,後の位置],現在の位置]が入る
//lineList2 に自分を動かさないダミーは別で保持したので、現在の位置いらなかったかも...
for (let i = 0; i < numList.length - 1; i++) {
lineList.push([[numList[i], numList[i + 1]], numList[i + 1]]);
}
//自分動かすダミー道追加
let last = [checked];
while (lineList.length < 8) {//8はテキトー 、もっと適切な数字があるかも
let rand = Math.floor(Math.random() * lineList.length);
let before = lineList[rand][lineList[rand].length - 1];
let middleWay = Math.random() > 0.5 ? 1 : -1;
let middle = before + middleWay;
//できるだけ同じ場所を往復することを避ける
if (last.includes(middle)) {
middle = middle - 2 * middleWay;
}
if (middle == -1) {
middle = 1;
} else if (middle == 5) {
middle = 3;
}
last = [before, middle];
lineList.splice(rand + 1, 0, [[before, middle], middle]);
lineList.splice(rand + 2, 0, [[middle, before], before]);
if (middle != 0 && middle != 4) {
//できるだけ横方向の移動をして欲しいから追加した
//ダミー道の途中に再度ダミー道追加
let middle2 = middle + (middle - before);
lineList.splice(rand + 2, 0, [[middle, middle2], middle2]);
lineList.splice(rand + 3, 0, [[middle2, middle], middle]);
}
}
clicked = true;
//自分動かさないダミー道追加
lineList2.push([checked]); //-1で取り出せるように
for (line of lineList) {
if (line.length != 1) {
let minNum = Math.min(...line[0]);
let before, middle;
if (minNum == 0) {
//なぜ0.7にしたか忘れた、普通に0.5でいいかも
if (Math.random() > 0.7) {
before = 2;
middle = 3;
} else {
before = 3;
middle = 4;
}
}
if (minNum == 1) {
before = 3;
middle = 4;
}
if (minNum == 2) {
before = 0;
middle = 1;
}
if (minNum == 3) {
if (Math.random() > 0.7) {
before = 0;
middle = 1;
} else {
before = 1;
middle = 2;
}
}
lineList2.push([before, middle]);
}
}
}
for (let i = 0; i < 5; i++) {
if (
//スタート地点がクリックされたら
point.y < 230 &&
point.y > 170 &&
point.x < (x / 6) * (1 + i) + 20 &&
point.x > (x / 6) * (1 + i) - 20
) {
checked = i;
}
}
}
});
let resize = () => {
//画面サイズをレスポンシブ対応
canvas.width = canvasParent.clientWidth;
canvas.height = canvasParent.clientHeight;
x = canvas.width;
y = canvas.height;
};
let clicked = false;
let checked = 0;
let wordList = [
"お金",
"家族",
"就活",
"サークル",
"食事",
"努力",
"友達",
"恋人",
];
let lineList = [];
let lineList2 = [];
const shuffle = ([...array]) => {
for (let i = array.length - 1; i >= 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
let draw = () => {
if (clicked) {
//最後の挨拶描画
context.beginPath();
context.font = "15pt Arial";
context.textAlign = "center";
context.fillStyle = "rgba(255, 0, 0, 1)";
context.fillText("あけましておめでとうございます", x / 2, 700);
context.stroke();
context.beginPath();
context.font = "15pt Arial";
context.textAlign = "center";
context.fillStyle = "rgba(255, 0, 0, 1)";
context.fillText("今年もよろしくお願いします", x / 2, 720);
context.stroke();
for (let i = 0; i < 5; i++) {
//縦棒描画
context.beginPath();
context.lineWidth = 4;
context.moveTo((x / 6) * (1 + i), 210);
context.lineTo((x / 6) * (1 + i), 600);
context.stroke();
}
for (let i = 0; i < 5; i++) {
//ゴール文字描画
context.beginPath();
context.font = "10pt Arial";
context.textAlign = "center";
context.fillStyle = "rgba(0, 0, 0, 1)";
context.fillText(wordList[i], (x / 6) * (1 + i), 630);
context.stroke();
}
const l = lineList.length;
let i = 0;
for (line of lineList) {
if (line.length != 1) {
//自分の通る横棒の描画
context.beginPath();
context.lineWidth = 4;
context.moveTo((x / 6) * (1 + line[0][0]), 250 + (i * 300) / l);
context.lineTo((x / 6) * (1 + line[0][1]), 250 + (i * 300) / l);
context.stroke();
i++;
}
}
i = -1;
for (line of lineList2) {
if (lineList2.length != 1) {
//自分の通らない横棒の描画
context.beginPath();
context.lineWidth = 4;
context.moveTo((x / 6) * (1 + line[0]), 250 + (i * 300) / l);
context.lineTo((x / 6) * (1 + line[1]), 250 + (i * 300) / l);
context.stroke();
i++;
}
}
}
for (let i = 0; i < 5; i++) {
//スタート地点描画
context.beginPath();
context.arc((x / 6) * (1 + i), 200, 10, 0, Math.PI * 2, true);
context.strokeStyle = "black";
context.lineWidth = 2;
context.stroke();
}
//選ばれたスタート地点の色変更
context.beginPath();
context.arc((x / 6) * (1 + checked), 200, 10, 0, Math.PI * 2, true);
context.fillStyle = "rgba(255,0,0,0.8)";
context.fill();
context.stroke();
};
let button1;
let drawButton = () => {
//あみだくじ!ボタン作成 、 ボタンの横幅をテキストと合わせるための処理
context.beginPath();
context.font = "40pt Arial";
context.textAlign = "center";
var button = context.measureText("あみだくじ!");
button1 = button;
context.fillStyle = "rgba(255, 0, 0, 0.25)";
context.fillRect(x / 2 - button.width / 2, 155, button.width, -50);
context.fillStyle = "rgba(255, 0, 0, 1)";
context.fillText("あみだくじ!", x / 2, 150);
context.stroke();
};
let world = () => {
resize();
draw();
drawButton();
};
setInterval(() => world(), 1000 / FRAMERATE);
</script>
</body>
</html>
感想
canvasを初めて使用したが、pygameと似てる部分があってすんなり実装できた。アルゴリズム作るのはやっぱり楽しい!!!
アルゴリズムは割とうまくいったと思うけど、自分が通る道が絶対2個セットになっちゃうのは美しくないかなぁ、改善の余地はまだまだありそうだ!