見出し画像

テトリス棒が落下するよりも早く実装されたChatGPT4によるテトリス。

テトリスConsole

コンソールで実行されるテトリス。

.htmlファイルで保存し、ダブルクリックしてブラウザで開く。
F12ないしCtrl+Shift+Iなどでデベロッパツールを開く。
表示機能にconsole.logを用いているのでコンソールを開いておく。
document.addEventListenerを用いているのでブラウザの表示領域をクリックしておく。

学習目的であり、それ以上でない。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Text-based Tetris</title>
</head>
<body>
<script>

const gameBoardWidth = 10;
const gameBoardHeight = 20;
const pieces = [
  [
    [1, 1, 1],
    [0, 1, 0]
  ],
  [
    [0, 1, 1],
    [1, 1, 0]
  ],
  [
    [1, 1, 0],
    [0, 1, 1]
  ],
  [
    [1, 1],
    [1, 1]
  ],
  [
    [1, 0, 0],
    [1, 1, 1]
  ],
  [
    [0, 0, 1],
    [1, 1, 1]
  ],
  [
    [1, 1, 1, 1]
  ]
];

let gameBoard = Array.from({
  length: gameBoardHeight
}, () => Array(gameBoardWidth).fill(0));

let currentPiece = {
  x: 0,
  y: 0,
  shape: pieces[Math.floor(Math.random() * pieces.length)]
};

function draw() {
  // ゲームボードをコピーして現在のピースを表示するために使用します
  const displayBoard = gameBoard.map(row => row.slice());
  // currentPieceをdisplayBoardに追加します
  currentPiece.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        displayBoard[currentPiece.y + y][currentPiece.x + x] = 1;
      }
    });
  });
  // displayBoardを使用してゲームボードと現在のピースを表示します
  console.clear();
  let display = displayBoard.map(row => row.map(cell => (cell ? '#' : '.')).join('')).join('\n');
  console.log(display);
}

function mergePiece() {
  currentPiece.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        gameBoard[currentPiece.y + y][currentPiece.x + x] = 1;
      }
    });
  });
}

function movePiece(direction) {
  currentPiece.x += direction;
  if (isCollision()) {
    currentPiece.x -= direction;
    return false;
  }
  return true;
}

function rotatePiece() {
  const temp = currentPiece.shape;
  currentPiece.shape = currentPiece.shape[0].map((_, i) => currentPiece.shape.map(row => row[i])).reverse();
  if (isCollision()) {
    currentPiece.shape = temp;
    return false;
  }
  return true;
}

function isCollision() {
  return currentPiece.shape.some((row, y) => row.some((value, x) => {
    if (!value) return false;
    const boardX = currentPiece.x + x;
    const boardY = currentPiece.y + y;
    return (boardX < 0 || boardX >= gameBoardWidth || boardY < 0 || boardY >= gameBoardHeight || gameBoard[boardY][boardX]);
  }));
}

function movePieceDown() {
  currentPiece.y += 1;
  if (isCollision()) {
    currentPiece.y -= 1;
    mergePiece();
    currentPiece = {
      x: 0,
      y: 0,
      shape: pieces[Math.floor(Math.random() * pieces.length)]
    };
    if (isCollision()) {
      console.log("Game Over");
      clearInterval(gameLoop);
    }
  }
  draw();
}

function clearLines() {
  outer: for (let y = gameBoardHeight - 1; y >= 0;) {
    for (let x = 0; x < gameBoardWidth; x++) {
      if (!gameBoard[y][x]) {
        y--; 
        continue outer;
      }
    }
    gameBoard.splice(y, 1);
    gameBoard.unshift(Array(gameBoardWidth).fill(0));
  }
}

function gameStep() {
  movePieceDown();
  clearLines();
  draw();
}

