道:
x1
x1
街:
x1
x1
x1
x1
都市:
x2
x3
発展:
x1
x1
x1
カタン 遊び方
view source
JavaScript
document.title = 'カタソ 開拓者たちのボードゲーム';
//正直六角マップの道の扱いとか対戦AIがめんどいので放置するかも
/*
カタン
遊び方&用語集
https://catan.jp/guide/
https://ja.wikipedia.org/wiki/%E3%82%AB%E3%82%BF%E3%83%B3%E3%81%AE%E9%96%8B%E6%8B%93%E8%80%85%E3%81%9F%E3%81%A1
カタソ Tkm Online
https://tkmax.github.io/tkmonline/
サーバー動かなくて悲しい
https://mitdok.github.io/tkmonline/
まだ生きているうちはオンラインで
*/
// #demo の中にキャンバスを作成する
const demoElement = document.getElementById('demo');
const canvas = document.createElement('canvas');
demoElement.appendChild(canvas);
// キャンバスの設定
canvas.width = 720; // キャンバスの幅
canvas.height = 720; // キャンバスの高さ
const ctx = canvas.getContext('2d');
const rect = canvas.getBoundingClientRect();
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const au = new Audio;
// 六角形の描画に必要な定数
const hexRadius = 60; // 六角形の半径
const hexWidth = Math.sqrt(3) * hexRadius; // 六角形の幅
const hexHeight = 2 * hexRadius; // 六角形の高さ
const hexSideL = hexRadius * Math.cos(Math.PI / 2);
let isDebug = false;
//ゲーム全体の変数
let diceNum = 0;
let gamePhase = "initial"; // "initial" or "main"
let initialPlacementStep = 0;
const initialPlacementOrder = []; // 生成は下記で
let initialPendingTown = null; // プレイヤーが拠点を置いたら保持
let robberMoving = false; // 盗賊を移動中かどうか
let currentRobberMover = null; // 盗賊を動かす権利があるプレイヤー
const cardDeck = [];// 共通のカード山札(カード全体)
let chatMode = false;
// 貿易の流れを管理するための tradeState
let tradeState = {
step: "selectGive", // 出す資源を選ぶ段階
give: null, // 出す資源
want: null, // 受け取りたい資源
};
//プレイヤーの手番
let currentPlayerIndex = 0;
// カタンのルールに従った固有情報
// マップのtipデータ島
let mapTips = [
[0, 1, 1, 1, 0],
[0, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 1, 1, 1, 1],
[0, 1, 1, 1, 0],
];
// 道:隣接タイルの方向と辺の対応(偶数行オフセット)
const directionOffsets = [
{ dx: 0, dy: -1, edge: 0, opposite: 3 }, // 上
{ dx: 1, dy: 0, edge: 1, opposite: 4 }, // 右上(偶数行) or 右下(奇数行)
{ dx: 1, dy: 1, edge: 2, opposite: 5 },
{ dx: 0, dy: 1, edge: 3, opposite: 0 }, // 下
{ dx: -1, dy: 1, edge: 4, opposite: 1 },
{ dx: -1, dy: 0, edge: 5, opposite: 2 },
];
// 番号のリスト(7を含む)
let uniNumbers = [2, 3, 4, 5, 6, 8, 9, 10, 11, 12];
let numbers = [...uniNumbers, ...uniNumbers, 7]; // 番号を2倍にして7も追加
// 地形情報
// 地形ごとの色とラベル設定
const terrainTypes = {
"soil": {
color: '#441100',
label: "土",
c:4,
talon:19,
},
"wood": {
color: '#006600',
label: "木",
c:4,
talon:19,
},
"wheat": {
color: '#FFD700',
label: "麦",
c:4,
talon:19,
},
"iron": {
color: '#A9A9A9',
label: "鉄",
c:3,
talon:19,
},
"sheep": {
color: '#BBCC00',
label: "羊",
c:4,
talon:19,
},
"desert": {
color: '#333333',
label: "砂漠",
c:0,
talon:0,
},
};
/*
カタンの公式ルール上の流れ
1. 🎲 サイコロを振る(全員に資源が配られる)
2. 🔁 プレイヤーの手番(好きな順で自由に):
・建設(街・都市・道)
・交渉(他プレイヤーとの交渉)
・貿易(4:1 or 港 3:1/2:1)
・発展カードの使用
3. ✅ 手番を終了する(次の人にバトンタッチ)
*/
const buildTypes = {
town:{
label:"街",
cost:{
"soil":1,
"wood":1,
"wheat":1,
"sheep":1,
},
},
city:{
label:"都市",
cost:{
"wheat":2,
"iron":3,
},
},
load:{
label:"道",
cost:{
"soil":1,
"wood":1,
},
},
}
const playerTypes = {
0:{color:'#ff0000'},//赤
1:{color:'#0000ff'},//青
2:{color:'#cc44ff'},//紫
3:{color:'#00ffff'},//水
};
let hexs = [];
let players = [];
let actionTypes = {
dice:{
label:'ダイス',
click:()=>{
const player = getCurrentPlayer();
updateResourcesWithDice(player);
changeButtonsStates('.action button', false);
changeButtonsStates('#diceBtn', true);
},
},
end:{
label:'終了',
click:()=>{
endTurn();
},
},
development:{
label:"発展",
cost:{
"wheat":1,
"iron":1,
"sheep":1,
},
},
trade:{
label:'貿易',
click:()=>{
//
},
},
nego:{
label:'交渉',
click:()=>{
logA('※交渉機能は未実装です 港貿易を利用してください');
//
},
},
};
const tradeTypes = {
bank:{
label:'銀行(4:1)',
rate: 4,
},
port:{
label:'一般港(3:1)',
rate: 3,
},
shop:{
label:'専門港(2:1)',
rate: 2,
},
}
const portTypes = {
any: { label: "一般港", rate: 3 }, // 3:1
soil: { label: "土の港", rate: 2 },
wood: { label: "木の港", rate: 2 },
wheat: { label: "麦の港", rate: 2 },
iron: { label: "鉄の港", rate: 2 },
sheep: { label: "羊の港", rate: 2 },
};
//港の位置
//左下のedgeが0で時計回り0-5
const portLocations = [
{ x: 2, y: 0, edge: 2, type: 'any' },
{ x: 1, y: 4, edge: 1, type: 'any' },
{ x: 4, y: 2, edge: 4, type: 'any' },
{ x: 1, y: 1, edge: 2, type: 'soil' },
{ x: 2, y: 4, edge: 5, type: 'wood' },
{ x: 0, y: 2, edge: 0, type: 'wheat' },
{ x: 4, y: 1, edge: 3, type: 'iron' },
{ x: 3, y: 4, edge: 4, type: 'sheep' },
];
//player.resources[resource] >= tradeTypes[type].rate;
const chatTypes = {
y:{
label:'はい',
click:()=>{
},
},
n:{
label:'いいえ',
click:()=>{
},
},
want:{
label:'求:',
click:()=>{
},
},
give:{
label:'出:',
click:()=>{
},
},
};
const cardTypes = {
knight:{
label:"騎士",
talon:14,
letter:"K",
color:'#444',
click: () => {
// 騎士カードがクリックされたときに呼ばれる処理
const player = getCurrentPlayer();
useKnightCard(player); // 騎士カードを使用する関数を呼び出す
},
},
road:{
label:"道路",
talon:2,
letter:"R",
color:'#944',
click: () => {
// 道路カードがクリックされたときの処理(例: 道を建設)
const player = getCurrentPlayer();
buildRoad(player); // 道を建設する関数を呼び出す
},
},
harvest:{
label:"収穫",
talon:2,
letter:"H",
color:'#994',
click: () => {
// 収穫カードがクリックされたときの処理
const player = getCurrentPlayer();
harvestResources(player); // 資源を収穫する関数を呼び出す
},
},
monopoly:{
label:"独占",
talon:2,
letter:"M",
color:'#909',
click: () => {
// 独占カードがクリックされたときの処理
const player = getCurrentPlayer();
monopolyEffect(player); // 独占効果を発動する関数を呼び出す
},
},
victory:{
label:"得点",
talon:5,
letter:"P",
color:'#339',
display:'none',
click: () => {
// 得点カードがクリックされたときの処理
const player = getCurrentPlayer();
gainVictoryPoint(player); // 得点を得る関数を呼び出す
},
},
};
const numTypes = {
0:{
label:"0",
},
1:{
label:"1",
},
2:{
label:"2",
},
3:{
label:"3",
},
4:{
label:"4",
},
5:{
label:"5",
},
};
/*
################################################################
################################################################
*/
const gameBoard = document.createElement('div');
gameBoard.id = 'gameBoard';
demoElement.appendChild(gameBoard);
const turnLabel = document.createElement('div');
turnLabel.id = 'turnLabel';
gameBoard.appendChild(turnLabel);
const decks = document.createElement('div');
decks.id = 'decks';
gameBoard.appendChild(decks);
function setBtns(types, name='div'){
const div = document.createElement('div');
div.classList.add('btns', name)
gameBoard.appendChild(div);
for (let type in types) {
let b = types[type];
const btn = document.createElement('button');
btn.id = type+'Btn';
btn.textContent = b.label;
if(b.display){btn.style.display=b.display;}
btn.addEventListener('click', function () {
if(b.click){
b.click()
} else {
inputA(b.label);
}
});
div.appendChild(btn);
}
}
function updateButtonStates() {
const isInteractive = chatMode || tradeState;
const allButtons = gameBoard.querySelectorAll('button');
allButtons.forEach(btn => {
const hasClick = btn.click || btn.getAttribute('data-has-click') === 'true';
// clickを持たないボタンは一律で無効化(chatMode/tradeState時のみ有効)
if (!hasClick) {
btn.disabled = !isInteractive;
}
});
}
function changeButtonsStates(selector, disabled=true) {
//selector:all:'button', '.action button'...
const btns = gameBoard.querySelectorAll(selector);
btns.forEach(btn => {
btn.disabled = disabled;
});
}
function disableAllButton() {
changeButtonsStates('button', true);
changeButtonsStates('input[type="number"]', true);
}
setBtns(actionTypes, 'action');
setBtns(cardTypes, 'cards');
//setBtns(buildTypes, 'build');
//setBtns(tradeTypes, 'trade');
//setBtns(terrainTypes, 'terrain');
//setBtns(chatTypes, 'chat');
//setBtns(numTypes, 'num');
updateButtonStates();
const tradePanel = document.createElement('div');
tradePanel.id = 'tradePanel';
tradePanel.style.display = 'block';
gameBoard.appendChild(tradePanel);
const inputs = document.createElement('div');
tradePanel.appendChild(inputs);
inputs.classList.add('inputs');
const label = document.createElement('label');
const span1 = document.createElement('span');
span1.textContent = `貿易`;
const span2 = document.createElement('span');
span2.textContent = `出`;
const span3 = document.createElement('span');
span3.textContent = `求`;
const span4 = document.createElement('span');
span4.textContent = `レート`;
label.appendChild(span1);
label.appendChild(span2);
label.appendChild(span3);
label.appendChild(span4);
inputs.appendChild(label);
// 出す資源・求める資源の入力フィールドを作成
const inputFields = {}; // 資源ごとの入力フィールドを保存
for (let type in terrainTypes) {
const b = terrainTypes[type];
if(b.c === 0){continue;}
//const inputWrap = document.createElement('div');
//inputs.appendChild(inputWrap);
// 出す資源と求める資源の入力フィールドを生成
const label = document.createElement('label');
const span = document.createElement('span');
span.textContent = `${b.label}:`;
label.appendChild(span);
const i = document.createElement('i');
i.style.backgroundColor = b.color;
i.classList.add('resource', type);
span.appendChild(i);
// 出す資源フィールド
const outputInput = document.createElement('input');
outputInput.type = 'number';
outputInput.min = '0';
outputInput.value = '0'; // 初期値は0
inputFields[`output_${type}`] = outputInput;
label.appendChild(outputInput);
inputs.appendChild(label);
// 求める資源フィールド
const inputInput = document.createElement('input');
inputInput.type = 'number';
inputInput.min = '0';
inputInput.value = '0'; // 初期値は0
inputFields[`input_${type}`] = inputInput;
label.appendChild(inputInput);
inputs.appendChild(label);
inputInput.addEventListener('change', ()=>{
tradeY.disabled = false;
tradeN.disabled = false;
});
// 交換レートフィールド
const rateInput = document.createElement('input');
rateInput.type = 'number';
rateInput.name = 'rate';
rateInput.readOnly = true;
rateInput.min = '0';
rateInput.value = '0'; // 初期値は0
inputFields[`rate_${type}`] = rateInput;
label.appendChild(rateInput);
inputs.appendChild(label);
}
const tradeY = document.createElement('button');
tradeY.textContent = '決定';
tradePanel.appendChild(tradeY);
const tradeN = document.createElement('button');
tradeN.textContent = '取消';
tradePanel.appendChild(tradeN);
// 決定ボタンの処理
tradeY.addEventListener('click', () => {
// 出す資源と求める資源を処理
const tradeData = {};
for (let type in terrainTypes) {
const b = terrainTypes[type];
if(b.c === 0){continue;}
let outputAmount = 0;
let inputAmount = 0;
// 存在するかどうかをチェック
if (inputFields[`output_${type}`]) {
outputAmount = parseInt(inputFields[`output_${type}`].value, 10);
inputAmount = parseInt(inputFields[`input_${type}`].value, 10);
console.log(outputAmount);
}
if (outputAmount > 0 || inputAmount > 0) {
tradeData[type] = { output: outputAmount, input: inputAmount };
}
}
if (Object.keys(tradeData).length > 0) {
console.log("貿易決定:", tradeData); // 貿易内容の確認(デバッグ用)
console.log(inputFields);
trade(tradeData);
// ここで貿易処理を呼び出す
} else {
console.log("貿易内容が選択されていません");
}
});
// CSS を動的に生成する関数
function generateTerrainCSS() {
let cssContent = '';
// terrainTypes に基づいて CSS を生成
for (const terrain in terrainTypes) {
const color = terrainTypes[terrain].color;
cssContent += `
.${terrain} {
background-color: ${color};
}
`;
}
return cssContent;
}
// CSS を <style> タグとして <head> に埋め込む
function insertCSS() {
const cssContent = generateTerrainCSS();
// <style> タグを作成
const styleTag = document.createElement('style');
styleTag.type = 'text/css';
styleTag.innerHTML = cssContent;
// <head> に <style> タグを追加
document.head.appendChild(styleTag);
}
// ページ読み込み時にスタイルを埋め込む
insertCSS();
// テキストエリアを作成
const area = document.createElement('div');
area.id = 'area';
demoElement.appendChild(area);
const logArea = document.createElement('textarea');
area.appendChild(logArea);
//const chatArea = document.createElement('textarea');
//area.appendChild(chatArea);
const inputArea = document.createElement('input');
area.appendChild(inputArea);
function getCorrectedCoords(e) {
// CSSで適用されているtransform(scaleなど)を考慮
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
};
}
// マウス移動イベントでhover対象を更新
canvas.addEventListener('mousemove', (e) => {
const {x,y} = getCorrectedCoords(e)
hoveredHex = getHexUnderMouse(x, y);
hoveredVertex = getVertexUnderMouse(x, y); //頂点
hoveredEdge = getEdgeUnderMouse(x, y); //辺
drawWithHover();
});
canvas.addEventListener('click', (e) => {
const { x, y } = getCorrectedCoords(e);
if (gamePhase === "initial") {//初期配置ターン
handleInitialPlacementClick(x, y);
return;
}
// 盗賊を移動できる状態
if (currentRobberMover) {
const hex = getHexUnderMouse(x, y);
if (hex && !hex.hasRobber) {
moveRobber(hex, currentRobberMover);
currentRobberMover = null;
drawWithHover();
return;
}
}
const player = getCurrentPlayer();
const vertex = getVertexUnderMouse(x, y);
if (vertex) {
handleVertexClick(vertex, player);
drawWithHover();
return;
}
const edge = getEdgeUnderMouse(x, y);
if (edge) {
handleEdgeClick(edge, player);
drawWithHover();
return;
}
});
// マウス位置からHexを取得
function getHexUnderMouse(mx, my) {
for (const hex of hexs) {
const dx = mx - hex.x;
const dy = my - hex.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < hexRadius) {
return hex;
}
}
return null;
}
//初期配置 拠点(街)x2建設の処理
/*
ダイスなどのUI gamePhase !== 'main' の間は無効化 or 非表示が安心
道の接続判定 edge.connects(vertex) でチェックして安全に(距離制限いらない)
初期資源配布 逆順(2つ目の街)設置後に checkDice() 的な資源配布入れてもOK
*/
// プレイヤーが建設した「拠点」:街 or 都市(type: 1 or 2)
// type: 1 = 街(初期), type: 2 = 都市(アップグレード)
function logBuild(type, player) {
const label = type === 1 ? "街" : type === 2 ? "都市" : "拠点";
logA(`${player.name} が拠点を建設しました(${label})`);
}
function handleInitialPlacementClick(x, y) {
const playerIndex = initialPlacementOrder[initialPlacementStep];
const player = players[playerIndex];
const vertex = getVertexUnderMouse(x, y);
const edge = getEdgeUnderMouse(x, y);
// ① 街を置こうとしているとき
if (vertex && !vertex.isOwned()) {
if (initialPendingTown) {
logA("❌ 拠点を建設した後は、道を建設してください");
return;
}
if (isVertexNearSettlement(vertex)) {
logA("❌ 隣接に他の拠点があるため、街を建てられません");
return;
}
vertex.setOwner(player, 1);
player.hasHex.push({ hex: null, type: 1 });
initialPendingTown = vertex;
logA(`${player.name} が拠点を建設しました(街)`);
drawWithHover();
return;
}
// ② 道を置こうとしているとき
if (edge) {
if (!initialPendingTown) {
logA("❌ まずは街を建設してください(初期配置)");
return;
}
if (!edge.connects(initialPendingTown)) {
logA("❌ 道は建設した拠点に隣接する必要があります(初期配置)");
return;
}
if (edge.owner) {
logA("❌ その道はすでに建設されています");
return;
}
edge.setOwner(player);
logA(`${player.name} が道を建設しました(初期配置)`);
initialPendingTown = null;
initialPlacementStep++;
if (initialPlacementStep >= initialPlacementOrder.length) {
gamePhase = "main";
currentPlayerIndex = 0;
updateTurnUI();
logA("初期配置が終了しました。");
logA("★★★★ゲーム開始!★★★★");
const au =new Audio('mp3/piro.mp3');
au.play();
initialResources()
updateTurnUI()
changeButtonsStates('#diceBtn', false);
} else {
showInitialPlacementTurn();//次の初期配置
}
drawWithHover();
return;
}
// ③ どちらでもなければ
logA("❌ 無効な場所です(初期配置)");
}
//建設系
function handleVertexClick(vertex, player) {
if (!vertex.isOwned()) {
if (isVertexNearSettlement(vertex)) {
logA("❌ 近すぎて拠点は建設できません");
return;
}
const cost = buildTypes.town.cost;
if (player.tryBuild(cost)) {
vertex.setOwner(player, 1);
logA(`${player.name} が街を建設`);
} else {
logA(`資源が足りません(街)`);
}
} else if (vertex.owner === player && vertex.type === 1) {
const cost = buildTypes.city.cost;
if (player.tryBuild(cost)) {
vertex.setOwner(player, 2);
logA(`${player.name} が都市を建設`);
} else {
logA(`資源が足りません(都市)`);
}
} else {
logA(`すでに拠点が建設されています`);
}
player.displayResources();
}
function handleEdgeClick(edge, player) {
if (!edge.owner) {
const cost = buildTypes.load.cost;
if (player.tryBuild(cost)) {
edge.setOwner(player);
player.updateLongestRoad();
logA(`${player.name} が道を建設`);
} else {
logA(`資源が足りません(道)`);
}
player.displayResources();
} else {
logA(`すでに道があります`);
}
}
function drawHoveredEdgeOverlay(edge) {
if (!edge || edge.owner) return; // 未所有のみ対象(好みに応じて)
edge.drawHighlight(); // path を使った描画に統一
}
let hoveredHex = null;
let hoveredEdge = null;
let hoveredVertex = null;
// 再描画(hover含む)
function drawWithHover() {
ctx.clearRect(0, 0, canvas.width, canvas.height); // クリア
draw(); // 通常描画
if (hoveredHex) hoveredHex.drawHighlight();
if (hoveredEdge) hoveredEdge.drawHighlight(); // ← Path2DのEdge
if (hoveredVertex) hoveredVertex.drawHighlight(); // ← Path2DのVertex
}
// コンソールログと同時にテキストエリアに追加する関数
function logA(message) {
console.log(message); // 通常のコンソール出力
logArea.value += message + '\n'; // テキストエリアに追加
logArea.scrollTop = logArea.scrollHeight;
}
function chatA(message) {
console.log(message); // 通常のコンソール出力
chatArea.value += message + '\n'; // テキストエリアに追加
chatArea.scrollTop = chatArea.scrollHeight;
}
function inputA(message) {
console.log(message); // 通常のコンソール出力
inputArea.value += message; // テキストエリアに追加
}
function playSound() {
const oscillator = audioContext.createOscillator();
oscillator.frequency.setValueAtTime(999, audioContext.currentTime);
oscillator.type = 'square';
const gainNode = audioContext.createGain();
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.05); // 0.05秒後に停止
}
/*
################################################################
################################################################
*/
// Playerクラスの定義
class Player {
constructor({i, name, isNPC}) {
this.index = i;
this.name = name || '----';
this.isNPC = isNPC || false;
this.point = 0;
this.color = playerTypes[this.index].color;
this.hasHex = []; //所有地Hex:1:街:2:都市
this.initDeck();
this.resources = {};
for (const terrain in terrainTypes) {
if (terrainTypes[terrain].c === 0) continue; // 砂漠など除外
this.resources[terrain] = 0;//資源の準備
}
this.cards = {}; // 手持ちカード(発展カードなど)
this.roadLengthMax = 0; // 最長道の長さを保存
this.knightCount = 0; // 騎士カード使用回数
console.log(this.name);
}
// 資源を追加するメソッド
addResource(terrain, amount = 1) {
if (this.resources.hasOwnProperty(terrain)) {
this.resources[terrain] += amount;
}
};
// 手持ちカードを追加
addCard(cardType) {
// カードがすでにある場合、枚数を追加する
if (this.cards[cardType]) {
this.cards[cardType]++;
} else {
this.cards[cardType] = 1;
}
}
// 資源の状態を表示
showResources() {
console.log(`${this.name}の資源:`, this.resources);
}
//建設
tryBuild(cost) {
if (this.canAfford(cost)) {
this.spendResources(cost);
return true;
}
return false;
}
//資源コストがあるか
canAfford(cost) {
for (const res in cost) {
if ((this.resources[res] || 0) < cost[res]) {
return false;
}
}
return true;
}
//資源コストの支払い
spendResources(cost) {
if (!this.canAfford(cost)) return false;
for (const res in cost) {
this.resources[res] -= cost[res];
}
return true;
}
getTotalPoint(){
this.point = 0;
// 街の数 × 1点
const townsCount = this.hasHex.filter(hex => hex.type === 1).length;
this.point += townsCount * 1;
// 都市の数 × 2点
const citiesCount = this.hasHex.filter(hex => hex.type === 2).length;
this.point += citiesCount * 2;
// 得点カードの数 × 1点
const victoryCardsCount = this.cards["victory"] || 0;
// 最長道ボーナスの判定
if (this.roadLengthMax >= 5 && this.roadLengthMax === getMaxRoadLength()) {
this.point += 2; // 最長道ボーナスを加算
console.log(`${this.name} が最長道ボーナス 2点を獲得!`);
}
// 騎士カード使用回数ボーナス
if (this.knightCount >= 3) {
// 全プレイヤーで最多の騎士カード使用回数を持つか確認
const maxKnightCount = Math.max(...players.map(player => player.knightCount));
if (this.knightCount === maxKnightCount) {
this.point += 2; // ボーナス点を加算
console.log(`${this.name} が最多の騎士使用回数でボーナス2点`);
}
}
this.point += victoryCardsCount * 1;
// 合計得点を返す
return this.point;
}
// 最長道の更新
updateLongestRoad(players) {
const longestRoad = findLongestRoad(this);
this.roadLengthMax = longestRoad.length; // 最長道の長さを保存
console.log(`${this.name} の最長道の長さ: ${this.roadLengthMax}`);
// 最長道ボーナスを更新
updateLongestRoad(this, players); // 最長道ボーナスを付与
}
initDeck() {
this.deck = document.createElement('div');
this.deck.classList.add('deck');
decks.appendChild(this.deck);
this.h3 = document.createElement('h3');
this.deck.appendChild(this.h3);
this.col = document.createElement('i');
this.col.textContent = '▲';
this.col.style.color = this.color;
this.h3.appendChild(this.col);
this.nameE = document.createElement('span');
this.nameE.textContent = this.name;
this.nameE.classList.add('name');
this.h3.appendChild(this.nameE);
this.pointE = document.createElement('span');
this.pointE.textContent = this.point;
this.pointE.classList.add('point');
this.h3.appendChild(this.pointE);
this.resourcePlace = document.createElement('div');
this.resourcePlace.classList.add('resourcePlace');
this.deck.appendChild(this.resourcePlace);
this.cardPlace = document.createElement('div');
this.cardPlace.classList.add('cardPlace');
this.deck.appendChild(this.cardPlace);
}
displayResources() {
this.resourcePlace.innerHTML = '';
for (const terrain in this.resources) {
for (let i = 0; i < this.resources[terrain]; i++) {
const resourceIcon = document.createElement('i');
resourceIcon.classList.add('resource', terrain); //
resourceIcon.style.backgroundColor = terrainTypes[terrain].color;
this.resourcePlace.appendChild(resourceIcon);
}
}
this.pointE.textContent = this.getTotalPoint();
}
displayCards() {
this.cardPlace.innerHTML = '';
for (const cardType in this.cards) {
for (let i = 0; i < this.cards[cardType]; i++) {
const cardIcon = document.createElement('i');
cardIcon.classList.add('card', cardType); //
cardIcon.style.backgroundColor = cardTypes[cardType].color;
this.cardPlace.appendChild(cardIcon);
}
}
}
}
//プレイヤーのターン関連
function getCurrentPlayer() {
return players[currentPlayerIndex];
}
function endTurn() {
// ゲーム終了条件のチェック
const player = getCurrentPlayer();
if (player.point >= 10) {
console.log(`${player.name} が ${player.point} 点で勝利!`);
endGame();
return;
}
changeButtonsStates('input[type="number"]', true);
changeButtonsStates('#diceBtn', true);
startTurn();
}
function startTurn() {
currentPlayerIndex = (currentPlayerIndex + 1) % players.length;
const player = getCurrentPlayer();
logA(`🎯 ${player.name} のターンです`);
const au =new Audio('mp3/ko.mp3');
au.play();
if (player.index === 0) {
const au = new Audio('mp3/your-turn.mp3'); //
au.play();
}
changeButtonsStates('.action button', true);
changeButtonsStates('#diceBtn', false);
logA(`ダイスをふってください`);
updateTurnUI();
if (player.isNPC) {
logA(`${player.name} が思考中...`);
setTimeout(() => npcTurn(player), Math.random() * 2000 + 1000); // 1〜3秒の遅延で開始
} else {
changeButtonsStates('.action button', true);
changeButtonsStates('#diceBtn', false);
logA(`ダイスをふってください`);
}
}
function endGame() {
logA('ゲーム終了!');
const au =new Audio('mp3/piro.mp3');
au.play();
}
function updateTurnUI() {
playSound();
let player = players[0];
if (gamePhase === "initial") {
const i = initialPlacementOrder[initialPlacementStep];
player = players[i];
turnLabel.textContent = `🛠 初期配置: ${player.name}`;
} else {
player = getCurrentPlayer();
turnLabel.textContent = `🎯 現在のターン: ${player.name}`;
}
if (player.isNPC && gamePhase === "initial") {
logA(`${player.name} が思考中...`);
setTimeout(() => {
placeNPCInitialSetup(player);
}, Math.random() * 4000 + 1000); // 1~5秒の間でランダム
}
const i = player.index;
const eDecks = gameBoard.querySelectorAll('.deck');
eDecks.forEach(deck => {
deck.classList.remove('thisTurn');
});
eDecks[i].classList.add('thisTurn');
}
/*
################################################################
################################################################
*/
// Hexクラスの定義
class Hex {
constructor(x, y, tip, number, terrain) {
this.x = x; // 六角形の中心X座標
this.y = y; // 六角形の中心Y座標
this.tip = tip; // 六角形の「tip」の値(例えば色を変えるため)
this.number = number; // 六角形の番号(2-6, 8-12)
this.terrain = terrain; // 六角形の地形(例えば "forest", "grain" など)
this.color = '#000'; //
this.vertex = [0,0,0,0,0,0];//頂点:拠点:上から時計回り
this.edges = [];//辺:道:右上から時計回り
this.isMatch = false;
this.hasRobber = false;//盗賊・資源なし状態
this.radius = hexRadius;
this.path = this.createPath(this.radius); // 通常描画用
this.hoverPath = this.createPath(this.radius * 0.8); // hover判定用は小さめ
//console.log(this.color);
if(terrainTypes[this.terrain]){
this.color = terrainTypes[this.terrain].color;
}
if(this.terrain === "desert") this.hasRobber = true;
}
createPath(radius) {
const path = new Path2D();
for (let i = 0; i < 6; i++) {
const angle = Math.PI / 3 * i + Math.PI / 2;
const dx = this.x + radius * Math.cos(angle);
const dy = this.y + radius * Math.sin(angle);
if (i === 0) path.moveTo(dx, dy);
else path.lineTo(dx, dy);
}
path.closePath();
return path;
}
containsPoint(mx, my) {
return ctx.isPointInPath(this.hoverPath, mx, my);
}
clearMatch(){
this.isMatch = false;
}
checkDice(d){
console.log([this.number,d]);
if (this.number === d) {// サイコロの目と一致
this.isMatch = true;
return true;
}
}
// 六角形を描くメソッド
draw() {
ctx.save();
ctx.fillStyle = this.color || '#777';
ctx.fill(this.path);
ctx.stroke(this.path);
// 番号背景
ctx.beginPath();
ctx.fillStyle = '#fff';
if(this.isMatch){
ctx.fillStyle = '#faa';
}
ctx.arc(this.x, this.y, hexRadius/3, 0, Math.PI * 2);
ctx.fill();
// 番号テキスト
ctx.font = '20px Arial';
ctx.fillStyle = '#000';
ctx.textAlign = 'center';
ctx.fillText(this.number, this.x, this.y + 8); // 番号を中央に配置
// 盗賊バツ
if (this.hasRobber) {
this.drawRobberMark();
}
ctx.restore();
}
drawHighlight() {
ctx.save();
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.fill(this.hoverPath);
ctx.restore();
}
drawRobberMark() {
const r = hexRadius / 3; // 数字円と同じ半径
const size = r * 0.8; // バツの長さ(少し内側)
const x = this.x;
const y = this.y;
const path = new Path2D();
path.moveTo(x - size, y - size);
path.lineTo(x + size, y + size);
path.moveTo(x + size, y - size);
path.lineTo(x - size, y + size);
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 4;
ctx.stroke(path);
}
}
/*
################################################################
################################################################
*/
const vertexMap = {};
// キー: 頂点座標, 値: { x, y, owner, type, hexes: [Hex, ...] }
class Vertex {
constructor(x, y) {
this.x = x;
this.y = y;
this.key = vertexKey(x, y); // "x,y" 形式のキーを保存
this.owner = null; // 所有プレイヤー(null:未所有)
this.type = null; // 拠点の種類(1:街, 2:都市)
this.hexes = []; // 関連するHex
this.path = this.createPath(); // ← pathを生成・保持
}
createPath() {
const r = hexRadius/4; // 頂点の見た目の半径
const path = new Path2D();
path.arc(this.x, this.y, r, 0, Math.PI * 2);
return path;
}
// 所有プレイヤーの設定
setOwner(player, type = 1) {
if (this.owner && this.type === 2) return false; // すでに都市なら無視
this.owner = player;
this.type = type;
//this.path = this.createPath(); // hover用path更新(形状は変わらなくても可)
return true;
}
//資源配布 Hexの資源を確認してプレイヤーに
provideResources(diceNumber) {
if (!this.owner) return;
this.hexes.forEach(hex => {
if (hex.number === diceNumber && !hex.hasRobber && hex.terrain !== "desert") {
const amount = this.type === 2 ? 2 : 1;
this.owner.addResource(hex.terrain, amount);
logA(`${this.owner.name} が ${terrainTypes[hex.terrain].label} を ${amount}枚 獲得`);
}
});
}
isOwned() {
return this.owner !== null;
}
containsPoint(mx, my) {
return ctx.isPointInPath(this.path, mx, my);
}
draw() {
if (this.type === 1) {
this.drawTown();
} else if (this.type === 2) {
this.drawCity();
} else {
this.drawCircle()
}
}
drawCircle() {
ctx.save();
ctx.fillStyle = this.owner ? this.owner.color : '#555';
ctx.fill(this.path);
ctx.restore();
}
drawTown() {
ctx.save();
ctx.fillStyle = this.owner ? this.owner.color : '#555';
ctx.strokeStyle = '#000';
ctx.lineWidth = 4;
ctx.translate(this.x, this.y);
const unit = hexRadius / 4;
const oY = hexRadius / 8;
const path = new Path2D();
// 土台(四角形)
const houseW = unit * 2;
const houseH = unit * 1.5;
// 屋根(三角形)をつなげる
// 三角形の屋根
path.moveTo(houseW / 2, -houseH / 2 + oY); // 右下
path.lineTo(0, -houseH / 2 - unit + oY); // 上
path.lineTo(-houseW / 2 , -houseH / 2 + oY); // 左下
// 四角形の土台(屋根の下部分)
path.lineTo(-houseW / 2, -houseH / 2 + oY); // 左上
path.lineTo(-houseW / 2, houseH / 2 + oY); // 左下
path.lineTo(houseW / 2, houseH / 2 + oY); // 右下
path.lineTo(houseW / 2, -houseH / 2 + oY); // 右上
path.closePath();
ctx.fill(path);
ctx.stroke(path);
ctx.restore();
}
drawCity() {
ctx.save();
ctx.fillStyle = this.owner ? this.owner.color : '#555';
ctx.strokeStyle = '#000';
ctx.lineWidth = 4;
ctx.translate(this.x, this.y);
const unit = hexRadius / 4;
const width = unit * 2;
const height = unit * 3;
const cx = 0;
const cy = 0;
// Path2Dで凸字形を作成(時計回り)
const path = new Path2D();
path.moveTo(cx + width*2/3, cy + height / 2); // 右下
path.lineTo(cx - width*2/3, cy + height / 2); // 左下
path.lineTo(cx - width*2/3, cy + 0); //
path.lineTo(cx - width*1/3, cy + 0); // へこみ左
path.lineTo(cx - width*1/3, cy - height/2); //
path.lineTo(cx + width*1/3, cy - height/2); //
path.lineTo(cx + width*1/3, cy +0); // へこみ右
path.lineTo(cx + width*2/3, cy +0); //
path.closePath();
ctx.fill(path);
ctx.stroke(path);
ctx.restore();
}
drawHighlight() {
ctx.save();
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.fill(this.path);
ctx.restore();
}
// 頂点に接する全Hexの地形を返す
getTerrains() {
return this.hexes.map(hex => hex.terrain);
}
addHex(hex) {
if (!this.hexes.includes(hex)) {
this.hexes.push(hex);
}
}
}
//頂点計算関数
function getHexVertexPosition(hex, cornerIndex) {
const angle = Math.PI / 3 * cornerIndex;
const x = hex.x + hexRadius * Math.cos(angle + Math.PI / 2);
const y = hex.y + hexRadius * Math.sin(angle + Math.PI / 2);
return { x, y };
}
//頂点一意化
function vertexKey(x, y) {
return `${Math.round(x)},${Math.round(y)}`;
}
//一意キーの関数:
function sharedEdgeKey(v1, v2) {
const [a, b] = [v1, v2].sort((a, b) => {
if (a.x !== b.x) return a.x - b.x;
return a.y - b.y;
});
return `${Math.round(a.x)},${Math.round(a.y)}|${Math.round(b.x)},${Math.round(b.y)}`;
}
function setVertex() {
hexs.forEach(hex => {
for (let i = 0; i < 6; i++) {
const { x, y } = getHexVertexPosition(hex, i);
const key = vertexKey(x, y);
if (!vertexMap[key]) {
vertexMap[key] = new Vertex(x, y);
}
vertexMap[key].addHex(hex); // 所属Hexを登録
}
});
}
//近隣に拠点があれば建設不可
function isVertexNearSettlement(vertex) {
const neighbors = getAdjacentVerticesByEdge(vertex);
return neighbors.some(v => v.isOwned());
}
function getAdjacentVerticesByEdge(vertex) {
const adjacent = new Set();
if (!vertex.edges) return [];
vertex.edges.forEach(edge => {
const other = edge.other(vertex);
if (other) {
adjacent.add(other);
}
});
return [...adjacent];
}
//Edgeを生成するときに「すでにあるかチェック」
function makeSharedEdge(hex, i, from, to) {
const key = sharedEdgeKey(from, to);
if (!edgeMap[key]) {
const edge = new Edge(hex, i, from, to);
edgeMap[key] = edge;
// 双方向登録
from.edges = from.edges || [];
to.edges = to.edges || [];
from.edges.push(edge);
to.edges.push(edge);
}
return edgeMap[key];
}
function placeSettlement(player, x, y, type = 1) {
const key = vertexKey(x, y);
const vertex = vertexMap[key];
if (!vertex) {
console.warn("無効な頂点です");
return false;
}
return vertex.setOwner(player, type);
}
function drawSettlements() {
for (const key in vertexMap) {
const vertex = vertexMap[key];
vertex.draw(); // Vertexクラスのdraw()呼び出し
}
}
//Hexがもつ頂点を取得
function getVerticesOfHex(hex) {
const vertices = [];
for (let i = 0; i < 6; i++) {
const { x, y } = getHexVertexPosition(hex, i);
const key = vertexKey(x, y);
const vertex = vertexMap[key];
if (vertex) {
vertices.push({
index: i, // 頂点番号(0~5)
x: vertex.x,
y: vertex.y,
key: key,
data: vertex // owner, type, hexes など全部入り
});
}
}
return vertices; // 配列長6(六角形の全頂点)
}
/*
################################################################
################################################################
*/
//Edge:辺関連
const edgeMap = {}; // key = "x1,y1|x2,y2" の形
class Edge {
constructor(hex, index, fromVertex, toVertex) {
//this.x = x; // 描画中心用の位置(optional)
//this.y = y;
this.hex = hex; // 所属Hex
this.index = index; // 0〜5:辺のインデックス(時計回り)
this.from = fromVertex; // Vertexオブジェクト
this.to = toVertex;
//this.key = edgeKey(hex, index); // 一意なキー(hex座標+辺index)
this.owner = null; // 道の所有プレイヤー(nullなら未建設)
this.updateLinePosition();//辺の当たり判定設定
this.path = this.createPath2D(); // ← Path2D 生成
this.lineWidth =hexRadius/4;
}
connects(vertex) {
return this.from === vertex || this.to === vertex;
}
other(vertex) {
return this.from === vertex ? this.to : this.from;
}
isOwnedBy(player) {
return this.owner === player;
}
setOwner(player) {
if (this.owner) return false;
this.owner = player;
return true;
}
isOwned() {
return this.owner !== null;
}
//Edge にクリック判定メソッド
updateLinePosition() {
const angle = (Math.PI / 3) * this.index + Math.PI / 2;
this.startX = this.hex.x + hexRadius * 0.6 * Math.cos(angle);
this.startY = this.hex.y + hexRadius * 0.6 * Math.sin(angle);
this.endX = this.hex.x + hexRadius * 0.6 * Math.cos(angle + Math.PI / 3);
this.endY = this.hex.y + hexRadius * 0.6 * Math.sin(angle + Math.PI / 3);
}
containsPoint(mx, my) {
ctx.lineWidth = this.lineWidth;
return ctx.isPointInStroke(this.path, mx, my);
}
createPath2D() {
//(Hex中心からの角度じゃなく、2頂点の中間を使う)
const shrinkRatio = 0.6; // 0〜1。1 = 頂点ぴったり、0.8 = 両端10%ずつカット
// 頂点座標(fromVertex / toVertex)
const x1 = this.from.x;
const y1 = this.from.y;
const x2 = this.to.x;
const y2 = this.to.y;
// ベクトル差分
const dx = x2 - x1;
const dy = y2 - y1;
// 中心から縮めるための調整
const mx1 = x1 + dx * (1 - shrinkRatio) / 2;
const my1 = y1 + dy * (1 - shrinkRatio) / 2;
const mx2 = x2 - dx * (1 - shrinkRatio) / 2;
const my2 = y2 - dy * (1 - shrinkRatio) / 2;
const path = new Path2D();
path.moveTo(mx1, my1);
path.lineTo(mx2, my2);
return path;
//path自体にlineWidthはもたせられない
}
drawHighlight() {
ctx.save();
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
ctx.lineWidth = this.lineWidth;
ctx.stroke(this.path);
ctx.restore();
}
drawPort() {
if (!this.port) return;
const midX = (this.from.x + this.to.x) / 2;
const midY = (this.from.y + this.to.y) / 2;
// 六角形の中心から辺の中心へのベクトルを使って外側に押し出す
const dx = midX - this.hex.x;
const dy = midY - this.hex.y;
const len = Math.sqrt(dx * dx + dy * dy);
const offsetRatio = 1.2; // 長めに押し出して Hex の外に
const px = this.hex.x + dx / len * hexRadius * offsetRatio;
const py = this.hex.y + dy / len * hexRadius * offsetRatio;
const r = hexRadius / 4;
let terrain = terrainTypes[this.portType];
if(!terrain){
terrain = {
color:'#fff',
label:'?',
}
}
ctx.save();
ctx.beginPath();
ctx.arc(px, py, r, 0, Math.PI * 2);
ctx.fillStyle = terrain.color;
ctx.fill();
ctx.strokeStyle = '#000';
ctx.lineWidth = 8;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = `${r * 0.9}px sans-serif`;
ctx.textAlign = 'center';
// portLocations の type に基づいて、日本語ラベルを取得
let portLabel = terrain.label;
//ctx.fillText(portLabel, px, py + r * 0.3); // 日本語ラベルを表示
ctx.restore();
}
draw() {
ctx.save();
ctx.strokeStyle = this.owner ? this.owner.color : '#999';
ctx.lineWidth = this.lineWidth;
ctx.stroke(this.path);
ctx.restore();
}
}
//一意キー
function edgeKey(hex, index) {
return `${hex.x},${hex.y},${index}`;
}
//Edge の生成:Hexの初期化時に追加
//各Edgeを組み立てるときにVertexを渡す
function assignEdgesToHex(hex) {
hex.edges = [];
const vertices = getVerticesOfHex(hex); // index付きで返ってくる6頂点
for (let i = 0; i < 6; i++) {
const from = vertices[i].data;
const to = vertices[(i + 1) % 6].data;
//const edge = new Edge(hex, i, from, to);//ここではnew Hexしない
//edge.updateLinePosition();//辺の当たり判定設定
const edge = makeSharedEdge(hex, i, from, to);
hex.edges.push(edge);//異なるHexの同じEdgeが共有される
// 双方向にEdgeを登録(後の探索のため)
from.edges = from.edges || [];
to.edges = to.edges || [];
from.edges.push(edge);
to.edges.push(edge);
}
}
function getEdgesOfVertexInHex(hex, vertexIndex) {
// 頂点インデックスから隣接する2つの辺インデックスを取得
const edge1 = vertexIndex;
const edge2 = (vertexIndex + 1) % 6;
return [
{
edgeIndex: edge1,
hex: hex,
direction: 'local' // このHex上の辺
},
{
edgeIndex: edge2,
hex: hex,
direction: 'local'
}
];
}
// 頂点に接続しているすべての辺(最大3)を返す
function getEdgesAtVertex(vertexData) {
const edges = [];
vertexData.hexes.forEach(hex => {
const vertices = getVerticesOfHex(hex);
for (let v of vertices) {
if (v.key === vertexKey(vertexData.x, vertexData.y)) {
const connectedEdges = getEdgesOfVertexInHex(hex, v.index);
edges.push(...connectedEdges);
}
}
});
return edges;
}
//マウス当たり判定
function getHexUnderMouse(mx, my) {
for (const hex of hexs) {
if (hex.containsPoint(mx, my)) return hex;
}
return null;
}
function getVertexUnderMouse(mx, my) {
for (const key in vertexMap) {
const vertex = vertexMap[key];
if (vertex.containsPoint(mx, my)) {
return vertex;
}
}
return null;
}
function getEdgeUnderMouse(mx, my) {
for (const hex of hexs) {
for (const edge of hex.edges) {
if (edge.containsPoint(mx, my)) {
return edge;
}
}
}
return null;
}
// プレイヤーの道を引く処理(テスト用)
function buildRoad(player, hex, edgeIndex) {
const edge = hex.edges[edgeIndex];
if (edge.setOwner(player)) {
logA(`${player.name}が道を建設しました`);
} else {
logA(`その場所にはすでに道があります`);
}
draw();
}
/*
################################################################
################################################################
*/
// 地形ごとの配列を作成し、ランダムに配置
function setHexs() {
const rows = 5; // 行数
const cols = [5, 5, 5, 5, 5]; // 各行の列数(5列)
// 地形配列を作成
let terrains = [];
for (let terrain in terrainTypes) {
for (let i = 0; i < terrainTypes[terrain].c; i++) {
terrains.push(terrain); // 各地形をその枚数だけ配列に追加
}
}
// 配列をランダムにシャッフル
terrains = terrains.sort(() => Math.random() - 0.5); // シャッフル処理
// グリッドを中央に配置するためのオフセット計算
const totalWidth = Math.max(...cols) * hexWidth + (Math.max(...cols) - 1) * hexWidth / 2;
const offsetX = (canvas.width - totalWidth) / 2;
// 各行の配置
let currentY = hexRadius+hexRadius/2;
for (let row = 0; row < rows; row++) {
const numCols = cols[row];
let currentX = offsetX;
if (row % 2 !== 0) {
currentX -= hexWidth / 2; // 偶数行
}
for (let col = 0; col < numCols; col++) {
currentX += hexWidth + hexWidth / 2 - hexWidth / 2; // 次の六角形の位置
if(mapTips[row][col] === 0){ continue; }
// 番号をランダムに選択
const number = numbers.splice(Math.floor(Math.random() * numbers.length), 1)[0];
// 7の場合は砂漠確定
const terrain = number === 7 ? "desert" : terrains.pop();
const hex = new Hex(currentX, currentY, mapTips[row][col], number, terrain); // Hexオブジェクトを作成
hex.row = row;
hex.col = col;//港用の座標を渡す
hexs.push(hex);
}
// 次の行は縦方向にずれる
currentY += hexRadius * 1.5;
}
}
/*
################################################################
################################################################
*/
//港関連
//港を辺に紐づけておく(初期化で)
function assignPorts() {
portLocations.forEach(p => {
const hex = getHexAt(p.x, p.y);
if (!hex) return;
const edge = hex.edges[p.edge];
if (!edge) return;
edge.port = portTypes[p.type];
edge.portLocation = p.type;
});
}
//拠点(Vertex)が接する港を取得できるようにする
function getPortsForVertex(vertex) {
//const ports = [];
const ports = new Set();
if (!vertex.edges) return [];
vertex.edges.forEach(edge => {
if (edge.port) ports.add(edge.port);
});
return [...ports]; // arrayにして返す
}
//プレイヤーが港をもっているか判定
// プレイヤーが所持している港を取得する関数
function getPlayerPorts(player) {
const ports = new Set(); // 港を格納するためのSet
// プレイヤーが所有する全てのHexを調べる
hexs.forEach(hex => {
if (hex.owner === player) { // プレイヤーが所有しているHex
// Hexの辺(edge)を調べて、その辺に港があるかを確認
hex.edges.forEach((edge, index) => {
if (edge.port) { // 港が設定されている辺
ports.add(edge.portType); // `portType` に基づいて港の種類を取得
}
});
}
});
return [...ports]; // Setを配列にして返す
}
// プレイヤーが指定した港に拠点を持っているか確認する関数
function playerHasPortAtLocation(player, portLocation) {
// プレイヤーが港を持っているかどうかを確認するロジック(仮定)
return player.hasHex.some(hex => {
return hex.x === portLocation.x && hex.y === portLocation.y; // (x, y) が一致すればこの港を持っている
});
}
// 貿易時に自動でレート判定する
function getTradeRate(player, resourceType) {
const ports = getPlayerPorts(player);
// 専門港優先
for (const port of ports) {
if (port.rate === 2 && portTypes[resourceType] === port) {
return 2;
}
}
// 一般港あれば3
if (ports.some(p => p.rate === 3)) return 3;
// 港なし
return 4;
}
//button.title = `この資源の交換レート: ${getTradeRate(currentPlayer, resType)}:1`;
//港をHexやEdgeにひも付け
function assignPortsToEdges() {
hexs.forEach(hex => {
portLocations.forEach(port => {
if (hex.row === port.y && hex.col === port.x) {
const edge = hex.edges[port.edge];
if (!edge) return;
edge.port = portTypes[port.type];
edge.portType = port.type;
edge.port.type = port.type;
}
});
});
}
//Edge クラスに drawPort() を追加:
function getHexCoordX(col) {
return canvas.width / 2 + (col - 2) * hexWidth; // 適宜修正
}
function getHexCoordY(row) {
return hexRadius + row * hexRadius * 1.5;
}
/*
################################################################
################################################################
*/
//盗賊関連
function moveRobber(targetHex) {
const currentPlayer = getCurrentPlayer();
clearRobber();
setRobber(targetHex);
const victims = getRobberVictims(targetHex, currentPlayer);
if (victims.length === 0) {
logA(" 奪える資源を持つ相手がいませんでした");
return;
}
const victim = victims[Math.floor(Math.random() * victims.length)];
stealRandomResource(victim, currentPlayer);
}
function clearRobber() {
for (const hex of hexs) {
hex.hasRobber = false;
}
}
function setRobber(hex) {
hex.hasRobber = true;
logA(`🦹♂️ 盗賊が [${terrainTypes[hex.terrain].label}] に移動しました`);
}
//土地のプレイヤーを抽出
function getRobberVictims(hex, currentPlayer) {
const victims = new Set();
for (const key in vertexMap) {
const vertex = vertexMap[key];
if (vertex.hexes.includes(hex) && vertex.owner && vertex.owner !== currentPlayer) {
victims.add(vertex.owner);
}
}
return [...victims];
}
//資源を奪う
function stealRandomResource(fromPlayer, toPlayer) {
const resourceOptions = Object.entries(fromPlayer.resources)
.filter(([_, count]) => count > 0);
if (resourceOptions.length === 0) {
logA(`${fromPlayer.name} は資源を持っていなかったため、奪えませんでした`);
return;
}
const [resType] = resourceOptions[Math.floor(Math.random() * resourceOptions.length)];
fromPlayer.resources[resType]--;
toPlayer.resources[resType]++;
logA(`🪙 ${toPlayer.name} は ${fromPlayer.name} から「${terrainTypes[resType].label}」を1枚奪いました`);
fromPlayer.displayResources();
toPlayer.displayResources();
}
//7バースト
/*
サイコロで 7 が出たとき:
各プレイヤーの資源が 8枚以上あれば、半分(切り捨て)を捨てる。
例:10枚 → 5枚捨てる、9枚 → 4枚捨てる
*/
function handleBurst(player) {
// 総資源数を計算
const total = Object.values(player.resources).reduce((sum, n) => sum + n, 0);
if (total < 8) return;
const toDiscard = Math.floor(total / 2);
logA(`⚠️ ${player.name} はバースト!資源を 半分 (${toDiscard} 枚) 捨てます`);
// ランダムに資源を選んで捨てる
for (let i = 0; i < toDiscard; i++) {
// 所持している資源タイプのみを抽出
const available = Object.keys(player.resources).filter(key => player.resources[key] > 0);
if (available.length === 0) break;
const rand = available[Math.floor(Math.random() * available.length)];
player.resources[rand]--;
}
player.displayResources();
}
/*
################################################################
################################################################
*/
//貿易関連//資材交換のUI
//1.貿易ボタンを押す
actionTypes.trade.click = () => {
startTrade();
}
// ポートと交換レートを設定する関数
function updateRateInputs(player) {
changeButtonsStates('input[type="number"]', false);
changeButtonsStates('input[name="rate"]', false);
// 各資源に対する交換レートを更新
let rateMax = 4;
const ports = getPlayerPorts(player);
/*
Array(2)
0: {label: '一般港', rate: 3, type: 'any'}
1: {label: '鉄の港', rate: 2, type: 'iron'}
*/
//console.log(ports);
ports.some(port=>{
if(port.rate === 3){
rateMax = 3;
return true;
}
});
for (let type in terrainTypes) {
if (terrainTypes[type].c === 0) continue; // 砂漠など除外
const rateInput = inputFields[`rate_${type}`];
//const port = getPlayerPort(player, type);
rateInput.value = rateMax;
ports.some(port=>{
if(port.type === type){
rateInput.value = 2;
return true;
}
});
}
}
// 2. プレイヤーが所有する港を確認する//["soil", "wood", "wheat"]
function getPlayerPorts(player) {
const ports = new Set();
for (const key in vertexMap) {
const vertex = vertexMap[key];
if (vertex.owner === player) {
const vertexPorts = getPortsForVertex(vertex);
vertexPorts.forEach(p => ports.add(p));
//console.log(`Player ${player.name} owns vertex ${key} with ports:`, vertexPorts); // ログ追加
}
}
return [...ports];
}
// 貿易成立を判定する関数
function checkTrade() {
let totalOutput = 0; // 出す資源の合計
let totalInput = 0; // 求む資源の合計
let maxInput = 0; // 求む資源の最大値
let isTradeDenug = false;
// 出す資源ごとの合計計算
for (let type in terrainTypes) {
const b = terrainTypes[type];
if (b.c === 0) continue; // 砂漠など除外
const outputAmount = parseInt(inputFields[`output_${type}`].value, 10);
const rate = parseInt(inputFields[`rate_${type}`].value, 10);
if(rate<1){isTradeDenug=true;}
if (outputAmount > 0) {
totalOutput += outputAmount;
maxInput += (outputAmount * rate); // 出す資源の数とレートで求む資源の最大合計を算出
}
// 求む資源の合計計算
const inputAmount = parseInt(inputFields[`input_${type}`].value, 10);
totalInput += inputAmount;
}
// プレイヤーの所持資源を考慮
for (let type in terrainTypes) {
const b = terrainTypes[type];
if (b.c === 0) continue; // 砂漠など除外
const outputAmount = parseInt(inputFields[`output_${type}`].value, 10);
if (outputAmount > 0 && player.resources[type] < outputAmount) {
logA(`❌ ${type}が足りません。`);
return false; // 所持資源が足りない場合、貿易は成立しない
}
}
// 貿易成立判定
if (totalInput <= maxInput) {
logA("貿易成立!"); // 成立
return true;
} else if (isTradeDenug){
logA("貿易成立!デバッグ中…"); // 成立
return true;
}else {
logA("貿易成立不可。求む資源が多すぎます"); // 成立しない
return false;
}
}
// 決定ボタンの処理
tradeY.addEventListener('click', () => {
// 貿易成立判定を行う
const isTradeValid = checkTrade();
if (isTradeValid) {
// 貿易が成立した場合の処理(貿易実行)
trade();
} else {
// 貿易成立しなかった場合の処理(エラーメッセージ等)
logA("貿易が成立しませんでした");
}
});
// 取り消しボタンの処理
tradeN.addEventListener('click', () => {
// 入力フィールドをリセット
for (let type in terrainTypes) {
inputFields[`output_${type}`].value = '0';
inputFields[`input_${type}`].value = '0';
}
console.log("貿易が取り消されました");
});
function trade(tradeData){
const player = getCurrentPlayer();
//資源チェック:出す資源を持っているか確認
for (let res in tradeData) {
const output = parseInt(tradeData[res].output);
if (output > 0 && player.resources[res] < output) {
logA(`❌ ${player.name} は ${terrainTypes[res].label} を ${output}枚 出せません(手持ち: ${player.resources[res]}枚)`);
return; // 処理中止
}
}
for (let i in tradeData) {
player.resources[i] += parseInt(tradeData[i].input);
player.resources[i] -= parseInt(tradeData[i].output);
}
player.displayResources();
}
// 貿易を開始するボタンを押したときの処理
function startTrade() {
const player = getCurrentPlayer();
updateRateInputs(player)
}
/*
################################################################
################################################################
*/
// 発展カードをプレイヤーに配布する処理
function giveDevelopmentCard(player) {
const card = drawDevelopmentCard();
if (card) {
player.addCard(card.type); // プレイヤーにカードを追加
logA(`${player.name} が発展カード「${card.label}」を入手`);
player.displayCards();
}
}
// 発展ボタンの処理
actionTypes.development.click = () => {
const player = getCurrentPlayer();
// 発展カードに必要な資源があるか確認
const cost = actionTypes.development.cost;
if (player.canAfford(cost)) {
// 資源を消費して発展カードを与える
player.spendResources(cost);
giveDevelopmentCard(player); // ランダムに発展カードを与える
// ログに表示
logA(`${player.name} は発展カードを引きました`);
} else {
logA(`❌ 資源が足りません(発展カード)`);
}
player.displayResources();
};
// 発展カードの山札を作成してシャッフル
function createDevelopmentDeck() {
// 各カードタイプに応じて、カードを追加
for (const cardKey in cardTypes) {
const { talon } = cardTypes[cardKey]; // そのカードの枚数
for (let i = 0; i < talon; i++) {
cardDeck.push({ type: cardKey, label: cardTypes[cardKey].label });
}
}
// 山札をシャッフル
shuffleDeck(cardDeck);
return cardDeck;
}
// 山札をシャッフルする関数
function shuffleDeck(deck) {
for (let i = deck.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[deck[i], deck[j]] = [deck[j], deck[i]]; // 交換
}
}
// 山札から発展カードを引く
function drawDevelopmentCard() {
if (cardDeck.length === 0) {
logA("発展カードがもうありません!");
return null;
}
// 山札から一枚引く
const card = cardDeck.pop(); // 配列の最後の要素を引く(LIFO)
return card;
}
// 発展カードをプレイヤーに配布する処理
function giveDevelopmentCard(player) {
const card = drawDevelopmentCard();
if (card) {
player.addCard(card.type); // プレイヤーにカードを追加
logA(`${player.name} が発展カード「${card.label}」を入手`);
player.displayCards();
}
}
// 発展ボタンの処理
actionTypes.development.click = () => {
const player = getCurrentPlayer();
// 発展カードに必要な資源があるか確認
const cost = actionTypes.development.cost;
if (player.canAfford(cost)) {
// 資源を消費して発展カードを引く
player.spendResources(cost);
giveDevelopmentCard(player); // ランダムに発展カードを与える
// ログに表示
//logA(`${player.name} は発展カードを引きました`);
} else {
logA(`❌ 資源が足りません(発展カード)`);
}
player.displayResources();
};
// カードボタンを有効化する関数(1ターンで1枚だけ使用可能)
function enableCardButtons(player) {
// すべてのカードボタンを無効化
disableCardButtons();
const cardBtns = document.querySelectorAll('.btns .card');
// プレイヤーが持っているカードを基に、対応するカードボタンを有効化
for (const cardType in player.cards) {
// プレイヤーがそのカードを1枚以上持っていればボタンを有効化
const cardBtn = document.getElementById(`${cardType}Btn`); // ボタンのIDはカードタイプ名(例: knightBtn)
if (cardBtn && player.cards[cardType] > 0) {
cardBtn.disabled = false; // カードを持っていればボタンを有効にする
}
}
}
function disableCardButtons() {
const cardBtns = document.querySelectorAll('.btns .cards');
// すべてのカードボタンを無効化
cardBtns.forEach(btn => {
btn.disabled = true;
});
}
function useCard(player, type){
if(type === "knight"){
useKnightCard(player)
}
}
// 騎士カードを使用する処理
function useKnightCard(player) {
disableCardButtons();
if (player.cards["knight"] && player.cards["knight"] > 0) {
// 騎士カードがあれば1枚消費
player.cards["knight"]--;
player.knightCount += 1;
logA(`${player.name} が騎士カードを使用!`);
// 盗賊を移動させる処理
handleRobberMovement(player);
player.displayCards(); // 手持ちカードの表示を更新
} else {
logA(`❌ 騎士カードが足りません`);
}
}
// 街道建設(資源なしで2本の街道を建設)
function buildRoad(player) {
disableCardButtons();
logA(`${player.name} が街道カードを使用!`);
// 道の建設を開始するためのフラグ
let roadsBuilt = 0;
// 道を建設する処理
canvas.addEventListener('click', handleRoadPlacement);
function handleRoadPlacement(e) {
// マウスの座標をキャンバス内に変換
const { x, y } = getCorrectedCoords(e);
// クリックした場所に道が建設できるか確認
const edge = getEdgeUnderMouse(x, y); // 六角形の辺を取得する関数(仮定)
if (edge && !edge.owner) {
// 道がまだ建設されていない場合
edge.setOwner(player); // 道を建設
roadsBuilt++;
player.updateLongestRoad(); // 道を建設した後に最長道を再計算
logA(`${player.name} が道を建設しました`);
// 道を2本建設したら終了
if (roadsBuilt >= 2) {
canvas.removeEventListener('click', handleRoadPlacement); // 道の建設終了
player.displayResources(); // 資源の表示を更新
enableCardButtons(player); // 他のカードボタンを有効化
}
} else {
logA("❌ 道を建設できない場所です");
}
}
}
// 収穫カードの効果を発動
function harvestResources(player) {
disableCardButtons();
logA(`${player.name} が収穫カードを使用!`);
const availableResources = Object.keys(terrainTypes).filter(terrain => terrain !== 'desert'); // 砂漠は除外
// 資源選択ボタンを表示
createResourceSelectionButtons(availableResources, resource => {
player.addResource(resource); // 選ばれた資源をプレイヤーに加える
logA(`${player.name} は「${terrainTypes[resource].label}」を収穫`);
player.displayResources(); // 資源の表示を更新
// もう一度同様に選択させる
createResourceSelectionButtons(availableResources, resource => {
player.addResource(resource); // 2枚目の資源を加える
logA(`${player.name} は「${terrainTypes[resource].label}」を収穫`);
player.displayResources(); // 資源の表示を更新
});
});
}
// 独占カードの効果を発動
function monopolyEffect(player) {
disableCardButtons();
logA(`${player.name} が独占カードを使用!`);
const availableResources = Object.keys(terrainTypes).filter(terrain => terrain !== 'desert'); // 砂漠は除外
// 資源選択ボタンを表示(どの資源を奪うか選ばせる)
createResourceSelectionButtons(availableResources, resource => {
// 他のプレイヤーから資源を奪う
players.forEach(otherPlayer => {
if (otherPlayer !== player && otherPlayer.resources[resource] > 0) {
const stolenAmount = otherPlayer.resources[resource];
player.addResource(resource, stolenAmount); // プレイヤーに資源を追加
otherPlayer.resources[resource] = 0; // 他プレイヤーから資源を削除
logA(`${player.name} は ${otherPlayer.name} から「${terrainTypes[resource].label}」を ${stolenAmount} 枚奪いました`);
otherPlayer.displayResources();
player.displayResources();
}
});
});
}
// 得点(勝利ポイントを1ポイント得る)
function gainVictoryPoint(player) {
disableCardButtons();
player.point++; // プレイヤーの勝利ポイントを1増やす
logA(`${player.name} が勝利ポイントを1ポイント獲得しました`);
player.displayResources(); // 資源状態を更新
}
// 資源選択のボタンを作成する関数
function createResourceSelectionButtons(resourceList, callback) {
const resourceButtonsContainer = document.createElement('div');
resourceButtonsContainer.classList.add('resource-selection');
resourceList.forEach(resource => {
const button = document.createElement('button');
button.textContent = `${terrainTypes[resource].label}`;
button.style.backgroundColor = terrainTypes[resource].color; // 資源の色に合わせる
button.addEventListener('click', () => {
callback(resource); // クリックした資源を引数としてコールバックを呼び出す
resourceButtonsContainer.remove(); // ボタン選択後にボタンを削除
});
resourceButtonsContainer.appendChild(button);
});
// ゲームボードに追加(仮にボード全体に表示)
gameBoard.appendChild(resourceButtonsContainer);
}
/*
################################################################
################################################################
*/
// 最長道の更新とボーナス付与
function updateLongestRoad(player, players) {
const longestRoad = findLongestRoad(player); // 最長道を取得
player.roadLengthMax = longestRoad.length; // 最長道の長さを更新
// 最長道が5本以上かつ最長道の権利を持っている場合にボーナス
if (player.roadLengthMax >= 5 && player.roadLengthMax > getMaxRoadLength()) {
// 5本以上の道を最初に作ったプレイヤーにボーナス
if (player.roadLengthMax === longestRoad.length) {
player.point += 2; // 最長道ボーナスを加算
console.log(`${player.name} が最長道ボーナス 2点を獲得!`);
}
}
// 他のプレイヤーに最長道が更新された場合、ボーナス権利を失う
if (player.roadLengthMax < getMaxRoadLength()) {
console.log(`${player.name} は最長道の権利を失いました`);
}
}
// 全プレイヤーの中で最長道を持っているプレイヤーの道の長さを取得
function getMaxRoadLength() {
let maxLength = 0;
players.forEach(player => {
if (player.roadLengthMax > maxLength) {
maxLength = player.roadLengthMax;
}
});
return maxLength;
}
/*
隣接Hexとの同期はしていない
Edgeの連結ロジック(最長道路)を今の「各Hexが自前のEdgeを持つだけ」の設計でいける
最長道路の探索を「Edge単位」ではなく「Vertex→Vertexの連続」として構成する
Edge は fromVertex と toVertex を持つ
各 Vertex から Edge をたどって次の Vertex へ進むことで、道のパスを構築できる
BFS / DFS で「現在地 → 隣接 → 隣接…」を繰り返して最長を取ればOK
*/
// 最長道を探索する関数(深さ優先探索)
function findLongestRoadFromVertex(startVertex, player, visitedEdges = new Set(), path = []) {
let maxPath = [...path];
const edges = startVertex.edges || [];
for (let edge of edges) {
// 他のプレイヤーの道を通過しない
if (!edge.isOwnedBy(player)) continue; // その道がプレイヤーのものでない場合スキップ
// すでに訪れた道をスキップ
if (visitedEdges.has(edge)) continue;
// 道の先に他プレイヤーの拠点がないか確認
const nextVertex = edge.other(startVertex);
if (nextVertex.owner && nextVertex.owner !== player) {
continue; // 他プレイヤーの拠点があればその道は通過できない
}
const newVisited = new Set(visitedEdges);
newVisited.add(edge);
const newPath = [...path, edge];
const candidate = findLongestRoadFromVertex(nextVertex, player, newVisited, newPath);
if (candidate.length > maxPath.length) {
maxPath = candidate;
}
}
return maxPath;
}
//プレイヤーごとの最大を探す:
function findLongestRoad(player) {
let max = [];
for (const key in vertexMap) {
const vertex = vertexMap[key];
const path = findLongestRoadFromVertex(vertex, player);
if (path.length > max.length) {
max = path;
}
}
return max;
}
/*
################################################################
################################################################
*/
/*
################################################################
################################################################
*/
//初期配置//スネークドラフト方式(A,B,C,D,D,C,B,A...のような順)
function setupInitialPlacementOrder() {
logA("順番に[拠点]と[道]を建設してください");
logA("・[拠点]は六角形の頂点");
logA("・[道]は六角形の辺");
logA("マップをタップして選んでください");
logA("※詳しいルールはページ下部のリンク先「カタン 遊び方」を参照してください")
const forward = players.map((_, i) => i); // [0, 1, 2, 3]
const backward = [...forward].reverse(); // [3, 2, 1, 0]
initialPlacementOrder.push(...forward, ...backward); // 合計8手番
}
function startInitialPlacement() {
setupInitialPlacementOrder(); // ←順番だけセット
showInitialPlacementTurn(); //
}
function showInitialPlacementTurn() {
const playerIndex = initialPlacementOrder[initialPlacementStep];
const player = players[playerIndex];
logA(`==== ${player.name} ====`);
logA(`==== 初回: [拠点]と[道] の建設 ====`);
updateTurnUI();
}
function dice() {
const au =new Audio('mp3/dice.mp3');
au.play();
diceNum = dice1()+dice1();
return diceNum;
}
function dice1() {
return Math.floor(Math.random() * 6) + 1;
}
function drawDice() {
ctx.fillStyle = '#000';
ctx.font = `${hexRadius}px sans-serif`;
ctx.textAlign = 'center';
ctx.fillText(diceNum, hexRadius, hexRadius);
}
//拠点に関係する地形の取得:
function getTerrainsAtVertex(x, y) {
const key = vertexKey(x, y);
const vertex = vertexMap[key];
if (!vertex) return [];
return vertex.hexes.map(hex => hex.terrain);
}
//サイコロ結果で該当するHexがあった場合に、隣接拠点に資源を与える:
function giveResourcesFromHex(hex, numberRolled) {
for (const key in vertexMap) {
const vertex = vertexMap[key];
if (vertex.hexes.includes(hex) && vertex.owner && hex.number === numberRolled) {
vertex.owner.addResource(hex.terrain);
logAndDisplay(`${vertex.owner.name}が${terrainTypes[hex.terrain].label}を獲得!`);
}
}
}
//初回の所持土地に基づく資源配布
function initialResources() {
logA("土地の所有者に初回資源を配布します");
const numbers = Array.from({ length: 11 }, (_, i) => i + 2);
numbers.forEach(num => {
updateResourcesWithDice(null, num)
});
}
// サイコロの結果を使って、資源を増やす処理
function updateResourcesWithDice(player, num=0) {
if(player){
logA("===="+ player.index + ":" + player.name + "====");
}
const diceRoll = num || dice(); // サイコロを振る
if(player){
const message = `■サイコロの結果: [ ${diceRoll} ]`;
logA(message); // ログとテキストエリアに出力
}
hexs.forEach(hex => hex.clearMatch());
// サイコロの結果が7の場合、盗賊が動くため資源を増やさない
if (player && diceRoll === 7) {
const au =new Audio('mp3/byu.mp3');
au.play();
logA("盗賊発生!");
players.forEach(p => handleBurst(p));
handleRobberMovement(player)
return;
}
hexs.forEach(hex => {
if (hex.number === diceRoll) hex.isMatch = true;
});
for (const key in vertexMap) {
const vertex = vertexMap[key];
vertex.provideResources(diceRoll); //
}
players.forEach(player => {
player.displayResources();
});
if(player){
enableCardButtons(player);
}
draw();
}
// 盗賊を移動させる(7を出したときもこれを使用)
function handleRobberMovement(player) {
if (player.isNPC) {
handleRobberMovementNPC(player);
} else {
// 人間プレイヤーなら盗賊移動モードに
robberMoving = true;
logA("🔴 盗賊を移動させてください");
currentRobberMover = player;
}
}
function handleRobberMovementNPC(player) {
// NPCが自分の陣地以外で盗賊を置ける Hex を探す
const candidateHexes = hexs.filter(hex => {
if (hex.hasRobber || hex.terrain === "desert") return false;
// このHexにある拠点の所有者が自分以外ならOK
for (let key in vertexMap) {
const vertex = vertexMap[key];
if (vertex.hexes.includes(hex) && vertex.owner && vertex.owner !== player) {
return true;
}
}
return false;
});
if (candidateHexes.length === 0) {
logA(`${player.name} は盗賊を移動できる場所がありませんでした`);
return;
}
const targetHex = candidateHexes[Math.floor(Math.random() * candidateHexes.length)];
moveRobber(targetHex);
drawWithHover();
}
function draw(){
ctx.clearRect(0, 0, canvas.width, canvas.height);
hexs.forEach(hex => {
hex.draw();
hex.edges.forEach(edge => {
edge.draw(); // 道
edge.drawPort(); // 港 ←追加
});
});
drawSettlements();
drawDice();
}
function shufflePlayers(players) {
for (let i = players.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[players[i], players[j]] = [players[j], players[i]]; // 交換
}
}
//初期
function init(){
logA("土地が生成されました");
// プレイヤーの作成
players.push(new Player({i:0,name:"あなた"}));
players.push(new Player({i:1,name:"プレイヤー1",isNPC:true}));
players.push(new Player({i:2,name:"プレイヤー2",isNPC:true}));
players.push(new Player({i:3,name:"プレイヤー3",isNPC:true}));
setHexs();
setVertex();
hexs.forEach(hex => {
hex.vertices = getVerticesOfHex(hex);
assignEdgesToHex(hex);
});
assignPortsToEdges();//港
createDevelopmentDeck();
//setupInitialPlacementOrder();
//updateTurnUI();
draw();
// 初期配置フェーズ開始
startInitialPlacement();
disableAllButton();//デバッグ時は無効化しない
}
init();
/*
################################################################
################################################################
*/
function placeNPCInitialSetup(player) {
// NPCがプレイヤーである場合に自動配置を行う
if (!player.isNPC) return;
// ランダムな座標で拠点と道を設置
// NPCの拠点を配置する場所をランダムで選ぶ
let validVertex = getRandomValidVertex();
while (validVertex.isOwned()) {
validVertex = getRandomValidVertex(); // 既に他のプレイヤーが所有している場合は再度ランダム選択
}
// NPCはランダムで街を置く
validVertex.setOwner(player, 1); // 街を設置
console.log(`${player.name} が街を設置しました`);
// 街の周りに道を設置
placeRoadAroundCity(validVertex, player);
// 次のターンへ
initialPendingTown = null;
initialPlacementStep++;
if (initialPlacementStep >= initialPlacementOrder.length) {
gamePhase = "main";
currentPlayerIndex = 0;
updateTurnUI();
logA("初期配置が終了しました。");
logA("★★★★ゲーム開始!★★★★");
const au = new Audio('mp3/piro.mp3');
au.play();
initialResources();
updateTurnUI();
changeButtonsStates('#diceBtn', false);
} else {
showInitialPlacementTurn(); // 次の初期配置プレイヤーへ
}
drawWithHover();
}
function getRandomValidVertex2() {
// ランダムな頂点を取得する関数
const vertexKeys = Object.keys(vertexMap);
const randomKey = vertexKeys[Math.floor(Math.random() * vertexKeys.length)];
return vertexMap[randomKey];
}
function getRandomValidVertex() {
const valid = Object.values(vertexMap).filter(vertex => {
return !vertex.isOwned() && !isVertexNearSettlement(vertex);
});
if (valid.length === 0) {
console.warn("有効な頂点が見つかりませんでした");
return null;
}
return valid[Math.floor(Math.random() * valid.length)];
}
function placeRoadAroundCity(vertex, player) {
// 与えられた頂点の周りに道を設置
const adjacentEdges = getEdgesOfVertex(vertex); // 隣接する辺を取得
console.log(adjacentEdges)
// 道をランダムに配置(隣接する辺に道を置く)
adjacentEdges.some(edge => {
if (!edge.isOwned()) {
const cost = buildTypes.load.cost;
if (player.tryBuild(cost) || gamePhase === "initial") {
edge.setOwner(player); // 道を設置
console.log(`${player.name} が道を設置しました`);
return true;
}
}
});
}
function getEdgesOfVertex(vertex) {
// 頂点に隣接する道(Edge)を取得
const edges = [];
for (let i = 0; i < 6; i++) {
// vertex.edges[i] が undefined でないかを確認
const edge = vertex.edges[i];
if (edge) { // undefined でない場合のみ処理を行う
edges.push(edge); // 辺を追加
}
}
return edges;
}
function npcTurn(player) {
// ステップ1:ダイスを振る
const roll = dice();
logA(`🎲 ${player.name} がダイスを振った → [${roll}]`);
updateResourcesWithDice(player, roll);
// ステップ2:建設できるかチェック(街 or 道)
setTimeout(() => {
npcTryBuild(player);
// ステップ3:交渉(とりあえずスキップ)
//logA(`${player.name} は交渉しませんでした`);
// ステップ4:ターン終了
setTimeout(() => {
logA(`${player.name} のターンを終了します`);
endTurn();
}, 2000); // 建設後に少し待ってから終了
}, 2000); // ダイス後に少し考える
}
function npcTryBuild(player) {
// 頂点の中から建設可能なものを探す
const possibleVertices = Object.values(vertexMap).filter(v =>
!v.isOwned() && !isVertexNearSettlement(v)
);
const costTown = buildTypes.town.cost;
const costRoad = buildTypes.load.cost;
if (player.canAfford(costTown) && possibleVertices.length > 0) {
const vertex = possibleVertices[Math.floor(Math.random() * possibleVertices.length)];
vertex.setOwner(player, 1);
player.spendResources(costTown);
player.hasHex.push({ hex: null, type: 1 });
logA(`${player.name} が街を建設しました`);
player.displayResources();
// 街の周りに道を設置
const edges = getEdgesOfVertex(vertex);
const edge = edges.find(e => !e.owner);
if (edge && player.canAfford(costRoad)) {
edge.setOwner(player);
player.spendResources(costRoad);
logA(`${player.name} が街の周りに道を建設しました`);
player.displayResources();
}
} else if (player.canAfford(costRoad)) {
// 道だけ建設(ランダムに空いてる辺を探す)
const allEdges = Object.values(edgeMap);
const freeEdges = allEdges.filter(e => !e.owner);
if (freeEdges.length > 0) {
const edge = freeEdges[Math.floor(Math.random() * freeEdges.length)];
edge.setOwner(player);
player.spendResources(costRoad);
logA(`${player.name} が道を建設しました`);
player.displayResources();
}
} else {
logA(`${player.name} は建設できる資源がありません`);
}
}
/*
################################################################
################################################################
*/
function changeDebug(){
changeButtonsStates('button', false);
let input;
input = document.querySelectorAll('[readonly]');
input.forEach(i => {
i.removeAttribute('readonly');
});
input = document.querySelectorAll('[disabled]');
input.forEach(i => {
i.removeAttribute('disabled');
});
}
const debugInput = document.getElementById("debug");
debugInput.addEventListener("change", (e) => {
isDebug = e.target.checked;
changeDebug();
});
CSS
body{
overflow:hidden;
background-color:#ffffff;
font-family:monospace;
}
canvas{
display: block;
margin: auto;
width: 100vmin;
aspect-ratio: 9 / 9;
position:absolute;
left:0;
top:0;
}
#demo{
position:relative;
left:0;
top:0;
}
#area{
position:absolute;
right:0;
bottom:0;
width:320px;
height:240px;
}
#area textarea{
box-sizing: border-box;
width:100%;
height:200px;
padding-bottom:40px;
}
#area input{
padding:0;
width:100%;
}
#gameBoard{
position:absolute;
right:0;
top:0;
width:320px;
padding-bottom:40px;
}
#rule{
position:absolute;
left:0;
bottom:0;
z-index:2;
}
#rule form{
position:absolute;
left:0;
bottom:0;
}
#rule span{
padding:0 12px;
}
input[readonly]{
background-color:#aaa;
}
input:disabled,
button:disabled {
opacity: 0.8;
cursor: not-allowed;
}
#diceBtn,
#endBtn{
width:100%;
height:40px;
}
#diceBtn:disabled,
#endBtn:disabled{
display:none;
}
#decks{
border:1px solid #000;
}
.deck{
border-bottom:1px solid #000;
}
.deck.thisTurn{
background-color:#ffdddd;
}
.resourcePlace,
.cardPlace{
min-height:16px;
min-width:16px;
}
.name,
.point{
padding:0 8px;
}
.name:before {
content: "[";
}
.name:after {
content: "]";
}
.point:after {
content: " P";
}
i.resource,
i.resource,
i.resource,
i.card{
display:inline-block;
width:16px;
height:16px;
margin-right:1px;
}
#tradePanel button{
width:50%;
}
#tradePanel span{
display:block;
width:100%;
height:20px;
padding:2px 0;
}
#tradePanel{
width:100%;
padding:2px 0;
}
#tradePanel input{
display:block;
width:40px;
}
#tradePanel .inputs{
display:flex;
}
#tradePanel .inputs label{
display:block;
width: 16%;
}
HTML
ページのソースを表示 : Ctrl+U , DevTools : F12
view-source:https://hi0a.com/demo/-js/js-game-catan/
ABOUT
hi0a.com 「ひまあそび」は無料で遊べるミニゲームや便利ツールを公開しています。
プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。
必要な機能の関数をコピペ利用したり勉強に活用できます。
プログラムの動作サンプル結果は画面上部に出力表示されています。
環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。
画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。
仕様変更、API廃止等、実験途中で動かないページもあります。