Balance Puzzle
<html>
    <head>
        <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0">
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="">
        <link href="https://fonts.googleapis.com/css2?family=Alfa+Slab+One&amp;display=swap" rel="stylesheet">
        <link href="https://1119s.wdfiles.com/local--code/balance-puzzle/2" rel="stylesheet">
    </head>
    <body>
        <div class="content-wrapper">
            <div id="puzzle">
                <div id="balance" class="balance">
                    <div class="bar">
                        <div id="plate_left" class="plate left">
                        </div>
                        <div id="plate_right" class="plate right">
                        </div>
                    </div>
                </div>
                <button onclick="weigh()" class="weigh">はかる</button>
                <div id="bottles" class="bottles">
                    <div id="bottle_a" class="bottle" data-tag="A">
                    </div>
                    <div id="bottle_b" class="bottle" data-tag="B">
                    </div>
                    <div id="bottle_c" class="bottle" data-tag="C">
                    </div>
                    <div id="bottle_d" class="bottle" data-tag="D">
                    </div>
                    <div id="bottle_e" class="bottle" data-tag="E">
                    </div>
                    <div id="bottle_f" class="bottle" data-tag="F">
                    </div>
                    <div id="bottle_g" class="bottle" data-tag="G">
                    </div>
                </div>
            </div>
            <div id="side-bar">
                <div class="side-block">
                    <form id="seed">
                        シード(数字4桁):
                        <br>
                        <input type="text" name="seed" size="4">
                        <input type="submit" value="決定" pattern="\d{4}">
                    </form>
                </div>
                <div class="side-block">
                    <button onclick="newPuzzle()">ランダム出題</button>
                </div>
                <div class="side-block">
                    <button onclick="genPuzzle()">このシードで再挑戦</button>
                </div>
                <div class="side-block">
                    <button onclick="copySeed()">シードをコピー</button>
                    <br>
                    <button onclick="copyURL()">シード付URLをコピー</button>
                </div>
                <div class="menu">
                    <span></span>
                    <span></span>
                    <span></span>
                </div>
            </div>
            <script type="text/javascript" src="https://1119s.wdfiles.com/local--code/balance-puzzle/3"></script>
        </div>
    </body>
</html>
body {
    margin: 0;
}
.content-wrapper {
    position: relative;
    height: 100%;
}
#puzzle {
    display: grid;
    grid-template-rows: 1fr auto;
}
.balance {
    width: 60%;
    max-width: 25em;
    aspect-ratio: 4 / 3;
    margin: 3em auto;
    position: relative;
}
.balance::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    background-image: var(--pillar);
    background-position: center;
    background-size: 100%;
    background-repeat: no-repeat;
}
.bar {
    position: absolute;
    top: 12%;
    left: 10%;
    width: 80%;
    height: 2.5%;
    background: #000;
    transform-origin: center;
    transform: rotate(var(--a));
    transition: --a 1s;
}
.balance.tilt-left .bar {
    --a: -10deg;
}
.balance.tilt-right .bar {
    --a: 10deg;
}
 
button.weigh {
    justify-self: center;
    margin: 0 auto 3em;
}
 
.plate {
    position: absolute;
    top: 50%;
    width: 50%;
    aspect-ratio: 1;
    transform-origin: center 0;
    transform: rotate(calc(var(--a) * -1));
}
.plate::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    background-image: var(--plate);
    background-position: center;
    background-size: 100%;
    background-repeat: no-repeat;
}
.plate.left {
    left: -16%;
}
.plate.right{
    right: -16%;
}
.bottles {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    width: 90%;
    margin: 0 auto;
}
.bottle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 5rem;
    max-width: 25%;
    aspect-ratio: 1;
    background: var(--bottle) center no-repeat;
    outline: solid 0.3125em transparent;
    outline-offset: -0.625em;
    cursor: pointer;
}
.bottle::before {
    content: attr(data-tag);
    position: relative;
    top: 0.625em;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.5em;
    aspect-ratio: 1;
    border-radius: 50%;
    background: #000;
    color: #fff;
    font-family: "Alfa Slab One", serif;
    font-size: 90%;
}
.bottle.selected_l {
    outline-color: #00f;
}
.bottle.selected_r {
    outline-color: #f44;
}
 
