junkerstock
 ccc0.0.09 

<!DOCTYPE html>
<html lang="ja">
<!-- 蛇ゲーム グリッドサイズをさらに120PXに変更してみた -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Snake Game (120px Huge Mode)</title>
<style>
body {
background-color: #111;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
color: white;
font-family: 'Courier New', Courier, monospace;
overflow: hidden;
touch-action: none; user-select: none; -webkit-user-select: none;
}
#scoreBoard {
font-size: 24px;
margin-bottom: 10px;
z-index: 5;
position: absolute;
top: 20px;
text-shadow: 2px 2px 0 #000;
}
canvas {
border: 4px solid #333;
background-color: #0a0a0a;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
border-radius: 8px;
/* 120pxは大きいので画面からはみ出さないよう縮小表示を許可 */
max-width: 98vw;
max-height: 85vh;
object-fit: contain;
}
#gameOverScreen {
display: none;
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background-color: rgba(0, 0, 0, 0.9);
padding: 30px; border: 2px solid #fff;
z-index: 10; min-width: 220px; border-radius: 10px;
}
#replayBtn {
padding: 15px 20px; font-size: 20px; cursor: pointer;
background-color: #00ffcc; border: none; width: 100%;
border-radius: 5px; font-weight: bold; margin-top: 20px;
}
#startMessage {
position: absolute; bottom: 5%; color: #aaa;
font-size: 14px; text-align: center; width: 100%;
text-shadow: 1px 1px 2px #000;
}
</style>
</head>
<body>

<div id="scoreBoard">Score: <span id="scoreVal">0</span></div>
<canvas id="gameCanvas"></canvas>

<div id="startMessage">PC: 矢印キー / スマホ: スワイプ<br>餌1つで障害物が2つ増えます</div>

<div id="gameOverScreen">
<h2 style="color:red; margin:0 0 20px 0;">GAME OVER</h2>
<p>Score: <span id="finalScore">0</span></p>
<button id="replayBtn">REPLAY</button>
</div>

<script>
let canvas, ctx;
let gameInterval;

// --- 設定変更エリア ---
const gs = 120; // ★120pxに変更
const viewTiles = 9; // ★奇数に変更(9x9)することで、プレイヤーを完全な中心に配置
const worldSize = 60;

// --- アセット管理システム ---
const assets = {
head: { src: 'head.png', img: new Image(), loaded: false, fallback: null },
body: { src: 'body.png', img: new Image(), loaded: false, fallback: null },
food: { src: 'food.png', img: new Image(), loaded: false, fallback: null },
obstacle: { src: 'obstacle.png', img: new Image(), loaded: false, fallback: null }
};

// --- フォールバック画像の生成(線幅などをサイズに合わせて調整) ---
function createFallbackGraphic(type) {
const c = document.createElement('canvas');
c.width = gs; c.height = gs;
const x = c.getContext('2d');

if (type === 'head') {
x.fillStyle = "#228B22";
x.fillRect(0, 0, gs, gs);
x.fillStyle = "white";
x.fillRect(gs*0.2, gs*0.2, gs*0.25, gs*0.25);
x.fillRect(gs*0.55, gs*0.2, gs*0.25, gs*0.25);
x.fillStyle = "black";
x.fillRect(gs*0.25, gs*0.25, gs*0.1, gs*0.1);
x.fillRect(gs*0.6, gs*0.25, gs*0.1, gs*0.1);
}
else if (type === 'body') {
x.fillStyle = "#32CD32";
x.fillRect(0, 0, gs, gs);
x.strokeStyle = "#228B22";
x.lineWidth = gs * 0.05; // サイズに合わせて線を太く
x.strokeRect(gs*0.02, gs*0.02, gs*0.96, gs*0.96);
}
else if (type === 'food') {
x.beginPath();
x.arc(gs/2, gs/2, gs/2 - (gs*0.1), 0, Math.PI*2);
x.fillStyle = "cyan";
x.fill();
x.lineWidth = gs * 0.05;
x.strokeStyle = "white";
x.stroke();
}
else if (type === 'obstacle') {
x.fillStyle = "#cc0000";
x.fillRect(0, 0, gs, gs);
x.strokeStyle = "#550000";
x.lineWidth = gs * 0.08;
x.strokeRect(gs*0.05, gs*0.05, gs*0.9, gs*0.9);
x.beginPath();
x.strokeStyle = "white";
x.lineWidth = gs * 0.08;
x.moveTo(gs*0.2, gs*0.2); x.lineTo(gs*0.8, gs*0.8);
x.moveTo(gs*0.8, gs*0.2); x.lineTo(gs*0.2, gs*0.8);
x.stroke();
}
return c;
}

function initAssets() {
const types = ['head', 'body', 'food', 'obstacle'];
types.forEach(type => {
assets[type].fallback = createFallbackGraphic(type);
assets[type].img.onload = () => { assets[type].loaded = true; };
assets[type].img.src = assets[type].src;
});
}

