テトリスつくってみた
index.htm
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>簡易テトリス</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: #2c3e50;
color: #ecf0f1;
font-family: monospace;
padding: 20px;
}
canvas {
border: 2px solid #ecf0f1;
background-color: #34495e;
touch-action: none; /* タッチ操作でのスクロールを防止 */
}
.controls {
margin-top: 20px;
display: grid;
grid-template-areas:
". rotate ."
"left . right"
". drop .";
gap: 10px;
width: 200px;
}
.control-button {
padding: 15px 25px;
font-size: 16px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
user-select: none; /* テキスト選択を防止 */
-webkit-tap-highlight-color: transparent; /* タップ時のハイライトを非表示 */
}
.control-button:active {
background-color: #2980b9;
}
#moveLeft { grid-area: left; }
#rotate { grid-area: rotate; }
#moveRight { grid-area: right; }
#hardDrop { grid-area: drop; }
#score-display {
font-size: 24px;
margin-top: 10px;
}
#high-scores {
margin-top: 20px;
font-size: 18px;
text-align: center;
}
</style>
</head>
<body>
<h1>簡易テトリス</h1>
<canvas id="tetris" width="200" height="400"></canvas>
<div id="score-display">スコア: 0</div>
<div id="high-scores">
<h2>ハイスコア</h2>
<p>記録なし</p>
</div>
<div class="controls">
<button id="moveLeft" class="control-button">←</button>
<button id="rotate" class="control-button">↑</button>
<button id="moveRight" class="control-button">→</button>
<button id="hardDrop" class="control-button">↓</button>
</div>
<script>
const canvas = document.getElementById('tetris');
const context = canvas.getContext('2d');
const scoreDisplay = document.getElementById('score-display');
const highScoresDisplay = document.getElementById('high-scores');
const scale = 20;
const width = canvas.width / scale;
const height = canvas.height / scale;
const playfield = [];
for (let row = 0; row < height; row++) {
playfield[row] = new Array(width).fill(0);
}
const tetrominos = [
// T-piece
[
[0, 1, 0],
[1, 1, 1],
[0, 0, 0]
],
// S-piece
[
[0, 2, 2],
[2, 2, 0],
[0, 0, 0]
],
// Z-piece
[
[3, 3, 0],
[0, 3, 3],
[0, 0, 0]
],
// I-piece
[
[0, 4, 0, 0],
[0, 4, 0, 0],
[0, 4, 0, 0],
[0, 4, 0, 0]
],
// L-piece
[
[0, 5, 0],
[0, 5, 0],
[0, 5, 5]
],
// J-piece
[
[0, 6, 0],
[0, 6, 0],
[6, 6, 0]
],
// O-piece
[
[7, 7],
[7, 7]
]
];
let score = 0;
let currentTetromino = getRandomTetromino();
let tetrominoX = 3;
let tetrominoY = 0;
let dropCounter = 0;
let dropInterval = 1000;
let lastTime = 0;
let moveIntervalId = null;
function getRandomTetromino() {
const index = Math.floor(Math.random() * tetrominos.length);
return tetrominos[index];
}
function draw() {
context.fillStyle = '#34495e';
context.fillRect(0, 0, canvas.width, canvas.height);
drawMatrix(playfield, { x: 0, y: 0 });
drawMatrix(currentTetromino, { x: tetrominoX, y: tetrominoY });
updateScoreDisplay();
}
function drawMatrix(matrix, offset) {
matrix.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) {
context.fillStyle = getColor(value);
context.fillRect((offset.x + x) * scale, (offset.y + y) * scale, scale - 1, scale - 1);
}
});
});
}
function getColor(value) {
const colors = ['#000', '#9b59b6', '#2ecc71', '#e74c3c', '#3498db', '#e67e22', '#f1c40f', '#95a5a6'];
return colors[value];
}
function merge() {
currentTetromino.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) {
playfield[tetrominoY + y][tetrominoX + x] = value;
}
});
});
}
function collide() {
for (let y = 0; y < currentTetromino.length; y++) {
for (let x = 0; x < currentTetromino[y].length; x++) {
if (currentTetromino[y][x] !== 0) {
if (
tetrominoY + y >= height ||
(playfield[tetrominoY + y] && playfield[tetrominoY + y][tetrominoX + x]) !== 0
) {
return true;
}
}
}
}
return false;
}
function rotate(matrix) {
const newMatrix = matrix.map((_, colIndex) => matrix.map(row => row[colIndex])).reverse();
return newMatrix;
}
function sweepLines() {
let clearedLines = 0;
for (let y = height - 1; y >= 0; --y) {
let isLineFull = true;
for (let x = 0; x < width; ++x) {
if (playfield[y][x] === 0) {
isLineFull = false;
break;
}
}
if (isLineFull) {
const row = playfield.splice(y, 1)[0].fill(0);
playfield.unshift(row);
clearedLines++;
y++;
}
}
if (clearedLines > 0) {
const points = [0, 40, 100, 300, 1200];
score += points[clearedLines];
}
}
function updateScoreDisplay() {
scoreDisplay.innerText = `スコア: ${score}`;
}
function saveHighScore() {
const highScoreData = JSON.parse(localStorage.getItem('tetrisHighScore')) || { score: 0, timestamp: '' };
if (score > highScoreData.score) {
const now = new Date();
const formattedDate = `${now.getFullYear()}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getDate().toString().padStart(2, '0')} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
localStorage.setItem('tetrisHighScore', JSON.stringify({ score: score, timestamp: formattedDate }));
}
}
function loadAndDisplayHighScores() {
const highScoreData = JSON.parse(localStorage.getItem('tetrisHighScore'));
if (highScoreData && highScoreData.score > 0) {
highScoresDisplay.innerHTML = `<h2>ハイスコア</h2><p>スコア: ${highScoreData.score}<br>達成日時: ${highScoreData.timestamp}</p>`;
} else {
highScoresDisplay.innerHTML = `<h2>ハイスコア</h2><p>記録なし</p>`;
}
}
function gameover() {
saveHighScore();
alert(`ゲームオーバー!\nあなたのスコアは ${score} です。`);
playfield.forEach(row => row.fill(0));
score = 0;
loadAndDisplayHighScores();
}
function reset() {
currentTetromino = getRandomTetromino();
tetrominoX = 3;
tetrominoY = 0;
if (collide()) {
gameover();
}
}
function update(time = 0) {
const deltaTime = time - lastTime;
lastTime = time;
dropCounter += deltaTime;
if (dropCounter > dropInterval) {
tetrominoY++;
if (collide()) {
tetrominoY--;
merge();
sweepLines();
reset();
}
dropCounter = 0;
}
draw();
requestAnimationFrame(update);
}
// キーボード操作
document.addEventListener('keydown', event => {
if (event.key === 'ArrowLeft') {
tetrominoX--;
if (collide()) tetrominoX++;
} else if (event.key === 'ArrowRight') {
tetrominoX++;
if (collide()) tetrominoX--;
} else if (event.key === 'ArrowDown') {
tetrominoY++;
if (collide()) {
tetrominoY--;
merge();
sweepLines();
reset();
}
} else if (event.key === 'ArrowUp') {
const rotatedTetromino = rotate(currentTetromino);
const originalX = tetrominoX;
currentTetromino = rotatedTetromino;
while (collide()) {
tetrominoX++;
if (collide()) {
tetrominoX = originalX;
currentTetromino = rotate(rotate(rotate(currentTetromino)));
break;
}
}
}
});
// タッチ操作用のボタンを取得
const moveLeftBtn = document.getElementById('moveLeft');
const rotateBtn = document.getElementById('rotate');
const moveRightBtn = document.getElementById('moveRight');
const hardDropBtn = document.getElementById('hardDrop');
/*
// 連続移動を制御する関数
function startMove(direction) {
if (moveIntervalId) return;
move(direction);
moveIntervalId = setInterval(() => {
move(direction);
}, 100);
}
*/
// 左移動ボタン
moveLeftBtn.addEventListener('touchstart', (event) => { event.preventDefault(); move('left'); });
moveLeftBtn.addEventListener('click', (event) => { event.preventDefault(); move('left'); });
// 右移動ボタン
moveRightBtn.addEventListener('touchstart', (event) => { event.preventDefault(); move('right'); });
moveRightBtn.addEventListener('click', (event) => { event.preventDefault(); move('right'); });
function move(direction) {
if (direction === 'left') {
tetrominoX--;
if (collide()) {
tetrominoX++;
}
} else if (direction === 'right') {
tetrominoX++;
if (collide()) {
tetrominoX--;
}
}
}
/*
function stopMove() {
clearInterval(moveIntervalId);
moveIntervalId = null;
}
*/
/*
// 左移動ボタンのイベントリスナー
moveLeftBtn.addEventListener('touchstart', (event) => {
event.preventDefault();
startMove('left');
});
moveLeftBtn.addEventListener('mousedown', (event) => {
event.preventDefault();
startMove('left');
});
moveLeftBtn.addEventListener('touchend', stopMove);
moveLeftBtn.addEventListener('mouseup', stopMove);
// 右移動ボタンのイベントリスナー
moveRightBtn.addEventListener('touchstart', (event) => {
event.preventDefault();
startMove('right');
});
moveRightBtn.addEventListener('mousedown', (event) => {
event.preventDefault();
startMove('right');
});
moveRightBtn.addEventListener('touchend', stopMove);
moveRightBtn.addEventListener('mouseup', stopMove);
*/
// 回転ボタンのイベントリスナー
const handleRotate = (event) => {
event.preventDefault();
const rotatedTetromino = rotate(currentTetromino);
const originalX = tetrominoX;
currentTetromino = rotatedTetromino;
while (collide()) {
tetrominoX++;
if (collide()) {
tetrominoX = originalX;
currentTetromino = rotate(rotate(rotate(currentTetromino)));
break;
}
}
};
rotateBtn.addEventListener('touchstart', handleRotate);
rotateBtn.addEventListener('click', handleRotate);
// ハードドロップボタンのイベントリスナー
const handleHardDrop = (event) => {
event.preventDefault();
let linesDropped = 0;
while (!collide()) {
tetrominoY++;
linesDropped++;
}
tetrominoY--;
linesDropped--;
score += linesDropped * 2;
merge();
sweepLines();
reset();
};
hardDropBtn.addEventListener('touchstart', handleHardDrop);
hardDropBtn.addEventListener('click', handleHardDrop);
// ページ読み込み時にハイスコアを読み込んで表示
document.addEventListener('DOMContentLoaded', loadAndDisplayHighScores);
update();
</script>
</body>
</html>
昔見かけたのは、コードがもっとシンプルなのに豪華だった気が・・・
なんか、まとまらん。