天秤ゲーム 重さのバランスを取る

重さが釣り合うように荷物を左右に移動させよう

view source

JavaScript

document.title = '天秤ゲーム 重さのバランスを取る'
//重さが釣り合うように荷物を左右に移動させよう
//weight-balance

let { canvas, ctx, cx, cy, w, h, min, demo } = readyCanvas();

let stage = 1;
let angle = 0; // 天秤の傾き角度(後で操作可能)
let isClear = false;
let changeCount = 0;



function drawBox(){
  const postHeight = min/8;
  const postCenter = h - postHeight;
  // 左右の枠(上から postCenter まで)
  const boxWidth = min / 2;
  const boxHeight = h/2;
  const margin = 0;

  // 左の枠
  ctx.beginPath();
  ctx.rect(cx, 0, -boxWidth, boxHeight);
  ctx.stroke();

  // 右の枠
  ctx.beginPath();
  ctx.rect(cx, 0, boxWidth, boxHeight);
  ctx.stroke();

  // === 左右の総重量の計算 ===
  const leftTotal = leftWeights.reduce((sum, w) => sum + w.weight, 0);
  const rightTotal = rightWeights.reduce((sum, w) => sum + w.weight, 0);
  const fontSize = min /8;
  const height = h / 2 + fontSize;

  ctx.font = 'bold '+ fontSize+'px sans-serif';
  ctx.fillStyle = 'black';
  ctx.textAlign = 'center';

  if(stage < 5 || changeCount > 9 || isClear){
    // === 左の箱の下 ===
    ctx.fillText(
      leftTotal,
      cx - min / 4, // 左枠の中央
      height
    );

    // === 右の箱の下 ===
    ctx.fillText(
      rightTotal,
      cx + min / 4, // 右枠の中央
      height
    );
  }
}

function drawScale(angleRad = 0) {

  const centerX = cx;
  const centerY = cy;

  const postHeight = min/8;
  const postCenter = h - postHeight;
  // 支柱
  ctx.beginPath();
  ctx.moveTo(centerX, h);
  ctx.lineTo(centerX, postCenter);
  ctx.stroke();

  // アーム(支点を中心に回転)
  const armLength = min/4;
  const p = min/16;
  const leftX = centerX - Math.cos(angleRad) * armLength;
  const leftY = postCenter + Math.sin(angleRad) * armLength;
  const rightX = centerX + Math.cos(angleRad) * armLength;
  const rightY = postCenter - Math.sin(angleRad) * armLength;

  ctx.beginPath();
  ctx.moveTo(leftX, leftY);
  ctx.lineTo(centerX, postCenter);
  ctx.lineTo(rightX, rightY);
  ctx.stroke();

  // 左皿 - コの字
  const trayWidth = min/8;
  const trayHeight = min/32;
  ctx.beginPath();
  ctx.moveTo(leftX - trayWidth / 2, leftY - p);
  ctx.lineTo(leftX - trayWidth / 2, leftY - p + trayHeight);
  ctx.lineTo(leftX + trayWidth / 2, leftY - p + trayHeight);
  ctx.lineTo(leftX + trayWidth / 2, leftY - p);
  ctx.stroke();

  // 右皿 - コの字
  ctx.beginPath();
  ctx.moveTo(rightX - trayWidth / 2, rightY - p);
  ctx.lineTo(rightX - trayWidth / 2, rightY - p + trayHeight);
  ctx.lineTo(rightX + trayWidth / 2, rightY - p + trayHeight);
  ctx.lineTo(rightX + trayWidth / 2, rightY - p);
  ctx.stroke();
}


const shapeList = ['circle', 'square', 'triangle', 'dia'];

const labelWeights = {
  A: 2+Math.floor(Math.random() * 2),
  B: 5+Math.floor(Math.random() * 2),
  C: 7+Math.floor(Math.random() * 2),
  D: 1,  // 重さ1の補填用
};
const weightLabels = ['A', 'B', 'C'];
const shapeMap = {};