function userInput(e) {
  switch (e.key) {
  case "ArrowLeft":
    movePiece(-1);
    break;
  case "ArrowRight":
    movePiece(1);
    break;
  case "ArrowUp":
    rotatePiece();
    break;
  case "ArrowDown":
    movePieceDown();
    break;
  }
  draw(); // ユーザー操作の後に画面を更新します
}

document.addEventListener("keydown", userInput);
const gameLoop = setInterval(gameStep, 500);
draw(); 

</script>
</body>
</html>


gameBoard

二重配列を作成する。
この二重配列を最終的に表示領域に転写することになる。


let gameBoard = Array.from({
  length: gameBoardHeight
}, () => Array(gameBoardWidth).fill(0));

Array.from

Array.from({ length: gameBoardHeight }, () => Array(gameBoardWidth).fill(0));

これは二次元配列の作成。

  1. Array.from(): このメソッドは、配列風オブジェクトや反復可能なオブジェクト(配列、マップ、セットなど)から新しい配列インスタンスを作成します。

  2. { length: gameBoardHeight }: これは、プロパティ length を持つオブジェクトを作成しています。これが配列風オブジェクトです。このオブジェクトは、Array.from() に渡すと、指定された長さ(ここではgameBoardHeight )の配列を作成します。

  3. () => Array(gameBoardWidth).fill(0): これは、アロー関数です。この関数は、Array.from() によって作成される各要素に対して実行されます。関数は、長さが gameBoardWidth の新しい配列を作成し、それを fill(0) で0で埋めています。

array-like object

配列風オブジェクト。JavaScriptの仕様上にちゃんとある概念。

配列風オブジェクト(array-like object)とは、配列に似た構造を持つオブジェクトのことです。つまりオブジェクトです。配列風オブジェクトは、通常の配列と同様に、インデックスで要素にアクセスできるプロパティと、要素数を示すlengthプロパティを持ちます。lengthプロパティを持つのが配列風オブジェクトです。しかし、配列風オブジェクトは実際には配列ではないため、配列に固有のメソッド(.push()、.pop()、.splice()など)を持っていません。つまり配列風オブジェクトはオブジェクトであって配列ではありません。

配列風オブジェクトの例。

arrayLikeObject = {
  0: 'first element',
  1: 'second element',
  2: 'third element',
  length: 3
};

ただし

arrayLikeObject = { length: 3 }

としてもインデックスプロパティは生成されない。
それを解釈してくれるのはArray.from()

Array.from({ length: n }, callback) の形式を使用すると、length プロパティを持つオブジェクトを渡して、n の長さを持つ新しい配列を作成します。この場合、Array.from() は length プロパティに基づいてまずundefinedで要素を埋めた配列を作成し、その各要素に対して callback 関数を実行します。

currentPiece

piecesは二重配列を格納する配列。
この配列にはあらかじめ定義されたブロックのデータ(二重配列)が入っている。currentPieceはこのpiecesからランダムでピックアップ。
(x,y)のグリッド座標とセットで供される。


let currentPiece = {
  x: 0,
  y: 0,
  shape: pieces[Math.floor(Math.random() * pieces.length)]
};

draw

function draw() {
  // ゲームボードをコピーして現在のピースを表示するために使用します
  const displayBoard = gameBoard.map(row => row.slice());
  // currentPieceをdisplayBoardに追加します
  currentPiece.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        displayBoard[currentPiece.y + y][currentPiece.x + x] = 1;
      }
    });
  });
  // displayBoardを使用してゲームボードと現在のピースを表示します
  console.clear();
  let display = displayBoard.map(row => row.map(cell => (cell ? '#' : '.')).join('')).join('\n');
  console.log(display);
}

slice

slice()メソッドは、配列や配列風オブジェクトから指定された範囲の要素を含む新しい配列を作成します。このメソッドは元の配列やオブジェクトを変更せず、新しい配列を返します。

slice()メソッドは、以下の形式で使用されます。

array.slice([begin[, end]])