function getAsset(type) {
return assets[type].loaded ? assets[type].img : assets[type].fallback;
}

// --- ゲーム変数 ---
let px, py;
let ax, ay;
let xv, yv;
let trail;
let tail;
let score;
let obstacles = [];
let isGameOver = false;
let isGameStarted = false;
let animFrame = 0;
let touchStartX = 0;
let touchStartY = 0;

window.onload = function() {
canvas = document.getElementById("gameCanvas");
canvas.width = viewTiles * gs;
canvas.height = viewTiles * gs;
ctx = canvas.getContext("2d");

initAssets();

document.addEventListener("keydown", keyPush);
document.addEventListener("touchstart", handleTouchStart, {passive: false});
document.addEventListener("touchend", handleTouchEnd, {passive: false});
document.getElementById("replayBtn").addEventListener("click", resetGame);

resetGame();
}

function resetGame() {
px = Math.floor(worldSize / 2);
py = Math.floor(worldSize / 2);
xv = yv = 0;
trail = []; obstacles = [];
tail = 5; score = 0;
isGameOver = false; isGameStarted = false;
animFrame = 0;

spawnFood();
updateScore(0);
document.getElementById("gameOverScreen").style.display = "none";
document.getElementById("startMessage").style.visibility = "visible";

clearInterval(gameInterval);
// ★スピード変更: 1000 / 6 (秒間6フレーム = 遅め)
gameInterval = setInterval(game, 1000 / 6);
}

function isNearPlayer(x, y) {
const safeZone = Math.ceil(viewTiles / 2) + 2;
if (Math.abs(x - px) < safeZone && Math.abs(y - py) < safeZone) return true;
return false;
}

function isOccupied(x, y) {
if (isPositionOnSnake(x, y)) return true;
if (x === px && y === py) return true;
if (x === ax && y === ay) return true;
for (let obs of obstacles) if (obs.x === x && obs.y === y) return true;
return false;
}

function spawnFood() {
let attempt = 0;
do {
ax = Math.floor(Math.random() * worldSize);
ay = Math.floor(Math.random() * worldSize);
attempt++; if (attempt > 1000) break;
} while(isOccupied(ax, ay) || isNearPlayer(ax, ay));
}

function spawnObstacle() {
let ox, oy; let attempt = 0;
do {
ox = Math.floor(Math.random() * worldSize);
oy = Math.floor(Math.random() * worldSize);
attempt++; if (attempt > 1000) return;
} while(isOccupied(ox, oy) || isNearPlayer(ox, oy));
obstacles.push({x: ox, y: oy});
}

function game() {
px += xv; py += yv;
if (px < 0 || px >= worldSize || py < 0 || py >= worldSize) { gameOver(); return; }
for (let obs of obstacles) { if (px === obs.x && py === obs.y) { gameOver(); return; } }

// --- 描画処理 ---
ctx.fillStyle = "#222"; ctx.fillRect(0, 0, canvas.width, canvas.height);

// ★中心位置計算の修正
// viewTilesが奇数(9)なので、Math.floor(9/2) = 4。
// プレイヤーpxがワールド座標でどこにいても、キャンバス上では常にインデックス4(ど真ん中)に来るようにオフセット計算
const centerTileIndex = Math.floor(viewTiles / 2);
const offsetX = centerTileIndex - px;
const offsetY = centerTileIndex - py;

// 床とグリッド
ctx.fillStyle = "#0a0a0a";
ctx.fillRect(offsetX * gs, offsetY * gs, worldSize * gs, worldSize * gs);
ctx.strokeStyle = "#333"; ctx.lineWidth = 1; ctx.beginPath();

let startX = Math.floor(-offsetX) - 1;
let endX = startX + viewTiles + 2;
let startY = Math.floor(-offsetY) - 1;
let endY = startY + viewTiles + 2;

for (let x = startX; x <= endX; x++) {
if(x >= 0 && x <= worldSize) {
let drawX = (x + offsetX) * gs;
ctx.moveTo(drawX, (0 + offsetY) * gs); ctx.lineTo(drawX, (worldSize + offsetY) * gs);
}
}
for (let y = startY; y <= endY; y++) {
if(y >= 0 && y <= worldSize) {
let drawY = (y + offsetY) * gs;
ctx.moveTo((0 + offsetX) * gs, drawY); ctx.lineTo((worldSize + offsetX) * gs, drawY);
}
}
ctx.stroke();

ctx.strokeStyle = "#cc0000"; ctx.lineWidth = 8;
ctx.strokeRect(offsetX * gs, offsetY * gs, worldSize * gs, worldSize * gs);

// --- オブジェクト描画 ---

// 障害物
animFrame++;
const pulseScale = 1 + 0.05 * Math.sin(animFrame * 0.2);
const pulseSize = gs * pulseScale;
const pulseOffsetXY = (gs - pulseSize) / 2;
const obstacleImg = getAsset('obstacle');

for (let obs of obstacles) {
let drawOx = (obs.x + offsetX) * gs;
let drawOy = (obs.y + offsetY) * gs;
if (drawOx >= -gs && drawOx <= canvas.width && drawOy >= -gs && drawOy <= canvas.height) {
ctx.drawImage(obstacleImg, drawOx + pulseOffsetXY, drawOy + pulseOffsetXY, pulseSize, pulseSize);
}
}

// エサ
ctx.drawImage(getAsset('food'), (ax + offsetX) * gs, (ay + offsetY) * gs, gs, gs);

// ヘビの体
const bodyImg = getAsset('body');
for (var i = 0; i < trail.length; i++) {
let drawTx = (trail[i].x + offsetX) * gs;
let drawTy = (trail[i].y + offsetY) * gs;
if (drawTx >= -gs && drawTx <= canvas.width && drawTy >= -gs && drawTy <= canvas.height) {
ctx.drawImage(bodyImg, drawTx, drawTy, gs, gs);
}
if (isGameStarted && trail[i].x == px && trail[i].y == py) { gameOver(); return; }
}

// ヘビの頭
ctx.drawImage(getAsset('head'), (px + offsetX) * gs, (py + offsetY) * gs, gs, gs);

trail.push({ x: px, y: py });
while (trail.length > tail) { trail.shift(); }

if (ax == px && ay == py) {
tail++; score += 10; updateScore(score);
spawnFood();
// ★変更: 1つ食べるにつき、障害物を2つ追加
spawnObstacle();
spawnObstacle();
}
}

