view source

JavaScript

document.title = 'ダイアモンドゲーム(チャイニーズチェッカー)';
/*
https://ja.wikipedia.org/wiki/%E3%83%80%E3%82%A4%E3%83%A4%E3%83%A2%E3%83%B3%E3%83%89%E3%82%B2%E3%83%BC%E3%83%A0
子駒は相手、味方関係なく線に沿っていれば、子駒1つ分だけ跳び越えられる。また、跳び越えた後に子駒が1つ分空いていれば、連続して跳び越えられ
*/

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 480;
canvas.height = 480;
document.getElementById('demo').appendChild(canvas);
canvas.classList.add('square');

class Piece {
  constructor(player, isKing = false) {
    this.player = player;
    this.king = isKing;
  }

  isKing() {
    return this.king;
  }

  isEnemy(other) {
    return other && this.player !== other.player;
  }
}

function pointInTriangle(px, py, ax, ay, bx, by, cx, cy) {
  // バリツェントリック座標で三角形内にあるか判定
  const v0x = cx - ax, v0y = cy - ay;
  const v1x = bx - ax, v1y = by - ay;
  const v2x = px - ax, v2y = py - ay;

  const dot00 = v0x * v0x + v0y * v0y;
  const dot01 = v0x * v1x + v0y * v1y;
  const dot02 = v0x * v2x + v0y * v2y;
  const dot11 = v1x * v1x + v1y * v1y;
  const dot12 = v1x * v2x + v1y * v2y;

  const denom = dot00 * dot11 - dot01 * dot01;
  if (denom === 0) return false;

  const u = (dot11 * dot02 - dot01 * dot12) / denom;
  const v = (dot00 * dot12 - dot01 * dot02) / denom;

  return u >= 0 && v >= 0 && (u + v <= 1);
}


class TrueChineseCheckers {

  constructor() {
    this.radius = 4;
    this.points = this.generatePoints();
    this.pieces = new Map();
    this.selected = null;
    this.validMoves = [];
    this.hovered = null;

    canvas.addEventListener('click', this.handleClick.bind(this));
    canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));

    this.spacing = 30;
    this.origin = { x: canvas.width / 2, y: canvas.height / 2 };
    this.goalZones = { 1: [], 2: [] };

    this.initPieces();
    this.draw();
  }