// シャッフル関数
function shuffle(array) {
  return array.sort(() => Math.random() - 0.5);
}

// 重さラベル → 具体的な形状にマッピング
function createShapeMapping() {
  const shuffled = shuffle([...shapeList]);
  for (let i = 0; i < weightLabels.length; i++) {
    shapeMap[weightLabels[i]] = shuffled[i];
  }
}


function initBalancedWeights() {
  leftWeights.length = 0;
  rightWeights.length = 0;

  createShapeMapping(); // 重さラベルと形状のマッピングを初期化

  const labels = weightLabels; // A, B, C
  let targetWeight = 24;//目標総重量
  targetWeight = 12 + stage * 4;
  targetWeight = Math.min(64, targetWeight);

  let leftTotal = 0;
  let rightTotal = 0;

  while (leftTotal < targetWeight) {
    const label = labels[Math.floor(Math.random() * labels.length)];
    const w = new Weight(0, 0, label);
    leftTotal += w.weight;
    leftWeights.push(w);
  }

  while (rightTotal < targetWeight) {
    const label = labels[Math.floor(Math.random() * labels.length)];
    const w = new Weight(0, 0, label);
    rightTotal += w.weight;
    rightWeights.push(w);
  }

  // バランス補正に D を使う
  const diff = leftTotal - rightTotal;
  if (diff !== 0) {
    const addTo = diff > 0 ? rightWeights : leftWeights;
    const count = Math.abs(diff);
    for (let i = 0; i < count; i++) {
      const w = new Weight(0, 0, 'D');
      addTo.push(w);
    }
  }

  arrangeWeights(leftWeights, true);
  arrangeWeights(rightWeights, false);
}

function randomSwapWeights(count = 3) {
  for (let i = 0; i < count; i++) {
    if (leftWeights.length === 0 || rightWeights.length === 0) break;

    const leftIndex = Math.floor(Math.random() * leftWeights.length);
    const rightIndex = Math.floor(Math.random() * rightWeights.length);

    const leftItem = leftWeights.splice(leftIndex, 1)[0];
    const rightItem = rightWeights.splice(rightIndex, 1)[0];

    rightWeights.push(leftItem);
    leftWeights.push(rightItem);
  }

  // 再配置して整列
  arrangeWeights(leftWeights, true);
  arrangeWeights(rightWeights, false);
}
function isBalanced() {
  const leftTotal = leftWeights.reduce((sum, w) => sum + w.weight, 0);
  const rightTotal = rightWeights.reduce((sum, w) => sum + w.weight, 0);
  return leftTotal === rightTotal;
}
function swapUntilUnbalanced() {
  const maxTries = 20; // 無限ループ防止の安全装置
  let tries = 0;

  while (isBalanced() && tries < maxTries) {
    let count = Math.min(9, stage);
    randomSwapWeights(count); //
    tries++;
  }
}




// 重りをボックス内に並べる
function arrangeWeights(list, isLeft) {
  const margin = 30;
  const boxWidth = min / 2 - margin * 2;
  const boxHeight = h / 2 - 60;

  const itemsPerRow = 5;
  const spacingX = boxWidth / itemsPerRow;
  const spacingY = min/8; // 各行の高さ

  const startX = isLeft
    ? cx - min / 2 + margin + spacingX / 2
    : cx + margin + spacingX / 2;
  const startY = 50;

  for (let i = 0; i < list.length; i++) {
    const w = list[i];
    const col = i % itemsPerRow;
    const row = Math.floor(i / itemsPerRow);
    w.x = startX + col * spacingX;
    w.y = startY + row * spacingY;
  }
}





class Weight {
  constructor(x, y, label) {
    this.x = x;
    this.y = y;
    this.label = label;
    this.weight = labelWeights[label];
    this.shape = label === 'D' ? 'circle' : shapeMap[label];
    this.size = min / 12;
  }