#side-bar {
    position: absolute;
    z-index: 9;
    top: 0;
    left: -18em;
    width: 15em;
    height: 100%;
    border-right: solid 2px #0cf;
    background-color: #e4e4e4;
    transition: left 0.25s;
}
#side-bar:hover {
    left: 0;
}
.side-block {
    padding: 1em;
    border-bottom: solid 2px #888;
}
.side-block form {
    margin: 0;
}
#side-bar .menu {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5em;
    position: fixed;
    z-index: -1;
    left: 1em;
    bottom: 1em;
    width: 3em;
    height: 3em;
    transition: opacity 0.25s;
}
#side-bar .menu:hover {
    opacity: 0;
}
#side-bar .menu span {
    display: inline-block;
    width: 2.5em;
    height: 0.25em;
    background: #000;
}
 
:root {
    --bottle: url('https://upload.wikimedia.org/wikipedia/commons/5/5f/202003_Laboratory_instrument_reagent_bottle_mono.svg');
    --pillar: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHZpZXdCb3g9IjAgMCAxNjAwIDEyMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTgwMCAwIDQwIDYwLTIwIDIwaC00MGwtMjAtMjB6bS0yMCA4NWg0MGwyMCAyMGgtODB6Ii8+PHBhdGggZD0ibTc2MCAxMTBoODB2OTBoLTgwem0wIDk1aDgwdjMwaC04MHoiLz48cGF0aCBkPSJtNzYwIDI0MGg4MGwyMCAxNS0yMCAxNWgtODBsLTIwLTE1em0wIDM1aDgwdjQyMGgtODB6Ii8+PHBhdGggZD0ibTc2MCA3MDBoODBsMjAgMTUtMjAgMTVoLTgwbC0yMC0xNXptMCAzNWg4MGwyMCAzMGgtMTIweiIvPjxwYXRoIGQ9Im03NDAgNzcwaDEyMHYyODBoLTEyMHptMCAyODVoMTIwbDIwIDE1LTIwIDE1aC0xMjBsLTIwLTE1eiIvPjxwYXRoIGQ9Im03NDAgMTA5MGgxMjB2MjBoLTEyMHptLTY2MCAyNWgxNDQwdjMwaC0xNDQwem0tMzAgMzVoMTUwMHY1MGgtMTUwMHoiLz48cGF0aCBkPSJtNzU4IDExMHY5MG04NCAwdi05MCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjQiLz48L3N2Zz4=');
    --plate: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHZpZXdCb3g9IjAgMCA2NDAgNjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Im00MyA1OTAgMjc3LTU5MCAyNzcgNTkwIiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMTAiLz48cGF0aCBkPSJtMCA1ODBoNjQwdjIwaC0yMGwtMTAgNDBoLTU4MGwtMTAtNDBoLTIweiIvPjwvc3ZnPg==');
}
 
@property --a {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: true;
}
const bottles = document.getElementById("bottles");
const btls = Array.from(bottles.children);
 
// ボトルの選択状況の付け外し機能
btls.forEach((elm) => {
  elm.addEventListener("click", function() {
    if (this.classList.length == 1) {
      this.classList.add("selected_l");
    } else if (this.classList.contains("selected_l")) {
      this.classList.replace("selected_l", "selected_r");
    } else {
      this.classList.remove("selected_r");
    }
  }, false);
});
 
const bottleTypes = new Map();
const bottle_list = ["pd", "pd", "pd", "pt", "ps", "ps", "ps"];
const WEIGHTS = {
  pd: 1,    // power drug - 劇薬
  pt: 2,    // potion - 魔法薬
  ps: 3     // poison - 毒薬
};
 
// URLからシードを取得する
function getSeed() {
  const params = new URLSearchParams(window.location.search);
  const pz = params.get("pz");
  if (pz && /^\d{4}$/.test(pz)) {
    return pz;
  } else {
    return null;
  }
}
// シード付き疑似乱数生成器
// https://github.com/cprosche/mulberry32
function mulberry32(seed) {
  return function() {
    let t = seed += 0x6D2B79F5;
    t = Math.imul(t ^ t >>> 15, t | 1);
    t ^= t + Math.imul(t ^ t >>> 7, t | 61);
    return frac(((t ^ t >>> 14) >>> 0), 4294967296);
  };
}
function shuffle(array, seed) {
  const rng = mulberry32(seed);
  const arr = array.slice();
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}
function genPuzzle() {
  const seed = parseInt(getSeed(), 10);
  if (seed == null) {
    seed = Math.floor(Math.random() * 10000);
    history.pushState(null, "", `?pz=${seed}`);
  }
  const shuffled = shuffle(bottle_list, seed);
 
  btls.forEach((bottle, i) => {
    const id = bottle.id;
    bottleTypes.set(id, shuffled[i]);
  });
 
  console.log(seed, shuffled);
}
 