function isPositionOnSnake(x, y) {
if (!trail) return false;
for (let i = 0; i < trail.length; i++) { if (trail[i].x === x && trail[i].y === y) return true; }
return false;
}
function updateScore(newScore) { document.getElementById("scoreVal").innerText = newScore; }
function gameOver() {
isGameOver = true; clearInterval(gameInterval);
document.getElementById("finalScore").innerText = score;
document.getElementById("gameOverScreen").style.display = "block";
document.getElementById("startMessage").style.visibility = "hidden";
}
function startGameTrigger() {
if (!isGameStarted) { isGameStarted = true; document.getElementById("startMessage").style.visibility = "hidden"; }
}
function keyPush(evt) {
if (isGameOver) return; startGameTrigger();
switch (evt.keyCode) {
case 37: if (xv !== 1) { xv = -1; yv = 0; } break;
case 38: if (yv !== 1) { xv = 0; yv = -1; } break;
case 39: if (xv !== -1) { xv = 1; yv = 0; } break;
case 40: if (yv !== -1) { xv = 0; yv = 1; } break;
}
}
function handleTouchStart(evt) {
if(evt.target.id === "gameCanvas" || evt.target.tagName === "BODY") evt.preventDefault();
touchStartX = evt.touches[0].clientX; touchStartY = evt.touches[0].clientY;
}
function handleTouchEnd(evt) {
if (isGameOver) return;
if(evt.target.id === "gameCanvas" || evt.target.tagName === "BODY") evt.preventDefault();
const xDiff = touchStartX - evt.changedTouches[0].clientX;
const yDiff = touchStartY - evt.changedTouches[0].clientY;
if (Math.abs(xDiff) < 10 && Math.abs(yDiff) < 10) return;
startGameTrigger();
if (Math.abs(xDiff) > Math.abs(yDiff)) {
if (xDiff > 0) { if (xv !== 1) { xv = -1; yv = 0; } } else { if (xv !== -1) { xv = 1; yv = 0; } }
} else {
if (yDiff > 0) { if (yv !== 1) { xv = 0; yv = -1; } } else { if (yv !== -1) { xv = 0; yv = 1; } }
}
}
</script>
</body>
</html>


使用変数

animFrame
assets
attempt
ax
ay
bodyImg
c
canvas
centerTileIndex
charset
content
createFallbackGraphic -------( Function )
ctx
display
drawOx
drawOy
drawTx
drawTy
drawX
drawY
endX
endY
fallback
fillStyle
game -------( Function )
gameInterval
gameOver -------( Function )
getAsset -------( Function )
gs
handleTouchEnd -------( Function )
handleTouchStart -------( Function )
height
i
id
initAssets -------( Function )
innerText
isGameOver
isGameStarted
isNearPlayer -------( Function )
isOccupied -------( Function )
isPositionOnSnake -------( Function )
keyPush -------( Function )
lang
lineWidth
loaded
name
obstacleImg
obstacles
offsetX
offsetY
onload
ox
oy
pulseOffsetXY
pulseScale
pulseSize
px
py
resetGame -------( Function )
safeZone
scalable
scale
score
spawnFood -------( Function )
spawnObstacle -------( Function )
src
startGameTrigger -------( Function )
startX
startY
strokeStyle
style
tagName
tail
touchStartX
touchStartY
trail
type
types
updateScore -------( Function )
viewTiles
visibility
width
worldSize
x
xDiff
xv
y
yDiff
yv
秒間6フレーム