  draw(ctx) {
    const size = this.size;
    ctx.beginPath();
    switch (this.shape) {
      case 'circle':
        ctx.arc(this.x, this.y, size / 2, 0, Math.PI * 2);
        break;
      case 'square':
        ctx.rect(this.x - size / 2, this.y - size / 2, size, size);
        break;
      case 'triangle':
        ctx.moveTo(this.x, this.y - size / 2);
        ctx.lineTo(this.x - size / 2, this.y + size / 2);
        ctx.lineTo(this.x + size / 2, this.y + size / 2);
        ctx.closePath();
        break;
      case 'dia':
        ctx.moveTo(this.x, this.y - size / 2);
        ctx.lineTo(this.x - size / 2, this.y);
        ctx.lineTo(this.x, this.y + size / 2);
        ctx.lineTo(this.x + size / 2, this.y);
        ctx.closePath();
        break;
    }
    ctx.stroke();

    if(stage < 3 || changeCount > 18 || isClear){
      // 重さの表示
      ctx.font = (size/2) +'px sans-serif';
      ctx.fillStyle = 'black';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(this.weight.toString(), this.x, this.y);
    }
  }
}








const leftWeights = [];
const rightWeights = [];


function getAngleFromWeights() {
  const leftTotal = leftWeights.reduce((sum, w) => sum + w.weight, 0);
  const rightTotal = rightWeights.reduce((sum, w) => sum + w.weight, 0);
  const diff = leftTotal - rightTotal;

  // 傾き最大±0.3ラジアンで正規化(差が±20までの想定)
  return Math.max(-0.3, Math.min(0.3, diff / 20));
}
function drawWeights() {
  [...leftWeights, ...rightWeights].forEach(w => w.draw(ctx));
}


function drawTitle() {
  const fontSize = min/24;
  let text = document.title;
  if(stage>1){
    text = stage;
  }
  ctx.fillStyle = 'black';
  if(isClear){
    text = 'クリア!';
    ctx.fillStyle = 'lightgreen';
  }
  ctx.font = fontSize+'px sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'top';
  ctx.fillText(text, cx, h-fontSize*1.2);
}


function setStage(){
  isClear = false;
  changeCount = 0;
  initBalancedWeights();
  swapUntilUnbalanced();
  drawAll();
}
setStage();

function drawAll() {
  ctx.fillStyle = 'white';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  angle = getAngleFromWeights();
  drawScale(angle);
  drawBox();
  drawTitle();
  drawWeights();
}




canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect();
  const mx = e.clientX - rect.left;
  const my = e.clientY - rect.top;

  for (const w of [...leftWeights, ...rightWeights]) {
    // ヒット判定だけは座標(クリック位置と重りの中心)
    this.size = min / 12;
    if (Math.abs(mx - w.x) < size/2 && Math.abs(my - w.y) < size/2) {

      changeCount++;
      // どのリストに属しているかで左右判定
      if (leftWeights.includes(w)) {
        leftWeights.splice(leftWeights.indexOf(w), 1);
        rightWeights.push(w);
        arrangeWeights(rightWeights, false);
      } else if (rightWeights.includes(w)) {
        rightWeights.splice(rightWeights.indexOf(w), 1);
        leftWeights.push(w);
        arrangeWeights(leftWeights, true);
      }

      // === 釣り合ったらクリア判定 ===
      if (isBalanced()) {
        stage++;
        isClear = true;
        setTimeout(() => {
          setStage(); // 次のステージへ(初期化)
        }, 1999);
      }

      drawAll();
      return;
    }
  }

});

CSS

HTML

ページのソースを表示 : Ctrl+U , DevTools : F12

view-source:https://hi0a.com/game/weight-balance/

ABOUT

hi0a.com 「ひまあそび」は無料で遊べるミニゲームや便利ツールを公開しています。

プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。

必要な機能の関数をコピペ利用したり勉強に活用できます。

プログラムの動作サンプル結果は画面上部に出力表示されています。

環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。

画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。

仕様変更、API廃止等、実験途中で動かないページもあります。