<!DOCTYPE html>
<html lang="ja">
<!-- 蛇ゲーム スクロールタイプ 障害物追加 自分近くにはださない スワイプ対応 -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Survival Snake</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: #222;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
border-radius: 4px;
max-width: 95vw;
max-height: 95vh;
}
#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: 10%;
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" width="400" height="400"></canvas>
<div id="startMessage">PC: 矢印キー / スマホ: スワイプ<br>赤ブロックと壁に注意</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 = 20; // グリッドサイズ
const viewTiles = 20; // 画面に見えるタイル数 (半径10)
const worldSize = 60; // 世界の広さ
let px, py;
let ax, ay;
let xv, yv;
let trail;
let tail;
let score;
let foodEatenCount; // エサを食べた回数(赤ブロック出現用)
let obstacles = []; // 赤い障害物のリスト
let isGameOver = false;
let isGameStarted = false;
let touchStartX = 0;
let touchStartY = 0;
window.onload = function() {
canvas = document.getElementById("gameCanvas");
ctx = canvas.getContext("2d");
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;
foodEatenCount = 0;
isGameOver = false;
isGameStarted = false;
spawnFood();
updateScore(0);
document.getElementById("gameOverScreen").style.display = "none";
document.getElementById("startMessage").style.visibility = "visible";
clearInterval(gameInterval);
// 速度調整: 1000/12 = 約83ms (以前の80%の速度)
gameInterval = setInterval(game, 1000 / 12);
}
// 指定座標が「画面内(プレイヤー周辺)」かどうか判定する関数
// 視野(10マス) + 余白(5マス) = 15マス以内なら「画面近辺」とみなす
function isNearPlayer(x, y) {
const safeZone = (viewTiles / 2) + 5; // 10 + 5 = 15
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));
// ↑ isNearPlayerにより、画面外(+5マス)に出現する
}
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);
const centerTile = viewTiles / 2;
const offsetX = centerTile - px;
const offsetY = centerTile - py;
// 床
ctx.fillStyle = "black";
ctx.fillRect(offsetX * gs, offsetY * gs, worldSize * gs, worldSize * gs);
// グリッド
ctx.strokeStyle = "#333";
ctx.lineWidth = 1;
ctx.beginPath();
for (let x = 0; x <= worldSize; x++) {
let drawX = (x + offsetX) * gs;
if (drawX >= -gs && drawX <= canvas.width + gs) {
ctx.moveTo(drawX, (0 + offsetY) * gs);
ctx.lineTo(drawX, (worldSize + offsetY) * gs);
}
}
for (let y = 0; y <= worldSize; y++) {
let drawY = (y + offsetY) * gs;
if (drawY >= -gs && drawY <= canvas.height + gs) {
ctx.moveTo((0 + offsetX) * gs, drawY);
ctx.lineTo((worldSize + offsetX) * gs, drawY);
}
}
ctx.stroke();
// 壁枠
ctx.strokeStyle = "#cc0000"; // 壁は少し暗めの赤
ctx.lineWidth = 4;
ctx.strokeRect(offsetX * gs, offsetY * gs, worldSize * gs, worldSize * gs);
// 赤い障害物の描画
ctx.fillStyle = "red"; // 障害物は明るい赤
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.fillRect(drawOx + 1, drawOy + 1, gs - 2, gs - 2);
// バツ印風のデザインを入れて危険さをアピール
ctx.fillStyle = "darkred";
ctx.fillRect(drawOx + 5, drawOy + 5, gs - 10, gs - 10);
ctx.fillStyle = "red"; // 色を戻す
}
}
// エサ
ctx.fillStyle = "cyan";
ctx.fillRect((ax + offsetX) * gs + 1, (ay + offsetY) * gs + 1, gs - 4, gs - 4);
// ヘビの体
ctx.fillStyle = "lime";
for (var i = 0; i < trail.length; i++) {
let drawTx = (trail[i].x + offsetX) * gs;
let drawTy = (trail[i].y + offsetY) * gs;
ctx.fillRect(drawTx, drawTy, gs - 2, gs - 2);
if (isGameStarted && trail[i].x == px && trail[i].y == py) {
gameOver();
return;
}
}
// ヘビの頭
ctx.fillStyle = "#32CD32";
ctx.fillRect((px + offsetX) * gs, (py + offsetY) * gs, gs - 2, gs - 2);
trail.push({ x: px, y: py });
while (trail.length > tail) {
trail.shift();
}
// エサを食べた時の処理
if (ax == px && ay == py) {
tail++;
score += 10;
foodEatenCount++;
updateScore(score);
spawnFood(); // 遠くに再配置
// 2回に1回、障害物を追加
if (foodEatenCount % 2 === 0) {
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();
const firstTouch = evt.touches[0];
touchStartX = firstTouch.clientX;
touchStartY = firstTouch.clientY;
}
function handleTouchEnd(evt) {
if (isGameOver) return;
if(evt.target.id === "gameCanvas" || evt.target.tagName === "BODY") evt.preventDefault();
const xUp = evt.changedTouches[0].clientX;
const yUp = evt.changedTouches[0].clientY;
const xDiff = touchStartX - xUp;
const yDiff = touchStartY - yUp;
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>
使用変数
| 12 | |
| 2 | |
| 5 | |
| attempt | |
| ax | |
| ay | |
| canvas | |
| centerTile | |
| charset | |
| content | |
| ctx | |
| display | |
| drawOx | |
| drawOy | |
| drawTx | |
| drawTy | |
| drawX | |
| drawY | |
| fillStyle | |
| firstTouch | |
| foodEatenCount | |
| game -------( Function ) | |
| gameInterval | |
| gameOver -------( Function ) | |
| gs | |
| handleTouchEnd -------( Function ) | |
| handleTouchStart -------( Function ) | |
| height | |
| i | |
| id | |
| innerText | |
| isGameOver | |
| isGameStarted | |
| isNearPlayer -------( Function ) | |
| isOccupied -------( Function ) | |
| isPositionOnSnake -------( Function ) | |
| keyPush -------( Function ) | |
| lang | |
| lineWidth | |
| name | |
| obstacles | |
| offsetX | |
| offsetY | |
| onload | |
| ox | |
| oy | |
| px | |
| py | |
| resetGame -------( Function ) | |
| safeZone | |
| scalable | |
| scale | |
| score | |
| spawnFood -------( Function ) | |
| spawnObstacle -------( Function ) | |
| startGameTrigger -------( Function ) | |
| strokeStyle | |
| style | |
| tagName | |
| tail | |
| touchStartX | |
| touchStartY | |
| trail | |
| updateScore -------( Function ) | |
| viewTiles | |
| visibility | |
| width | |
| worldSize | |
| x | |
| xDiff | |
| xUp | |
| xv | |
| y | |
| yDiff | |
| yUp | |
| yv |