// ページロード時にパズルを生成する
document.addEventListener("DOMContentLoaded", () => {
  genPuzzle();
});
 
// 瓶の重さの合計を求める
function calcWeight(bottleElements) {
  return bottleElements.reduce((sum, el) => {
    let type = bottleTypes.get(el.id);
    return sum + WEIGHTS[type];
  }, 0);
}
 
// 左右の皿を比べる
function compare(leftWeight, rightWeight) {
  const balance = document.getElementById("balance");
  if (leftWeight > rightWeight) {
    balance.className = "balance tilt-left";
  } else if (rightWeight > leftWeight) {
    balance.className = "balance tilt-right";
  } else {
    balance.className = "balance";
  }
}
 
const sel_l = document.getElementsByClassName("selected_l");
const sel_r = document.getElementsByClassName("selected_r");
 
function weigh() {
  const left = Array.from(sel_l);
  const right = Array.from(sel_r);
 
  const lw = calcWeight(left);
  const rw = calcWeight(right);
 
  compare(lw, rw);
 
  console.log(`左: ${lw}, 右: ${rw}`);
}
 
/* ==== ナビゲーション ==== */
 
const seedForm = document.getElementById("seed");
seedForm.addEventListener("submit", (e) => {
    e.preventDefault();
  const input = seedForm.elements["seed"].value.trim();
  if (/^\d{4}$/.test(input)) {
    location.href = `?pz=${input}`;
  } else {
    alert("4桁の数値を入力してください");
  }
});
 
function newPuzzle() {
  const newSeed = Math.floor(Math.random() * 10000).toString().padStart(4, "0");
  location.href = `?pz=${newSeed}`;
}
 
function copySeed() {
  const seed = getSeed();
  navigator.clipboard.writeText(seed)
    .then(() => alert(`シード ${seed} をコピーしました`))
    .catch(() => alert("コピーに失敗しました"));
}
function copyURL() {
  const url = location.href;
  navigator.clipboard.writeText(url)
    .then(() => alert("URLをコピーしました"))
    .catch(() => alert("コピーに失敗しました"));
}
 
// 除算記号を引用符判定するのやめてください 
function frac(dividend, divisor) {
  return dividend / divisor;
}
/* source: http://ah-sandbox.wikidot.com/component:collapsible-sidebar-x1 */
 
#top-bar .open-menu a {
        position: fixed;
        bottom: 0.5em;
        left: 0.5em;
        z-index: 15;
        font-family: san-serif;
        font-size: 30px;
        font-weight: 700;
        width: 30px;
        height: 30px;
        line-height: 0.9em;
        text-align: center;
        border: 0.2em solid #888 !important;
        background-color: #fff !important;
        border-radius: 3em;
        color: #888 !important;
        text-decoration: none!important;
}
 
@media (min-width: 768px) {
 
    .mobile-top-bar {
        display: block;
    }
 
    .mobile-top-bar li {
        display: none;
    }
 
    #main-content {
        max-width: 708px;
        margin: 0 auto;
        padding: 0;
        transition: max-width 0.2s ease-in-out;
    }
 
    #side-bar {
        display: block;
        position: fixed;
        top: 0;
        left: -25em;
        width: 17em;
        height: 100%;
        background-color: rgb(184, 134, 134);
        overflow-y: auto;
        z-index: 10;
        padding: 1em 1em 0 1em;
        -webkit-transition: left 0.5s ease-in-out 0.1s;
        -moz-transition: left 0.5s ease-in-out 0.1s;
        -ms-transition: left 0.5s ease-in-out 0.1s;
        -o-transition: left 0.5s ease-in-out 0.1s;
        transition: left 0.5s ease-in-out 0.1s;
    }
 
    #side-bar:after {
        content: "";
        position: absolute;
        top: 0;
        width: 0;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.2);
 
    }
 
    #side-bar:target {
        display: block;
        left: 0;
        width: 17em;
        margin: 0;
        border: 1px solid #dedede;
        z-index: 10;
    }
 
    #side-bar:target + #main-content {
        left: 0;
    }
 
    #side-bar:target .close-menu {
        display: block;
        position: fixed;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        background: rgba(0,0,0,0.3) 1px 1px repeat;
        z-index: -1;
    }
}
特に明記しない限り、このページのコンテンツは次のライセンスの下にあります: Creative Commons Attribution-ShareAlike 3.0 License