<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>🚢 バトルシップ 🚢</title>
<!-- ============ ① 見た目を整える CSS ============ -->
<style>
:root{
--cell:50px; /* 1 マスの大きさ(旧 70px → 60px) */
--bg-board:#1e3a8a; /* ボード背景 (紺) */
--bg-cell:#60a5fa; /* セル背景 (水色) */
--bg-hit:#f87171; /* 命中 (赤) */
--bg-miss:#cbd5e1; /* 失敗 (灰) */
--bg-near:#facc15; /* ニアミス (黄) */
--font-white:#f8fafc;
}
/* 全体リセット+背景 */
*{margin:0;padding:0;box-sizing:border-box;font-family:"Segoe UI","Yu Gothic","sans-serif"}
body{
min-height:100vh;display:flex;align-items:center;justify-content:center;
background:url('battleship_bg.png') center/cover fixed,#0c4a6e;
color:var(--font-white);
}
/* ===== 共通 800px パネル ===== */
.panel{
width:800px; /* 旧 700px → 800px */
margin:auto;background:rgba(0,0,0,.55);
border-radius:12px;padding:1.5rem 1rem;
backdrop-filter:blur(4px);
}
/* ===== タイトル画面 & 終了画面 ===== */
#titleScreen,#endScreen{
position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
background:rgba(0,0,0,.4);
}
#titleInner,#endInner{display:flex;flex-direction:column;gap:1.2rem;text-align:center}
#titleInner img{max-width:320px;margin:auto}
#titleInner h1{font-size:2.6rem}
#ruleTitle{font-size:1.4rem;font-weight:bold}
#rules{line-height:1.6;text-align:left;margin:auto;max-width:620px}
/* ===== ボタン ===== */
button{
padding:.8rem 2.4rem;font-size:1.2rem;border:none;border-radius:8px;
cursor:pointer;background:#38bdf8;color:#0c4a6e;font-weight:bold;
transition:transform .2s;
}
/* ▼ スタートボタンだけ幅 200px に固定 ▼ */
#startBtn{
width:200px;
padding:.8rem 0; /* 横幅固定なので左右パディングは不要 */
margin:auto;
}
/* ===== ゲーム画面 ===== */
#gameScreen{display:none}
.boardsWrapper{
display:flex;gap:2rem;flex-wrap:wrap;justify-content:center;margin-bottom:1rem
}
.board{
display:grid;grid-template-columns:repeat(6,auto);
background:var(--bg-board);padding:6px;border-radius:8px;
}
.label{
width:var(--cell);height:var(--cell);display:flex;align-items:center;justify-content:center;
background:#0f172a;font-weight:bold;user-select:none
}
.cell{
width:var(--cell);height:var(--cell);
display:flex;align-items:center;justify-content:center;
background:var(--bg-cell);border:2px solid #0f172a;font-size:2rem;cursor:pointer;
transition:background .2s
}
.disabled{cursor:default}
.hit{background:var(--bg-hit)!important}
.miss{background:var(--bg-miss)!important;color:#0f172a!important}
.near{background:var(--bg-near)!important;color:#0f172a!important}
/* 選択可能セルのハイライト (黄破線) */
.selectable{
outline:none;border:3px dashed var(--bg-near)!important;
}
.info{text-align:center;line-height:1.6}
#message{font-size:1.1rem}
#status{font-size:1.3rem;font-weight:bold;margin-top:.4rem}
#actionBtns{display:flex;gap:1rem;justify-content:center;margin:.8rem 0}
/* スマホ幅調整 */
@media(max-width:500px){
:root{--cell:45px} /* 旧 50px → 45px */
#titleInner h1{font-size:2rem}
}
</style>
</head>
<body>
<!-- ============ ② タイトル画面 ============ -->
<section id="titleScreen">
<div id="titleInner" class="panel">
<img src="battleship_title.png" alt="Battleship">
<h1>🚢 バトルシップ 🚢</h1>
<div id="ruleTitle">🎮 ルール概要</div>
<div id="rules">
・5×5 海域に <strong>戦艦(🚢HP3)・駆逐艦(⛴️HP2)・潜水艦(🤿HP1)</strong> を各 1 隻隠す。<br>
・自ターンは「<strong>攻撃</strong>」or「<strong>移動</strong>」。<br>
・攻撃:自艦が隣接(8方向)するマスのみ射程。<br>
・移動:上下左右へ障害物まで好きな距離。<br>
・外れても敵艦が隣接していれば「💦水しぶき」等を通知。<br>
・敵艦をすべて沈めれば勝利!
</div>
<button id="startBtn">🚀 スタート</button>
</div>
</section>
<!-- ============ ③ ゲーム画面 ============ -->
<main id="gameScreen" class="panel">
<div class="boardsWrapper">
<section>
<h2 style="text-align:center;margin:.5rem 0">🛡️ あなたの艦隊</h2>
<div id="playerBoard" class="board"></div>
</section>
<section>
<h2 style="text-align:center;margin:.5rem 0">🎯 CPU 海域 (攻撃先)</h2>
<div id="cpuBoard" class="board"></div>
</section>
</div>
<div class="info">
<div id="message">💡 行動を選んでください。</div>
<div id="actionBtns">
<button id="attackBtn">🔫 攻撃</button>
<button id="moveBtn">⚓ 移動</button>
</div>
<div id="status"></div>
</div>
</main>
<!-- ============ ④ 終了画面 ============ -->
<section id="endScreen" style="display:none">
<div id="endInner" class="panel">
<h1 id="endMsg">🏆 勝利!</h1>
<button id="toTitleBtn">🏠 タイトル画面に戻る</button>
</div>
</section>
<!-- ============ ⑤ ゲームロジック ============ -->
<script>
/*--------------------------------------------------
定数・データ
--------------------------------------------------*/
const SIZE = 5;
const SHIPS = [
{ name: '戦艦', icon: '🚢', hp: 3 },
{ name: '駆逐艦', icon: '⛴️', hp: 2 },
{ name: '潜水艦', icon: '🤿', hp: 1 }
];
const createEmpty = () =>
Array.from({ length: SIZE }, () =>
Array.from({ length: SIZE }, () => ({ shipIdx: null, hpLeft: 0 })));
const inBoard = (x, y) => x >= 0 && x < SIZE && y >= 0 && y < SIZE;
const neighbors = (x, y) => [
[x - 1, y - 1], [x, y - 1], [x + 1, y - 1],
[x - 1, y], [x + 1, y],
[x - 1, y + 1], [x, y + 1], [x + 1, y + 1]
].filter(([nx, ny]) => inBoard(nx, ny));
/*--------------------------------------------------
グローバルゲーム状態
--------------------------------------------------*/
let player, cpu, gameOver = false, currentAction = null;
let moveStage = null; // null | 'selectShip' | 'selectDest'
let selShipCoord = null; // {x,y}
/*--------------------------------------------------
DOM 取得
--------------------------------------------------*/
const playerBoardEl = document.getElementById('playerBoard');
const cpuBoardEl = document.getElementById('cpuBoard');
const msgEl = document.getElementById('message');
const statusEl = document.getElementById('status');
const attackBtn = document.getElementById('attackBtn');
const moveBtn = document.getElementById('moveBtn');
/*--------------------------------------------------
視覚効果クリア
--------------------------------------------------*/
function clearSplashWave() {
[cpuBoardEl, playerBoardEl].forEach(boardEl=>{
boardEl.querySelectorAll('.cell.near, .cell.miss').forEach(c=>{
c.classList.remove('near','miss','disabled'); c.textContent='';
});
});
}
/*--------------------------------------------------
盤面生成
--------------------------------------------------*/
const makeLabel = t=>{
const d=document.createElement('div'); d.className='label'; d.textContent=t; return d;
};
function buildBoard(parent,isCpu){
parent.innerHTML='';
parent.appendChild(makeLabel(''));
for(let c=0;c<SIZE;c++) parent.appendChild(makeLabel(String.fromCharCode(65+c)));
for(let r=0;r<SIZE;r++){
parent.appendChild(makeLabel(r+1));
for(let c=0;c<SIZE;c++){
const cell=document.createElement('div');
cell.className='cell'; cell.dataset.x=c; cell.dataset.y=r;
if(isCpu) cell.addEventListener('click',()=>onCpuBoardClick(cell));
parent.appendChild(cell);
}
}
}
/*--------------------------------------------------
ゲーム初期化
--------------------------------------------------*/
document.getElementById('startBtn').addEventListener('click',startGame);
document.getElementById('toTitleBtn').addEventListener('click',()=>{
document.getElementById('endScreen').style.display='none';
document.getElementById('titleScreen').style.display='flex';
});
function startGame(){
player={board:createEmpty(),ships:JSON.parse(JSON.stringify(SHIPS))};
cpu ={board:createEmpty(),ships:JSON.parse(JSON.stringify(SHIPS))};
placeRandom(player.board); placeRandom(cpu.board);
gameOver=false; currentAction=null; moveStage=null; selShipCoord=null;
buildBoard(playerBoardEl,false); buildBoard(cpuBoardEl,true);
renderPlayerShips(); updateStatus();
msgEl.textContent='💡 行動を選んでください。';
document.getElementById('titleScreen').style.display='none';
document.getElementById('gameScreen').style.display='block';
}
/* ランダム配置 */
function placeRandom(board){
SHIPS.forEach((_,idx)=>{
let x,y;
do{ x=Math.random()*SIZE|0; y=Math.random()*SIZE|0 }
while(board[y][x].shipIdx!==null);
board[y][x]={shipIdx:idx,hpLeft:SHIPS[idx].hp};
});
}
/*--------------------------------------------------
ボタンハンドラ
--------------------------------------------------*/
attackBtn.onclick=()=>{
if(gameOver) return;
currentAction='attack'; prepareAttack();
};
moveBtn.onclick=()=>{
if(gameOver) return;
currentAction='move'; prepareMove();
};
/*--------------------------------------------------
攻撃準備
- 既に hit のセルも再選択可
- miss は無視
--------------------------------------------------*/
function prepareAttack(){
clearSplashWave(); clearHighlights();
const atkSet=new Set();
for(let y=0;y<SIZE;y++)for(let x=0;x<SIZE;x++)
if(player.board[y][x].shipIdx!==null)
neighbors(x,y).forEach(([nx,ny])=>atkSet.add(`${nx},${ny}`));
cpuBoardEl.querySelectorAll('.cell').forEach(cell=>{
const key=`${cell.dataset.x},${cell.dataset.y}`;
if(atkSet.has(key)&&!cell.classList.contains('miss'))
cell.classList.add('selectable');
});
msgEl.textContent='🔫 攻撃:ハイライトされたマスをクリック!';
}
/*--------------------------------------------------
CPU ボードクリック
--------------------------------------------------*/
function onCpuBoardClick(cell){
if(gameOver||currentAction!=='attack'||!cell.classList.contains('selectable')) return;
const x=+cell.dataset.x, y=+cell.dataset.y;
clearHighlights(); currentAction=null;
executeAttack(player,cpu,cell,x,y,true);
endPlayerTurn();
}
/*--------------------------------------------------
移動準備
--------------------------------------------------*/
function prepareMove(){
clearSplashWave(); clearHighlights();
moveStage='selectShip'; msgEl.textContent='⚓ 移動:艦をクリックしてください。';
playerBoardEl.querySelectorAll('.cell').forEach(cell=>{
if(player.board[cell.dataset.y][cell.dataset.x].shipIdx!==null)
cell.classList.add('selectable');
});
playerBoardEl.addEventListener('click',onPlayerBoardClick);
}
/*--------------------------------------------------
プレイヤーボードクリック
--------------------------------------------------*/
function onPlayerBoardClick(e){
const cell=e.target.closest('.cell'); if(!cell) return;
const x=+cell.dataset.x, y=+cell.dataset.y;
if(moveStage==='selectShip'){
if(!cell.classList.contains('selectable')) return;
selShipCoord={x,y}; highlightDestinations(x,y); moveStage='selectDest';
}else if(moveStage==='selectDest'){
if(!cell.classList.contains('selectable')) return;
moveShip(selShipCoord.x,selShipCoord.y,x,y);
}
}
/*--------------------------------------------------
移動可能マスハイライト
--------------------------------------------------*/
function highlightDestinations(sx,sy){
clearHighlights();
playerBoardEl.querySelector(`.cell[data-x="${sx}"][data-y="${sy}"]`).classList.add('selectable');
[[1,0],[-1,0],[0,1],[0,-1]].forEach(([dx,dy])=>{
let x=sx,y=sy;
while(true){
x+=dx; y+=dy;
if(!inBoard(x,y)||player.board[y][x].shipIdx!==null) break;
playerBoardEl.querySelector(`.cell[data-x="${x}"][data-y="${y}"]`).classList.add('selectable');
}
});
msgEl.textContent='⚓ 行き先をクリックしてください。';
}
/*--------------------------------------------------
移動実行
--------------------------------------------------*/
function moveShip(sx,sy,dx,dy){
const info=player.board[sy][sx];
player.board[sy][sx]={shipIdx:null,hpLeft:0};
player.board[dy][dx]={shipIdx:info.shipIdx,hpLeft:info.hpLeft};
renderPlayerShips(); clearHighlights();
playerBoardEl.removeEventListener('click',onPlayerBoardClick);
moveStage=null; currentAction=null;
msgEl.textContent=`⚓ ${SHIPS[info.shipIdx].name} を移動!`;
endPlayerTurn();
}
/*--------------------------------------------------
ハイライト解除
--------------------------------------------------*/
const clearHighlights=()=>document.querySelectorAll('.selectable').forEach(c=>c.classList.remove('selectable'));
/*--------------------------------------------------
プレイヤー艦表示
--------------------------------------------------*/
function renderPlayerShips(){
playerBoardEl.querySelectorAll('.cell').forEach(cell=>{
const info=player.board[cell.dataset.y][cell.dataset.x];
cell.textContent=info.shipIdx!==null?SHIPS[info.shipIdx].icon:'';
});
}
/*--------------------------------------------------
攻撃処理
- HP が 0 になった艦は盤面から除去しアイコンも消す
- プレイヤー艦撃沈時は即座に描画更新
--------------------------------------------------*/
function executeAttack(atk,def,cellEl,x,y,isPlayer){
const target=def.board[y][x];
if(target.shipIdx!==null){ /* ===== 命中 ===== */
target.hpLeft--; def.ships[target.shipIdx].hp--; cellEl.classList.add('hit');
if(def.ships[target.shipIdx].hp>0){
msgEl.textContent=isPlayer
?`🎯 命中!${SHIPS[target.shipIdx].name} (残HP ${def.ships[target.shipIdx].hp})`
:`⚠️ CPU の攻撃が命中!あなたの ${SHIPS[target.shipIdx].name} (残HP ${def.ships[target.shipIdx].hp})`;
}else{ /* ===== 撃沈 ===== */
msgEl.textContent=isPlayer
?`💥 ${SHIPS[target.shipIdx].name} を撃沈!`
:`💥 CPU があなたの ${SHIPS[target.shipIdx].name} を撃沈!`;
/* 盤面から艦を消す(再度そのセルを攻撃すると miss / near 判定) */
def.board[y][x]={shipIdx:null,hpLeft:0};
if(def===player) renderPlayerShips(); // 自艦が沈んだ場合は描画更新
/* HP 合計が 0 になれば勝敗決定 */
if(def.ships.every(s=>s.hp===0)){ finishGame(isPlayer); return; }
}
}else{ /* ===== ミス / ニアミス ===== */
const near=neighbors(x,y).some(([nx,ny])=>def.board[ny][nx].shipIdx!==null);
cellEl.classList.add(near?'near':'miss');
cellEl.textContent=near?'💦':'🌊';
msgEl.textContent=near?'💦 水しぶき!':'🌊 波高し…';
}
updateStatus();
}
/*--------------------------------------------------
プレイヤーターン終了
--------------------------------------------------*/
function endPlayerTurn(){
if(gameOver) return;
setTimeout(cpuTurn,700);
}
/*--------------------------------------------------
CPU ターン
--------------------------------------------------*/
function cpuTurn(){
if(gameOver) return;
(Math.random()<0.5||!cpuCanMove())?cpuAttack():cpuMove();
if(!gameOver) msgEl.textContent+=' あなたの番です。行動を選んでください。';
}
/*--------------------------------------------------
CPU 攻撃
--------------------------------------------------*/
function cpuAttack(){
const cand=[];
for(let y=0;y<SIZE;y++)for(let x=0;x<SIZE;x++)
if(cpu.board[y][x].shipIdx!==null)
neighbors(x,y).forEach(([nx,ny])=>{
const c=playerBoardEl.querySelector(`.cell[data-x="${nx}"][data-y="${ny}"]`);
if(!c.classList.contains('hit')&&!c.classList.contains('miss')) cand.push({x:nx,y:ny});
});
const choice=cand.length?cand[Math.random()*cand.length|0]:
{x:Math.random()*SIZE|0,y:Math.random()*SIZE|0};
executeAttack(cpu,player,
playerBoardEl.querySelector(`.cell[data-x="${choice.x}"][data-y="${choice.y}"]`),
choice.x,choice.y,false);
}
/*--------------------------------------------------
CPU 移動可否
--------------------------------------------------*/
function cpuCanMove(){
for(let y=0;y<SIZE;y++)for(let x=0;x<SIZE;x++)
if(cpu.board[y][x].shipIdx!==null)
for(const [dx,dy] of [[1,0],[-1,0],[0,1],[0,-1]])
if(inBoard(x+dx,y+dy)&&cpu.board[y+dy][x+dx].shipIdx===null) return true;
return false;
}
/*--------------------------------------------------
CPU 移動
--------------------------------------------------*/
function cpuMove(){
const ships=[];
for(let y=0;y<SIZE;y++)for(let x=0;x<SIZE;x++)
if(cpu.board[y][x].shipIdx!==null) ships.push({x,y,info:cpu.board[y][x]});
shuffle(ships);
for(const s of ships){
const moves=calcDest(s.x,s.y,cpu.board);
if(moves.length){
const d=moves[Math.random()*moves.length|0];
cpu.board[s.y][s.x]={shipIdx:null,hpLeft:0};
cpu.board[d.y][d.x]={shipIdx:s.info.shipIdx,hpLeft:s.info.hpLeft};
return;
}
}
cpuAttack(); // 移動不可なら攻撃
}
/*--------------------------------------------------
目的セル計算
--------------------------------------------------*/
function calcDest(sx,sy,board){
const dest=[];
[[1,0],[-1,0],[0,1],[0,-1]].forEach(([dx,dy])=>{
let x=sx,y=sy;
while(true){
x+=dx; y+=dy;
if(!inBoard(x,y)||board[y][x].shipIdx!==null) break;
dest.push({x,y});
}
});
return dest;
}
/*--------------------------------------------------
Fisher–Yates シャッフル
--------------------------------------------------*/
const shuffle=arr=>{
for(let i=arr.length-1;i>0;i--){
const j=Math.random()*(i+1)|0;
[arr[i],arr[j]]=[arr[j],arr[i]];
}
};
/*--------------------------------------------------
ステータス更新
--------------------------------------------------*/
function updateStatus(){
const p=player.ships.reduce((a,s)=>a+s.hp,0);
const c=cpu.ships.reduce((a,s)=>a+s.hp,0);
statusEl.textContent=`🟦 あなた HP:${p} | 🟥 CPU HP:${c}`;
}
/*--------------------------------------------------
勝敗処理
--------------------------------------------------*/
function finishGame(playerWin){
gameOver=true;
document.getElementById('gameScreen').style.display='none';
document.getElementById('endMsg').textContent=playerWin?'🏆 勝利!':'😱 敗北…';
document.getElementById('endScreen').style.display='flex';
}
/*--------------------------------------------------
初回:背景用ダミーボード
--------------------------------------------------*/
buildBoard(playerBoardEl,false); buildBoard(cpuBoardEl,true);
</script>
</body>
</html>