引数:

  • begin: (オプション)新しい配列に含める最初の要素のインデックス。省略された場合、0が使用されます。

  • end: (オプション)新しい配列に含める最後の要素のインデックスの直後のインデックス。このインデックスは含まれません。省略された場合、元の配列の末尾までが含まれます。

const fruits = ['apple', 'orange', 'banana', 'grape', 'strawberry'];

const firstTwoFruits = fruits.slice(0, 2);
console.log(firstTwoFruits); // ["apple", "orange"]

const lastThreeFruits = fruits.slice(-3);
console.log(lastThreeFruits); // ["banana", "grape", "strawberry"]

const allFruits = fruits.slice();
console.log(allFruits); // ["apple", "orange", "banana", "grape", "strawberry"]

この例では、slice()メソッドを使用して、fruits配列から異なる範囲の要素を含む新しい配列を作成しています。

また、slice()メソッドは配列風オブジェクトにも適用できます。ただし、その場合はArray.prototype.slice.call()の形式で使用する必要があります。

const arrayLikeObject = {
  0: 'first',
  1: 'second',
  2: 'third',
  3: 'fourth',
  length: 4
};

const newArray = Array.prototype.slice.call(arrayLikeObject, 1, 3);
console.log(newArray); // ["second", "third"]

gameBoard.map(row => row.slice());

これは二重配列のコピー。

gameBoard.map(row => row.slice());は、二次元配列gameBoardの各行(配列)に対してslice()メソッドを適用し、新しい二次元配列を作成します。この操作は、元のgameBoard配列のコピー(ディープコピー)を作成することになります。

map()メソッドは、配列の各要素に対してコールバック関数を実行し、コールバック関数が返す値を要素とした新しい配列を作成して返します。この場合、コールバック関数はrow => row.slice()です。

row.slice()は、rowという配列のコピー(シャローコピー)を作成します。slice()メソッドに引数を指定しない場合、元の配列のすべての要素を含む新しい配列を返します。

つまり、gameBoard.map(row => row.slice());は、gameBoardの各行に対してslice()を適用し、各行のコピーを含む新しい二次元配列を作成します。これにより、gameBoardのディープコピーが作成されます。このディープコピーでは、元のgameBoardとは異なる新しい配列が作成され、その要素(行)も元の配列とは異なる新しい配列になります。

displayBoardへの転写

  currentPiece.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        displayBoard[currentPiece.y + y][currentPiece.x + x] = 1;
      }
    });
  });

pieceオブジェクトのshapeを操作する。
pieceのindexとshape内でのindexを足せばdisplayBoardのindexとなる。

map, join

let display = gameBoard.map(row => row.map(cell => (cell ? '#' : '.')).join('')).join('\n');

gameBoard : 二次元配列、あるいは行列
row : gameBoardから取り出した行
cell : rowから取り出した要素

.join('')でcellを全部連結
.join('\n')で各行を改行付きで連結

mapの戻り値は配列で、joinの戻り値は文字列
map()メソッドは、配列の各要素に対してコールバック関数を実行し、コールバック関数が返す値を要素とした新しい配列を作成して返します。map()は元の配列を変更せず、新しい配列を返すことに注意してください。
join()メソッドは、配列の要素を指定した区切り文字(セパレータ)で連結した文字列を返します。join()メソッドは、配列を直接変更せず、新しい文字列を返します。

  1. gameBoard.map(row => ...): 二次元配列gameBoardの各行に対してmap()メソッドを実行しています。これにより、新しい配列が生成され、その各要素に対して与えられたコールバック関数が実行されます。この例では、コールバック関数はrow => ...と表されており、各行を引数として受け取ります。

  2. row.map(cell => ...): 各行に対してもう一度map()メソッドを実行しています。これにより、各セル(cell)が与えられたコールバック関数によって処理されます。

  3. cell => (cell ? '#' : '.'): このコールバック関数は、セルの値(0または1)に基づいて新しい文字列を返します。セルの値が1(ブロックがあるセル)の場合、'#'を返し、0(空のセル)の場合、 '.' を返します。これにより、テキストベースのゲーム盤でブロックが存在するセルと空のセルを区別できます。

  4. .join(''): 各行のセルが変換された後、join()メソッドを使用して、各行のセルを連結し、1つの文字列にします。この例では、セル間に追加の文字を挿入せずに連結しています。

  5. .join('\n'): 最後に、変換された各行を連結して、最終的なゲーム盤の文字列を作成します。join()メソッドを使用して、各行を改行文字(\n)で連結します。これにより、各行が改行で区切られた複数行の文字列が得られます。

setInterval

ゲームのメインループを定期実行する。
入力も計算も描画処理も全部このタイミングで発動する。
今の実装では、ブロックの落下速度も同期している。
この方式は簡単だが、処理落ちなどに対応できない。
対応型はrequestAnimationFrameを参照。

テトリスhtml5

コンソールでなく、ちゃんと描画処理が入るテトリス。
コンソールとの最大の違いはdraw部分であり、

draw部分

function draw() {
  // Clear the canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // Draw the game board
  for (let y = 0; y < gameBoardHeight; y++) {
    for (let x = 0; x < gameBoardWidth; x++) {
      if (gameBoard[y][x]) {
        drawBlock(x, y, "#ccc");
      }
    }
  }
  // Draw the current piece
  for (let y = 0; y < currentPiece.shape.length; y++) {
    for (let x = 0; x < currentPiece.shape[y].length; x++) {
      if (currentPiece.shape[y][x]) {
        drawBlock(currentPiece.x + x, currentPiece.y + y, "#ccc");
      }
    }
  }
}


これができるならドットベースのゲームはだいたい作れる。
ただしまだsetIntervalである。

<!DOCTYPE html> 
<html lang = "ja"> 

<head>
<meta charset = "UTF-8">
<title> Text - based Tetris </title> 

<style> 
canvas {
  display: block;
  margin: auto;
  background - color: #222;
  border: 1px solid # ccc;
} 
</style> 

</head> 

<body>
<canvas id = "gameCanvas"
width = "300"
height = "600">
</canvas> 

<script>
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const blockSize = 30;
const gameBoardWidth = 10;
const gameBoardHeight = 20;
const pieces = [
  [
    [1, 1, 1],
    [0, 1, 0]
  ],
  [
    [0, 1, 1],
    [1, 1, 0]
  ],
  [
    [1, 1, 0],
    [0, 1, 1]
  ],
  [
    [1, 1],
    [1, 1]
  ],
  [
    [1, 0, 0],
    [1, 1, 1]
  ],
  [
    [0, 0, 1],
    [1, 1, 1]
  ],
  [
    [1, 1, 1, 1]
  ]
];

let gameBoard = Array.from({
  length: gameBoardHeight
}, () => Array(gameBoardWidth).fill(0));

let currentPiece = {
  x: 0,
  y: 0,
  shape: pieces[Math.floor(Math.random() * pieces.length)]
};

function drawBlock(x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x * blockSize, y * blockSize, blockSize, blockSize);
  ctx.strokeStyle = "#222";
  ctx.strokeRect(x * blockSize, y * blockSize, blockSize, blockSize);
}

function draw() {
  // Clear the canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  // Draw the game board
  for (let y = 0; y < gameBoardHeight; y++) {
    for (let x = 0; x < gameBoardWidth; x++) {
      if (gameBoard[y][x]) {
        drawBlock(x, y, "#ccc");
      }
    }
  }
  // Draw the current piece
  for (let y = 0; y < currentPiece.shape.length; y++) {
    for (let x = 0; x < currentPiece.shape[y].length; x++) {
      if (currentPiece.shape[y][x]) {
        drawBlock(currentPiece.x + x, currentPiece.y + y, "#ccc");
      }
    }
  }
}

// The rest of the functions remain the same as before
function mergePiece() {
  currentPiece.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        gameBoard[currentPiece.y + y][currentPiece.x + x] = 1;
      }
    });
  });
}

function movePiece(direction) {
  currentPiece.x += direction;
  if (isCollision()) {
    currentPiece.x -= direction;
    return false;
  }
  return true;
}

function rotatePiece() {
  const temp = currentPiece.shape;
  currentPiece.shape = currentPiece.shape[0].map((_, i) => currentPiece.shape.map(row => row[i])).reverse();
  if (isCollision()) {
    currentPiece.shape = temp;
    return false;
  }
  return true;
}

function isCollision() {
  return currentPiece.shape.some((row, y) => row.some((value, x) => {
    if (!value) return false;
    const boardX = currentPiece.x + x;
    const boardY = currentPiece.y + y;
    return (boardX < 0 || boardX >= gameBoardWidth || boardY < 0 || boardY >= gameBoardHeight || gameBoard[boardY][boardX]);
  }));
}

function movePieceDown() {
  currentPiece.y += 1;
  if (isCollision()) {
    currentPiece.y -= 1;
    mergePiece();
    currentPiece = {
      x: 0,
      y: 0,
      shape: pieces[Math.floor(Math.random() * pieces.length)]
    };
    if (isCollision()) {
      console.log("Game Over");
      clearInterval(gameLoop);
    }
  }
}

function clearLines() {
  outer: for (let y = gameBoardHeight - 1; y >= 0; y--) {
    for (let x = 0; x < gameBoardWidth; x++) {
      if (!gameBoard[y][x]) {
        continue outer;
      }
    }
    gameBoard.splice(y, 1);
    gameBoard.unshift(Array(gameBoardWidth).fill(0));
  }
}

function gameStep() {
  movePieceDown();
  clearLines();
  draw();
}

function userInput(e) {
  switch (e.key) {
  case "ArrowLeft":
    movePiece(-1);
    break;
  case "ArrowRight":
    movePiece(1);
    break;
  case "ArrowUp":
    rotatePiece();
    break;
  case "ArrowDown":
    movePieceDown();
    break;
  }
  draw();
}

document.addEventListener("keydown", userInput);
const gameLoop = setInterval(gameStep, 500);
draw(); 

</script>
</body>
</html>


テトリスrequestAnimationFrame

描画処理を60フレーム、秒間60回に安定させることを目指す。

計算が早く終わった場合、次の描画タイミングまで処理を止める。
計算が終わらなかった場合、描画処理をスキップする。

下記の実装では計算が終わらなかった場合に、もう一度計算処理を行い、実質描画処理を1回飛ばす。

<!DOCTYPE html>
<html lang = "ja">
<head>
<meta charset = "UTF-8">
<title> Canvas - based Tetris </title> 

<style>
canvas {
  display: block;
  margin: 0 auto;
  background - color: #eee;
}
</style>

</head>
<body>
<canvas id = "gameCanvas"
width = "300"
height = "600" >
</canvas> 
<script>

const gameBoardWidth = 10;
const gameBoardHeight = 20;
const blockSize = 30;
let dropCounter = 0;
const dropInterval = 500; // ブロックが500ミリ秒ごとに落下する
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const pieces = [
  [
    [1, 1, 1],
    [0, 1, 0]
  ],
  [
    [0, 1, 1],
    [1, 1, 0]
  ],
  [
    [1, 1, 0],
    [0, 1, 1]
  ],
  [
    [1, 1],
    [1, 1]
  ],
  [
    [1, 0, 0],
    [1, 1, 1]
  ],
  [
    [0, 0, 1],
    [1, 1, 1]
  ],
  [
    [1, 1, 1, 1]
  ]
];
let gameBoard = Array.from({
  length: gameBoardHeight
}, () => Array(gameBoardWidth).fill(0));
let currentPiece = {
  x: 0,
  y: 0,
  shape: pieces[Math.floor(Math.random() * pieces.length)]
};
// 新しいフォーマットに合わせてゲームループを実装
let lastFrameTime = performance.now();
const targetFrameDuration = 1000 / 60; // 60FPSの場合のフレーム間隔(ミリ秒)
function gameLoop(currentTime) {
  const deltaTime = currentTime - lastFrameTime;
  if (deltaTime >= targetFrameDuration) {
    update(deltaTime);
    render();
    lastFrameTime = currentTime;
  }
  requestAnimationFrame(gameLoop);
}

function update(deltaTime) {
  dropCounter += deltaTime;
  if (dropCounter > dropInterval) {
    movePieceDown();
    dropCounter = 0;
  }
  clearLines();
}

function render() {
  draw();
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (let y = 0; y < gameBoardHeight; y++) {
    for (let x = 0; x < gameBoardWidth; x++) {
      if (gameBoard[y][x]) {
        ctx.fillStyle = "black";
        ctx.fillRect(x * blockSize, y * blockSize, blockSize, blockSize);
        ctx.strokeStyle = "white";
        ctx.strokeRect(x * blockSize, y * blockSize, blockSize, blockSize);
      }
    }
  }
  for (let y = 0; y < currentPiece.shape.length; y++) {
    for (let x = 0; x < currentPiece.shape[y].length; x++) {
      if (currentPiece.shape[y][x]) {
        ctx.fillStyle = "black";
        ctx.fillRect((currentPiece.x + x) * blockSize, (currentPiece.y + y) * blockSize, blockSize, blockSize);
        ctx.strokeStyle = "white";
        ctx.strokeRect((currentPiece.x + x) * blockSize, (currentPiece.y + y) * blockSize, blockSize, blockSize);
      }
    }
  }
}

// The rest of the functions remain the same as before
function mergePiece() {
  currentPiece.shape.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        gameBoard[currentPiece.y + y][currentPiece.x + x] = 1;
      }
    });
  });
}

function movePiece(direction) {
  currentPiece.x += direction;
  if (isCollision()) {
    currentPiece.x -= direction;
    return false;
  }
  return true;
}

function rotatePiece() {
  const temp = currentPiece.shape;
  currentPiece.shape = currentPiece.shape[0].map((_, i) => currentPiece.shape.map(row => row[i])).reverse();
  if (isCollision()) {
    currentPiece.shape = temp;
    return false;
  }
  return true;
}

function isCollision() {
  return currentPiece.shape.some((row, y) => row.some((value, x) => {
    if (!value) return false;
    const boardX = currentPiece.x + x;
    const boardY = currentPiece.y + y;
    return (boardX < 0 || boardX >= gameBoardWidth || boardY < 0 || boardY >= gameBoardHeight || gameBoard[boardY][boardX]);
  }));
}

function movePieceDown() {
  currentPiece.y += 1;
  if (isCollision()) {
    currentPiece.y -= 1;
    mergePiece();
    currentPiece = {
      x: 0,
      y: 0,
      shape: pieces[Math.floor(Math.random() * pieces.length)]
    };
    if (isCollision()) {
      console.log("Game Over");
      clearInterval(gameLoop);
    }
  }
}

function clearLines() {
  outer: for (let y = gameBoardHeight - 1; y >= 0; y--) {
    for (let x = 0; x < gameBoardWidth; x++) {
      if (!gameBoard[y][x]) {
        continue outer;
      }
    }
    gameBoard.splice(y, 1);
    gameBoard.unshift(Array(gameBoardWidth).fill(0));
  }
}

function userInput(e) {
  switch (e.key) {
  case "ArrowLeft":
    movePiece(-1);
    break;
  case "ArrowRight":
    movePiece(1);
    break;
  case "ArrowUp":
    rotatePiece();
    break;
  case "ArrowDown":
    movePieceDown();
    break;
  }
  draw();
}

document.addEventListener("keydown", userInput);
requestAnimationFrame(gameLoop); 

</script> 
</body>
</html>

この記事が気に入ったらサポートをしてみませんか?