/*
  generatePoints() {
    const points = [];
    const R = this.radius;

    for (let row = -R * 2; row <= R * 2; row++) {
      const maxCols = R * 2 - Math.abs(row);
      for (let col = -maxCols; col <= maxCols; col += 2) {
        points.push({ col, row });
      }
    }
    return points;
  }
*/
  generatePoints() {
    const points = [];
    const R = 4; // 半径(= 中心から頂点までの段数)

    for (let row = -R * 2; row <= R * 2; row++) {
      const absRow = Math.abs(row);
      const maxCols = R * 2 - absRow;

      for (let col = -maxCols; col <= maxCols; col += 2) {
        // 六芒星条件:外周3角の外は除外
        if (Math.abs(col) + Math.abs(row) <= R * 4) {
          points.push({ col, row });
        }
      }
    }
    console.log(points);
  for (let row = -4; row <= -1; row++) {
      let startCol = 6 + (row + 4);   // row = -4 → 6, row = -3 → 7, etc.
      let endCol = 12 - (row + 4);    // row = -4 → 12, row = -3 → 11, etc.

      for (let col = startCol; col <= endCol; col += 2) {
          points.push({ col, row });
      }
  }
  for (let row = 1; row <= 4; row++) {
      let startCol = 9 - (row - 1);
      let endCol = 9 + (row - 1);

      for (let col = startCol; col <= endCol; col += 2) {
          points.push({ col, row });
      }
  }
  // 上向き三角形(右側)
  for (let row = -4; row <= -1; row++) {
      let startCol = 6 + (row + 4);
      let endCol = 12 - (row + 4);

      for (let col = startCol; col <= endCol; col += 2) {
          points.push({ col, row });       // 右側
          points.push({ col: -col, row }); // 左側(反転)
      }
  }

  // 下向き三角形(右側)
  for (let row = 1; row <= 4; row++) {
      let startCol = 9 - (row - 1);
      let endCol = 9 + (row - 1);

      for (let col = startCol; col <= endCol; col += 2) {
          points.push({ col, row });       // 右側
          points.push({ col: -col, row }); // 左側(反転)
      }
  }

    return points;
  }


  initPieces() {
    const points = this.points;
    const goalZones = this.goalZones;

    // 赤のゴール三角形(A-B-C)
    const redGoalA = { col: 0, row: 8 };
    const redGoalB = { col: -3, row: 5 };
    const redGoalC = { col: 3, row: 5 };

    // 青のゴール三角形(D-E-F)
    const blueGoalA = { col: 12, row: -4 };
    const blueGoalB = { col: 6, row: -4 };
    const blueGoalC = { col: 9, row: -1 };

    const A = { col: -12, row: 4 };
    const B = { col: -9, row: 1 };
    const C = { col: -6, row: 4 };

    for (let { col, row } of points) {
      const key = `${col},${row}`;

      // 🔴 赤プレイヤーの初期配置
      if (row <= -5 && Math.abs(col) <= row + 9) {
        this.pieces.set(key, new Piece(1));
      }

      // 🔵 青プレイヤーの初期配置(三角形ABC)
      if (pointInTriangle(col, row, A.col, A.row, B.col, B.row, C.col, C.row)) {
        this.pieces.set(key, new Piece(2));
      }

      // 🔴 赤の目標(赤が向かう)= ゴールゾーン1
      if (pointInTriangle(col, row, redGoalA.col, redGoalA.row, redGoalB.col, redGoalB.row, redGoalC.col, redGoalC.row)) {
        goalZones[1].push({ col, row });
      }

      // 🔵 青の目標(青が向かう)= ゴールゾーン2(修正後)
      if (pointInTriangle(col, row, blueGoalA.col, blueGoalA.row, blueGoalB.col, blueGoalB.row, blueGoalC.col, blueGoalC.row)) {
        goalZones[2].push({ col, row });
      }

      // 👑 King配置(例)
      if (row === -8 && col === 0) {
        this.pieces.set(key, new Piece(1, true));
      }
      if (col === A.col && row === A.row) {
        this.pieces.set(key, new Piece(2, true));
      }
    }
  }




  handleMouseMove(e) {
    const { x: mx, y: my } = getCorrectedCoords(e);
    this.hovered = null;

    for (let pt of this.points) {
      const { x, y } = this.toCanvasCoords(pt.col, pt.row);
      if (Math.hypot(mx - x, my - y) < 12) {
        this.hovered = pt;
        break;
      }
    }

    this.draw();
  }

  handleClick(e) {
    const { x, y } = getCorrectedCoords(e);
    const clicked = this.getClickedPoint(x, y);
    if (!clicked) return;

    const key = `${clicked.col},${clicked.row}`;
    const piece = this.pieces.get(key);

    if (this.selected && this.validMoves.some(p => p.col === clicked.col && p.row === clicked.row)) {
      const selKey = `${this.selected.col},${this.selected.row}`;
      const movingPiece = this.pieces.get(selKey);
      this.pieces.delete(selKey);
      this.pieces.set(key, movingPiece);
      this.selected = null;
      this.validMoves = [];
    } else if (piece) {
      this.selected = clicked;
      this.validMoves = this.getValidMoves(clicked);
    } else {
      this.selected = null;
      this.validMoves = [];
    }

    this.draw();
  }


  getClickedPoint(x, y) {
    for (let pt of this.points) {
      const { x: px, y: py } = this.toCanvasCoords(pt.col, pt.row);
      const dist = Math.hypot(x - px, y - py);
      if (dist < 15) return pt;
    }
    return null;
  }


  getValidMoves(start) {
    const moves = [];
    const piece = this.pieces.get(`${start.col},${start.row}`);
    const isKing = piece?.isKing?.(); // 安全に呼び出す
    const directions = [
      [2, 0],   // → col方向に1マス
      [-2, 0],  // ← col方向に1マス
      [1, 0], [-1, 0],
      [1, 1], [-1, -1],
      [-1, 1], [1, -1]
    ];

    // ✅ 単歩(±1方向だけ、隣接マス)
    for (let [dx, dy] of directions) {
      const to = { col: start.col + dx, row: start.row + dy };
      if (
        this.hasPoint(to) &&
        !this.hasPiece(to)
      ) {
        moves.push(to); // 単歩として追加
      }
    }

    const canContinueJump = (from) => {
      for (let [dx, dy] of directions) {
        let mid = { col: from.col + dx, row: from.row + dy };
        let dest = { col: mid.col + dx, row: mid.row + dy };

        if (
          this.hasPiece(mid) &&
          this.hasPoint(dest) &&
          !this.hasPiece(dest)
        ) {
          return true;
        }
      }
      return false;
    };


    // ✅ ジャンプ(±2方向で中間に駒があるとき)
    const visited = new Set();
    const jumpMoves = [];

    const dfs = (pos) => {
      for (let [dx, dy] of directions) {
        let mid = { col: pos.col + dx, row: pos.row + dy };
        let dest = { col: mid.col + dx, row: mid.row + dy };
        let jumped = 0;

        while (this.hasPiece(mid)) {
          const midPiece = this.pieces.get(`${mid.col},${mid.row}`);
          if (midPiece.isKing()) break; // 👑 Kingは飛び越え不可

          jumped++;

          if (!this.hasPoint(dest)) break;

          if (!this.hasPiece(dest)) {
            const destKey = `${dest.col},${dest.row}`;
            if (!visited.has(destKey)) {
              visited.add(destKey);
              jumpMoves.push({ col: dest.col, row: dest.row });
              dfs(dest); // 👑 Kingでも普通駒でも再帰続行OK
            }
            break;
          }

          // 👑 ここがポイント:Kingのみ、さらに先へ直進して次を試す
          if (!this.pieces.get(`${dest.col},${dest.row}`)) break;

          mid = dest;
          dest = { col: dest.col + dx, row: dest.row + dy };

          // ⚠️ 通常駒は1回しか飛べない → while 抜ける
          if (!isKing) break;
        }
      }
    };




    dfs(start);
    return moves.concat(jumpMoves);
  }




  hasPoint(pos) {
    return this.points.some(p => p.col === pos.col && p.row === pos.row);
  }

  hasPiece(pos) {
    return this.pieces.has(`${pos.col},${pos.row}`);
  }

  draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    ctx.fillStyle = '#fff';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // 薄くゴールゾーンの背景色を描画(駒がない位置のみ)
    for (let player of [1, 2]) {
      const color = player === 1 ? 'rgba(255, 0, 0, 0.5)' : 'rgba(0, 0, 255, 0.5)';
      for (let pt of this.goalZones[player]) {
        const key = `${pt.col},${pt.row}`;
        if (!this.pieces.has(key)) {
          const { x, y } = this.toCanvasCoords(pt.col, pt.row);
          this.drawDot(x, y, color, 5+2);
        }
      }
    }

    for (let pt of this.points) {
      const { x, y } = this.toCanvasCoords(pt.col, pt.row);
      this.drawDot(x, y, '#ccc');

      // ハイライト表示
      if (this.validMoves.some(p => p.col === pt.col && p.row === pt.row)) {
        this.drawDot(x, y, 'lime');
      }

      const key = `${pt.col},${pt.row}`;
      const piece = this.pieces.get(key);

      const isHovered = this.hovered && this.hovered.col === pt.col && this.hovered.row === pt.row;

      if (piece) {
        this.drawPiece(x, y, piece, isHovered);
      }

      if (this.hovered && pt.col === this.hovered.col && pt.row === this.hovered.row) {
        this.drawTooltip(x, y, `col: ${pt.col}, row: ${pt.row}`);
      }
    }

  }

  toCanvasCoords(col, row) {
    const x = this.origin.x + col * this.spacing / 2;
    const y = this.origin.y + row * this.spacing * Math.sin(Math.PI / 3);
    return { x, y };
  }

  drawDot(x, y, color = '#ccc', size=5) {
    ctx.beginPath();
    ctx.arc(x, y, size, 0, 2 * Math.PI);
    ctx.fillStyle = color;
    ctx.fill();
  }

  drawPiece(x, y, piece, isHovered = false) {
    ctx.beginPath();
    ctx.arc(x, y, 12, 0, 2 * Math.PI);
    ctx.fillStyle = piece.player === 1 ? 'red' : 'blue';
    if (isHovered) {
      ctx.fillStyle = piece.player === 1 ? 'orange' : 'deepskyblue';
    } else {
      ctx.fillStyle = piece.player === 1 ? 'red' : 'blue';
    }
    ctx.fill();

    if (piece.isKing()) {
      ctx.strokeStyle = 'gold';
      ctx.lineWidth = 3;
      ctx.stroke();
    }
  }

  drawTooltip(x, y, text) {
    ctx.font = '12px sans-serif';
    const padding = 4;
    const width = ctx.measureText(text).width + padding * 2;
    const height = 18;

    x = 40;
    y = 40;
    ctx.fillStyle = '#fff';
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 1;
    //ctx.fillRect(x + 10, y - 30, width, height);
    //ctx.strokeRect(x + 10, y - 30, width, height);

    ctx.fillStyle = '#000';
    ctx.fillText(text, x + 12, y - 17);
  }
}

function getCorrectedCoords(e) {
  // CSSで適用されているtransform(scaleなど)を考慮
  const rect = canvas.getBoundingClientRect();//DOM挿入が先
  const scaleX = canvas.width / rect.width;
  const scaleY = canvas.height / rect.height;
  let clientX, clientY;
  // マウスイベントまたはタッチイベントに対応
  if (e.touches) {
    clientX = e.touches[0].clientX;
    clientY = e.touches[0].clientY;
  } else {
    clientX = e.clientX;
    clientY = e.clientY;
  }
  // スケーリング補正された座標を返す
  return {
    x: (clientX - rect.left) * scaleX,
    y: (clientY - rect.top) * scaleY
  };
}


window.onload = () => {
  new TrueChineseCheckers();
};

CSS

HTML

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

view-source:https://hi0a.com/demo/-js/js-game-diamond/

ABOUT

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

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

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

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

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

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

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