Last active
December 23, 2023 16:20
-
-
Save Satomaru/787e2851455a71d04c43521ecf1ad382 to your computer and use it in GitHub Desktop.
パズルゲームです。宝石の場所を当ててね。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja" xmlns="http://www.w3.org/2000/svg"> | |
<head> | |
<meta charset="UTF-8" /> | |
<title>BlackBox v0.0</title> | |
<script src="https://cdn.jsdelivr.net/gh/exis9/squery@latest/squery.min.js"></script> | |
<style> | |
body { | |
overflow: hidden; | |
font-size: 14px; | |
} | |
article { | |
width: 560px; | |
position: relative; | |
margin: 16px; | |
} | |
main { | |
display: flex; | |
gap: 16px; | |
} | |
main aside { | |
display: flex; | |
flex-flow: column nowrap; | |
} | |
dialog { | |
width: 320px; | |
height: 64px; | |
position: absolute; | |
left: 66px; | |
top: 250px; | |
margin: 0; | |
border: 1px solid #000; | |
border-radius: 4px; | |
padding: 8px; | |
} | |
dialog > div { | |
width: 100%; | |
height: 100%; | |
display: flex; | |
flex-flow: column nowrap; | |
} | |
dialog form[method="dialog"] { | |
flex-grow: 1; | |
display: flex; | |
flex-flow: column nowrap; | |
justify-content: flex-end; | |
text-align: right; | |
} | |
svg.sprite { | |
display: none; | |
} | |
#help { | |
position: absolute; | |
border: 1px solid #888; | |
border-radius: 4px; | |
padding: 8px; | |
background-color: #fff; | |
font-size: 12px; | |
} | |
#help[open] { | |
width: 500px; | |
} | |
#help summary { | |
font-weight: bold; | |
cursor: pointer; | |
} | |
#help em { | |
font-weight: bold; | |
font-style: normal; | |
color: #f00; | |
} | |
#box { | |
empty-cells: show; | |
border-spacing: 0; | |
background-color: #eee; | |
user-select: none; | |
} | |
#box td { | |
border: 2px solid transparent; | |
padding: 0; | |
width: 32px; | |
height: 32px; | |
font-size: 16px; | |
text-align: center; | |
line-height: 0; | |
} | |
#box svg { | |
width: 32px; | |
height: 32px; | |
} | |
#box .box-rader { | |
border-color: #eef #aaf #aaf #eef; | |
background-color: #ccf; | |
} | |
#box .box-outer { | |
border-color: #ddd #fff #fff #ddd; | |
} | |
.box-closed #box .box-inner { | |
border-color: #eee #aaa #aaa #eee; | |
background-color: #ccc; | |
} | |
.box-closed #box .box-inner.box-turned { | |
border-color: #ccc #888 #888 #ccc; | |
background-color: #aaa; | |
} | |
.box-closed #box .box-inner .box-hidden { | |
visibility: hidden; | |
} | |
.box-closed #box .box-rader, | |
.box-closed #box .box-inner { | |
cursor: pointer; | |
} | |
.box-opened #box .box-inner { | |
border-color: #aac #668 #668 #aac; | |
background-color: #88a; | |
} | |
.box-opened #box .box-hidden { | |
visibility: visible; | |
} | |
#info { | |
flex-grow: 1; | |
margin: 0; | |
padding: 0; | |
list-style: none; | |
} | |
#info label { | |
display: inline-block; | |
width: 60px; | |
font-weight: bold; | |
} | |
#info label::after { | |
content: ":"; | |
} | |
#info span { | |
display: inline-block; | |
width: 32px; | |
text-align: right; | |
} | |
#buttons { | |
flex-grow: 0; | |
padding: 0 8px; | |
display: flex; | |
flex-flow: column nowrap; | |
justify-content: flex-end; | |
text-align: right; | |
} | |
.box-closed #reset { | |
display: none; | |
} | |
.box-opened #open { | |
display: none; | |
} | |
</style> | |
</head> | |
<body> | |
<header> | |
<h1>BlackBox v0.0.6</h1> | |
</header> | |
<article> | |
<details id="help"> | |
<summary>Help</summary> | |
<p> | |
上下左右にある<em>レーダー照射マス</em>をクリックして、<br /> | |
<em>ブラックボックス</em>の中に埋められている | |
<span class="gems">*</span> つの<em>宝石</em>を当てましょう。<br /> | |
レーダーが宝石に接触した際の挙動は、プレイしながら理解してください。<br /> | |
レーダーは <span class="rader">*</span> 回まで照射できます。<br /> | |
</p> | |
<p> | |
レーダー照射の結果から、宝石が埋まっていると思われるマスを推測して、<br /> | |
合計 <span class="gems">*</span> 箇所を<em>マーキング</em>してください。<br /> | |
マーキングしたら<em>Openボタン</em>でブラックボックスを開けましょう。<br /> | |
ひとつでもはずしたら、スコアは 0 点です。<br /> | |
全部当てたら、レーダーの照射数が少ないほどスコアがもらえます。<br /> | |
</p> | |
<p> | |
<em>シフトキー</em>を押しながらブラックボックスを | |
クリック・ドラッグすることで、<br /> | |
ブラックボックスのマスを暗くすることができます。<br /> | |
ゲーム上の意味はありませんが、考察に役立ててください。<br /> | |
</p> | |
</details> | |
<main class="box-closed"> | |
<table id="box"> | |
<tbody></tbody> | |
</table> | |
<aside> | |
<ul id="info"> | |
<li><label>Gems</label><span class="gems">*</span></li> | |
<li><label>Radar</label><span class="rader">*</span></li> | |
<li><label>Score</label><span id="score">-</span></li> | |
</ul> | |
<div id="buttons"> | |
<div> | |
<button id="reset" type="button">Reset</button> | |
<button id="open" type="button">Open</button> | |
</div> | |
</div> | |
</aside> | |
<dialog> | |
<div> | |
<div id="message"></div> | |
<form method="dialog"> | |
<div> | |
<button type="submit">OK</button> | |
</div> | |
</form> | |
</div> | |
</dialog> | |
</main> | |
</article> | |
<svg class="sprite"> | |
<defs> | |
<style> | |
.beam circle { | |
fill: #8f8; | |
} | |
.beam circle.border { | |
fill: #080; | |
filter: blur(1px); | |
} | |
.beam path { | |
fill: none; | |
stroke: #8f8; | |
stroke-width: 3; | |
} | |
.beam path.border { | |
stroke: #080; | |
stroke-width: 5; | |
filter: blur(1px); | |
} | |
#gem path { | |
fill: #dd7; | |
} | |
#gem path.highlight { | |
fill: #ff8; | |
} | |
#gem path.shadow { | |
fill: #bb6; | |
} | |
#mark path { | |
fill: none; | |
stroke: #f00; | |
stroke-width: 2; | |
} | |
</style> | |
<symbol id="beam-u" viewBox="0 0 32 32" class="beam"> | |
<path class="border" d="M 16,0 v 16" /> | |
<circle class="border" cx="16" cy="16" r="5" /> | |
<path d="M 16,0 v 16" /> | |
<circle cx="16" cy="16" r="4" /> | |
</symbol> | |
<symbol id="beam-r" viewBox="0 0 32 32" class="beam"> | |
<path class="border" d="M 32,16 h -16" /> | |
<circle class="border" cx="16" cy="16" r="5" /> | |
<path d="M 32,16 h -16" /> | |
<circle cx="16" cy="16" r="4" /> | |
</symbol> | |
<symbol id="beam-d" viewBox="0 0 32 32" class="beam"> | |
<path class="border" d="M 16,32 v -16" /> | |
<circle class="border" cx="16" cy="16" r="5" /> | |
<path d="M 16,32 v -16" /> | |
<circle cx="16" cy="16" r="4" /> | |
</symbol> | |
<symbol id="beam-l" viewBox="0 0 32 32" class="beam"> | |
<path class="border" d="M 0,16 h 16" /> | |
<circle class="border" cx="16" cy="16" r="5" /> | |
<path d="M 0,16 h 16" /> | |
<circle cx="16" cy="16" r="4" /> | |
</symbol> | |
<symbol id="beam-ur" viewBox="0 0 32 32" class="beam"> | |
<path class="border" d="M 16,0 v 16 h 16" /> | |
<path d="M 16,0 v 16 h 16" /> | |
</symbol> | |
<symbol id="beam-rd" viewBox="0 0 32 32" class="beam"> | |
<path class="border" d="M 32,16 h -16 v 16" /> | |
<path d="M 32,16 h -16 v 16" /> | |
</symbol> | |
<symbol id="beam-dl" viewBox="0 0 32 32" class="beam"> | |
<path class="border" d="M 0,16 h 16 v 16" /> | |
<path d="M 0,16 h 16 v 16" /> | |
</symbol> | |
<symbol id="beam-ul" viewBox="0 0 32 32" class="beam"> | |
<path class="border" d="M 16,0 v 16 h -16" /> | |
<path d="M 16,0 v 16 h -16" /> | |
</symbol> | |
<symbol id="beam-ud" viewBox="0 0 32 32" class="beam"> | |
<path class="border" d="M 16,0 v 32" /> | |
<path d="M 16,0 v 32" /> | |
</symbol> | |
<symbol id="beam-rl" viewBox="0 0 32 32" class="beam"> | |
<path class="border" d="M 0,16 h 32" /> | |
<path d="M 0,16 h 32" /> | |
</symbol> | |
<symbol id="beam-cr" viewBox="0 0 32 32" class="beam"> | |
<path class="border" d="M 16,0 v 32 M 0,16 h 32" /> | |
<path d="M 16,0 v 32 M 0,16 h 32" /> | |
</symbol> | |
<symbol id="gem" viewBox="0 0 32 32"> | |
<path d="M 16,0 L 0,16 L 16,32 L 32,16" /> | |
<path class="highlight" d="M 0,16 h 16 v -16" /> | |
<path class="shadow" d="M 16,32 v -16 h 16" /> | |
</symbol> | |
<symbol id="mark" viewBox="0 0 32 32"> | |
<path d="M 2,10 v -8 h 8" /> | |
<path d="M 2,22 v 8 h 8" /> | |
<path d="M 30,10 v -8 h -8" /> | |
<path d="M 30,22 v 8 h -8" /> | |
<path d="M 10,16 h 12" /> | |
<path d="M 16,10 v 12" /> | |
</symbol> | |
</defs> | |
</svg> | |
<script> | |
const Box = { | |
size: 12, | |
gems: 4, | |
rader: 10, | |
raderIndex: 0, | |
turned: null, | |
}; | |
const Status = { | |
NONE: 0b00000, | |
BEAM_U: 0b00001, | |
BEAM_R: 0b00010, | |
BEAM_UR: 0b00011, | |
BEAM_D: 0b00100, | |
BEAM_UD: 0b00101, | |
BEAM_RD: 0b00110, | |
BEAM_L: 0b01000, | |
BEAM_UL: 0b01001, | |
BEAM_RL: 0b01010, | |
BEAM_DL: 0b01100, | |
BEAM_CR: 0b01111, | |
GEM: 0b10000, | |
}; | |
class Cell { | |
td; | |
constructor(td) { | |
this.td = td; | |
} | |
getX() { | |
return parseInt(this.td.attr("box-x")); | |
} | |
getY() { | |
return parseInt(this.td.parent().attr("box-y")); | |
} | |
toPosition() { | |
return new Position(this.getX(), this.getY()); | |
} | |
} | |
class Target extends Cell { | |
constructor(td) { | |
super(td); | |
} | |
getStatus() { | |
return parseInt(this.td.attr("box-status")); | |
} | |
setStatus(status) { | |
this.td.attr("box-status", status); | |
return this; | |
} | |
mergeStatus(status) { | |
return this.setStatus(this.getStatus() | status); | |
} | |
isMarked() { | |
return this.td.attr("box-mark") === "true"; | |
} | |
setMark(mark) { | |
this.td.attr("box-mark", mark); | |
return this; | |
} | |
toggleMark() { | |
return this.setMark(!this.isMarked()); | |
} | |
existsGem() { | |
return this.getStatus() === Status.GEM; | |
} | |
show() { | |
this.td.children().remove(); | |
const status = this.getStatus(); | |
const marked = this.isMarked(); | |
const use = []; | |
switch (status) { | |
case Status.BEAM_U: | |
use.push('<use href="#beam-u" class="box-hidden" />'); | |
break; | |
case Status.BEAM_R: | |
use.push('<use href="#beam-r" class="box-hidden" />'); | |
break; | |
case Status.BEAM_UR: | |
use.push('<use href="#beam-ur" class="box-hidden" />'); | |
break; | |
case Status.BEAM_D: | |
use.push('<use href="#beam-d" class="box-hidden" />'); | |
break; | |
case Status.BEAM_UD: | |
use.push('<use href="#beam-ud" class="box-hidden" />'); | |
break; | |
case Status.BEAM_RD: | |
use.push('<use href="#beam-rd" class="box-hidden" />'); | |
break; | |
case Status.BEAM_L: | |
use.push('<use href="#beam-l" class="box-hidden" />'); | |
break; | |
case Status.BEAM_UL: | |
use.push('<use href="#beam-ul" class="box-hidden" />'); | |
break; | |
case Status.BEAM_RL: | |
use.push('<use href="#beam-rl" class="box-hidden" />'); | |
break; | |
case Status.BEAM_DL: | |
use.push('<use href="#beam-dl" class="box-hidden" />'); | |
break; | |
case Status.BEAM_CR: | |
use.push('<use href="#beam-cr" class="box-hidden" />'); | |
break; | |
case Status.GEM: | |
use.push('<use href="#gem" class="box-hidden" />'); | |
break; | |
} | |
if (marked) { | |
use.push('<use href="#mark" />'); | |
} | |
if (use.length > 0) { | |
const svg = this.td.append("<svg></svg>").find("svg").last(); | |
use.forEach((item) => svg.append(item)); | |
} | |
return this; | |
} | |
} | |
class Rader extends Cell { | |
constructor(td) { | |
super(td); | |
} | |
getDir() { | |
return parseInt(this.td.attr("box-rader-dir")); | |
} | |
getIndex() { | |
return parseInt(this.td.attr("box-rader-index")); | |
} | |
setIndex(index) { | |
this.td.attr("box-rader-index", index); | |
return this; | |
} | |
shoot(index) { | |
this.setIndex(index); | |
return this.toPosition().toCursor(this.getDir()); | |
} | |
show() { | |
const index = this.getIndex(); | |
this.td.text(index !== 0 ? index : ""); | |
return this; | |
} | |
} | |
class Position { | |
x; | |
y; | |
constructor(x, y) { | |
this.x = x; | |
this.y = y; | |
} | |
isBorder() { | |
return ( | |
this.x === 0 || | |
this.x === Box.size - 1 || | |
this.y === 0 || | |
this.y === Box.size - 1 | |
); | |
} | |
getTd() { | |
return sq("#box tbody") | |
.find(`tr[box-y="${this.y}"] td[box-x="${this.x}"]`) | |
.first(); | |
} | |
toCursor(dir) { | |
return new Cursor(this.x, this.y, dir); | |
} | |
} | |
class Cursor extends Position { | |
dir; | |
constructor(x, y, dir) { | |
super(x, y); | |
this.dir = dir; | |
} | |
forward() { | |
switch (this.dir) { | |
case 0: | |
--this.y; | |
break; | |
case 1: | |
++this.x; | |
break; | |
case 2: | |
++this.y; | |
break; | |
case 3: | |
--this.x; | |
break; | |
default: | |
throw `illegal dir: ${this.dir}`; | |
} | |
return new Target(this.getTd()); | |
} | |
turnRight() { | |
this.dir = (this.dir + 1) % 4; | |
return this; | |
} | |
turnLeft() { | |
this.dir = (this.dir + 3) % 4; | |
return this; | |
} | |
lookFrontRight() { | |
switch (this.dir) { | |
case 0: | |
return new Position(this.x + 1, this.y - 1); | |
case 1: | |
return new Position(this.x + 1, this.y + 1); | |
case 2: | |
return new Position(this.x - 1, this.y + 1); | |
case 3: | |
return new Position(this.x - 1, this.y - 1); | |
default: | |
throw `illegal dir: ${dir}`; | |
} | |
} | |
lookFrontCenter() { | |
switch (this.dir) { | |
case 0: | |
return new Position(this.x, this.y - 1); | |
case 1: | |
return new Position(this.x + 1, this.y); | |
case 2: | |
return new Position(this.x, this.y + 1); | |
case 3: | |
return new Position(this.x - 1, this.y); | |
default: | |
throw `illegal dir: ${dir}`; | |
} | |
} | |
lookFrontLeft() { | |
switch (this.dir) { | |
case 0: | |
return new Position(this.x - 1, this.y - 1); | |
case 1: | |
return new Position(this.x + 1, this.y - 1); | |
case 2: | |
return new Position(this.x + 1, this.y + 1); | |
case 3: | |
return new Position(this.x - 1, this.y + 1); | |
default: | |
throw `illegal dir: ${dir}`; | |
} | |
} | |
} | |
function buryGem() { | |
for (let count = 0; count < Box.gems; count++) { | |
let target; | |
do { | |
const x = Math.floor(Math.random() * (Box.size - 4)) + 2; | |
const y = Math.floor(Math.random() * (Box.size - 4)) + 2; | |
const position = new Position(x, y); | |
target = new Target(position.getTd()); | |
} while (target.getStatus() !== Status.NONE); | |
target.setStatus(Status.GEM); | |
} | |
} | |
function showBox() { | |
sq("#box .box-target").each(function () { | |
new Target(sq(this)).show(); | |
}); | |
sq("#box .box-rader").each(function () { | |
new Rader(sq(this)).show(); | |
}); | |
} | |
sq("#help span.gems").text(Box.gems); | |
sq("#help span.rader").text(Box.rader); | |
sq("#info span.gems").text(Box.gems); | |
sq("#info span.rader").text(Box.rader); | |
for (let y = 0; y < Box.size; y++) { | |
const tr = sq("#box tbody").append(`<tr box-y="${y}"></tr>`).find("tr").last(); | |
const innerY = y > 1 && y < Box.size - 2; | |
for (let x = 0; x < Box.size; x++) { | |
const td = tr.append(`<td box-x="${x}"></td>`).find("td").last(); | |
switch (x) { | |
case 0: | |
if (innerY) | |
td.addClass("box-rader") | |
.attr("box-rader-dir", 1) | |
.attr("box-rader-index", 0) | |
.attr("title", "レーダー照射"); | |
break; | |
case Box.size - 1: | |
if (innerY) | |
td.addClass("box-rader") | |
.attr("box-rader-dir", 3) | |
.attr("box-rader-index", 0) | |
.attr("title", "レーダー照射"); | |
break; | |
case 1: | |
case Box.size - 2: | |
if (innerY) | |
td.addClass("box-target").addClass("box-outer").attr("box-status", 0); | |
break; | |
default: | |
switch (y) { | |
case 0: | |
td.addClass("box-rader") | |
.attr("box-rader-dir", 2) | |
.attr("box-rader-index", 0) | |
.attr("title", "レーダー照射"); | |
break; | |
case Box.size - 1: | |
td.addClass("box-rader") | |
.attr("box-rader-dir", 0) | |
.attr("box-rader-index", 0) | |
.attr("title", "レーダー照射"); | |
break; | |
case 1: | |
case Box.size - 2: | |
td.addClass("box-target").addClass("box-outer").attr("box-status", 0); | |
break; | |
default: | |
td.addClass("box-target") | |
.addClass("box-inner") | |
.attr("box-status", 0) | |
.attr("box-mark", false) | |
.attr("title", "ブラックボックス"); | |
} | |
} | |
} | |
} | |
sq(".box-rader").on("click", function () { | |
if (sq("main").hasClass("box-opened")) { | |
return; | |
} | |
if (Box.raderIndex === Box.rader) { | |
return; | |
} | |
const rader = new Rader(sq(this)); | |
if (rader.getIndex() !== 0) { | |
return; | |
} | |
const cursor = rader.shoot(++Box.raderIndex); | |
let target = cursor.forward(); | |
let entered = false; | |
let disappeared = false; | |
do { | |
switch (cursor.dir) { | |
case 0: | |
target.mergeStatus(Status.BEAM_D); | |
break; | |
case 1: | |
target.mergeStatus(Status.BEAM_L); | |
break; | |
case 2: | |
target.mergeStatus(Status.BEAM_U); | |
break; | |
case 3: | |
target.mergeStatus(Status.BEAM_R); | |
break; | |
default: | |
throw `illegal dir: ${cursor.dir}`; | |
} | |
const frontLeft = new Target(cursor.lookFrontLeft().getTd()).existsGem(); | |
const frontCenter = new Target(cursor.lookFrontCenter().getTd()).existsGem(); | |
const frontRight = new Target(cursor.lookFrontRight().getTd()).existsGem(); | |
if ( | |
frontCenter || | |
(frontLeft && frontRight) || | |
(!entered && (frontLeft || frontRight)) | |
) { | |
disappeared = true; | |
break; | |
} else if (frontRight) { | |
cursor.turnLeft(); | |
} else if (frontLeft) { | |
cursor.turnRight(); | |
} | |
switch (cursor.dir) { | |
case 0: | |
target.mergeStatus(Status.BEAM_U); | |
break; | |
case 1: | |
target.mergeStatus(Status.BEAM_R); | |
break; | |
case 2: | |
target.mergeStatus(Status.BEAM_D); | |
break; | |
case 3: | |
target.mergeStatus(Status.BEAM_L); | |
break; | |
default: | |
throw `illegal dir: ${cursor.dir}`; | |
} | |
target = cursor.forward(); | |
entered = true; | |
} while (!cursor.isBorder()); | |
if (!disappeared) { | |
new Rader(cursor.getTd()).setIndex(Box.raderIndex); | |
} | |
showBox(); | |
}); | |
sq(".box-inner").on("click", function (event) { | |
if (sq("main").hasClass("box-opened")) { | |
return; | |
} | |
if (!event.shiftKey) { | |
new Target(sq(this)).toggleMark(); | |
showBox(); | |
} | |
}); | |
sq(".box-inner").on("mousedown", function (event) { | |
if (sq("main").hasClass("box-opened")) { | |
return; | |
} | |
if (event.shiftKey && event.button === 0) { | |
Box.turned = sq(this).toggleClass("box-turned").hasClass("box-turned"); | |
} | |
}); | |
sq(".box-inner").on("mousemove", function (event) { | |
if (sq("main").hasClass("box-opened")) { | |
return; | |
} | |
event.stopPropagation(); | |
if (event.shiftKey && Box.turned !== null) { | |
if (Box.turned) { | |
sq(this).addClass("box-turned"); | |
} else { | |
sq(this).removeClass("box-turned"); | |
} | |
} | |
}); | |
sq("body").on("mouseup", function (event) { | |
if (sq("main").hasClass("box-opened")) { | |
return; | |
} | |
Box.turned = null; | |
}); | |
sq("body").on("mousemove", function (event) { | |
if (sq("main").hasClass("box-opened")) { | |
return; | |
} | |
Box.turned = null; | |
}); | |
sq("#reset").on("click", function () { | |
location.reload(); | |
}); | |
sq("#open").on("click", function () { | |
if (sq('[box-mark="true"]').length !== Box.gems) { | |
sq("dialog #message").text(`宝石は ${Box.gems} 箇所です。`); | |
sq("dialog").get(0).showModal(); | |
return; | |
} | |
sq("main").prop("className", "box-opened"); | |
const hits = sq('[box-status="16"][box-mark="true"]').length; | |
const score = hits === Box.gems ? (Box.gems + Box.rader - Box.raderIndex) * 10 : 0; | |
sq("#score").text(score); | |
}); | |
buryGem(); | |
showBox(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment