乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

テトリス

今回もJSでクソゲーを作ってみました


解説動画はこちら







クソゲー作ってみた

その名も
「ずっこんテトリス」
です

普通のテトリスを改変して
少し面白くなるようにしてみました
こちらからプレイできます。

ずっこんテトリス

上下左右キーの操作のみの
シンプル設計で

長いテトリス棒を追加しました
sample


こやつは「ずっこんテトリス棒」ですね

コードはこんな感じになっています。
const game = document.getElementById("game");
const rareChanceInput = document.getElementById("rareChance");
const chanceValueDisplay = document.getElementById("chanceValue");
const ROWS = 20;
const COLS = 10;

// グリッドを初期化
const grid = Array.from({ length: ROWS }, () => Array(COLS).fill(0));

// 通常のテトリミノ
const tetrominoes = [
  [[1, 1, 1, 1]], // I
  [
    [1, 1],
    [1, 1],
  ], // O
  [
    [0, 1, 0],
    [1, 1, 1],
  ], // T
  [
    [1, 1, 0],
    [0, 1, 1],
  ], // S
  [
    [0, 1, 1],
    [1, 1, 0],
  ], // Z
  [
    [1, 1, 1],
    [1, 0, 0],
  ], // L
  [
    [1, 1, 1],
    [0, 0, 1],
  ], // J
];

// レアな「ロング棒」長さを8に変更
const rareTetromino = [
  [[1, 1, 1, 1, 1, 1, 1, 1]], // 長さ8の棒
];

// テトリミノの状態
let currentTetromino = getRandomTetromino();
let currentRow = 0;
let currentCol = Math.floor((COLS - currentTetromino[0].length) / 2);

// グリッドを描画
function drawGrid() {
  game.innerHTML = "";
  for (let row = 0; row < ROWS; row++) {
    for (let col = 0; col < COLS; col++) {
      const cell = document.createElement("div");
      cell.classList.add("cell");
      if (grid[row][col] === 1) {
        cell.classList.add("filled");
      }
      game.appendChild(cell);
    }
  }
}

// テトリミノを描画
function drawTetromino() {
  currentTetromino.forEach((row, r) => {
    row.forEach((value, c) => {
      if (value && currentRow + r >= 0) {
        grid[currentRow + r][currentCol + c] = 1;
      }
    });
  });
}

// テトリミノを削除
function clearTetromino() {
  currentTetromino.forEach((row, r) => {
    row.forEach((value, c) => {
      if (value && currentRow + r >= 0) {
        grid[currentRow + r][currentCol + c] = 0;
      }
    });
  });
}

// 衝突判定
function isValidMove(newRow, newCol, newTetromino) {
  return newTetromino.every((row, r) =>
    row.every((value, c) => {
      const x = newCol + c;
      const y = newRow + r;
      return (
        !value ||
        (y >= 0 && y < ROWS && x >= 0 && x < COLS && grid[y][x] === 0)
      );
    })
  );
}

// ラインを削除
function clearLines() {
  for (let row = ROWS - 1; row >= 0; row--) {
    if (grid[row].every((cell) => cell === 1)) {
      grid.splice(row, 1);
      grid.unshift(Array(COLS).fill(0));
      row++;
    }
  }
}

// ランダムなテトリミノを取得(レア形状の低確率出現を含む)
function getRandomTetromino() {
  const rareChance = parseFloat(rareChanceInput.value); // スライダーの値を取得
  if (Math.random() < rareChance) { // ロング棒の出現確率
    return rareTetromino[0];
  } else {
    return tetrominoes[Math.floor(Math.random() * tetrominoes.length)];
  }
}

// テトリミノを回転
function rotateTetromino() {
  const newTetromino = currentTetromino[0].map((_, colIndex) =>
    currentTetromino.map((row) => row[colIndex]).reverse()
  );

  if (isValidMove(currentRow, currentCol, newTetromino)) {
    currentTetromino = newTetromino;
  }
}

// ゲームのループ
function gameLoop() {
  clearTetromino();
  if (isValidMove(currentRow + 1, currentCol, currentTetromino)) {
    currentRow++;
  } else {
    drawTetromino();
    clearLines();

    // 次のテトリミノを生成
    currentTetromino = getRandomTetromino();
    currentRow = 0;
    currentCol = Math.floor((COLS - currentTetromino[0].length) / 2);

    // ゲームオーバー判定
    if (!isValidMove(currentRow, currentCol, currentTetromino)) {
      alert("Game Over");
      grid.forEach((row) => row.fill(0));
      currentTetromino = getRandomTetromino();
      currentRow = 0;
      currentCol = Math.floor((COLS - currentTetromino[0].length) / 2);
    }
  }
  drawTetromino();
  drawGrid();
}

// キー操作
document.addEventListener("keydown", (e) => {
  clearTetromino();
  if (e.key === "ArrowLeft" && isValidMove(currentRow, currentCol - 1, currentTetromino)) {
    currentCol--;
  } else if (e.key === "ArrowRight" && isValidMove(currentRow, currentCol + 1, currentTetromino)) {
    currentCol++;
  } else if (e.key === "ArrowDown") {
    if (isValidMove(currentRow + 1, currentCol, currentTetromino)) {
      currentRow++;
    }
  } else if (e.key === "ArrowUp") {
    rotateTetromino();
  }
  drawTetromino();
  drawGrid();
});

// スライダーの値を表示
rareChanceInput.addEventListener("input", () => {
  chanceValueDisplay.textContent = parseFloat(rareChanceInput.value).toFixed(2);
});

// ゲーム開始
setInterval(gameLoop, 500);
drawGrid();


ここでレアなテトリス棒として定義し
出現確率を画面上のスライダーの値で設定しています。

ここを上げると、出現確率も上がります。

どの確率が一番面白くなるかは分かりませんので
色々変えて試していただければと思います。

コードも改変すると
面白くなるかもしれないので
色々遊んでみてください

それでは






 

プログラミング上達講座の
2回目です。

テトリスを題材にして
プログラムを考えてみましょう。

解説動画はこちら



テトリスは
4つのブロックで構成されたテトリミノを
フィールドに並べていくパズルゲームです。

先日のぷよぷよもそうですが
このようなパズルゲームは
プログラミングの題材には打って付け!!

早速考えてみましょう。

1.テトリスゲームの素材を描画してみよう

1.1
盤面(10 x 20マス)のフィールドと
上方以外を囲むブロック下記のような
フィールドをデータ化してみよう。

■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■□□□□□□□□□□■
■■■■■■■■■■■■

ヒント:

白黒のマスを1つずつ表示させるのは面倒臭いので
繰り返しを用いてみよう。

全体では12マスかける21行となる。



回答:

単純に上記になるように
データを作成して出力するだけ。

データをひとまとめにしておくと
再利用しやすくなります。

縦横の揃ったデータは
リスト型を用いると表現しやすく
管理しやすくなる

リストの掛け算は要素の繰り返し
足し算は要素の連結となります。

data = [['■'] + ['□']*10 + ['■']]*20 + [['■']*12]

for y in data:
    print(''.join(y))

この場合は文字として
データを出力しますが

パズルゲームの場合
文字列でデータを格納すると
あとあと面倒い事もあるので
数値で考えるケースが多いです。
data = [[1] + [0]*10 + [1]]*20 + [[1]*12]

for y in data:
    for x in y:
        print('■' if x==1 else '□',end='')
    print()

■なら1
□なら0としてデータを格納して
出力時に変換します。

これで結果は一緒になります。

1.2
テトリミノ(7つ)
□■□□ □■□□ □■□□ □□■□ □■□□ □□■□ □□□□
□■□□ □■■□ □■□□ □□■□ □■■□ □■■□ □■■□
□■□□ □■□□ □■■□ □■■□ □□■□ □■□□ □■■□
□■□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□

上記のような7種類のテトリミノを
データ化してみよう


回答:

こちらもゲームで用いる際には
数値や文字を値とするリスト型のような
縦横の個数が決まったデータ型を
用いると表現しやすくなります。

7つのテトリミノは独立して考え
回転などを考慮すると
縦横4x4マスのリスト型で定義するのが
再利用しやすい形でしょう。

line  = [[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]]
tblo = [[0,1,0,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]]
lr      = [[0,1,0,0],[0,1,0,0],[0,1,1,0],[0,0,0,0]]
ll       = [[0,0,1,0],[0,0,1,0],[0,1,1,0],[0,0,0,0]]
hr     = [[0,1,0,0],[0,1,1,0],[0,0,1,0],[0,0,0,0]]
hl      = [[0,0,1,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]]
sq     = [[0,0,0,0],[0,1,1,0],[0,1,1,0],[0,0,0,0]]

t_minos = [line,tblo,lr,ll,hr,hl,sq]

for mino in t_minos:
    for y in mino:
        for x in y:
            print('■' if x==1 else '□',end='')
        print()
    print()

□■□□
□■□□
□■□□
□■□□

□■□□
□■■□
□■□□
□□□□

□■□□
□■□□
□■■□
□□□□

□□■□
□□■□
□■■□
□□□□

□■□□
□■■□
□□■□
□□□□

□□■□
□■■□
□■□□
□□□□

□□□□
□■■□
□■■□
□□□□



2.
テトリミノを回転させる
関数rotateを作ってみよう

先ほど作ったデータを使って
90度ずつ回転を行う関数を作成しよう

def rotate(data , 角度):
   処理
   return data

□■□□
□■■□
□■□□
□□□□

これを90度回転させると

□□□□
□■■■
□□■□
□□□□






回答:

まずテトリミノのデータを用意しましょう。
data = [[0,1,0,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]]
print_block(data)
□■□□
□■■□
□■□□
□□□□


毎回描画コードを書くのは面倒なので関数化します。
def print_block(block):
    for y in block:
        for x in y:
            print('■' if x==1 else '□',end='')
        print()

1つ目:
[::-1] とzip関数を用いる方法

[::-1] を用いると元のリストの
浅いコピーを逆順で作成します

zip関数は各引数の先頭から1つの要素を
繰り返し抽出しTupleを作成します。

合わせると要素が90度回転したことになります。

data = [[0,1,0,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]]
print_block(data[::-1])
□□□□
□■□□
□■■□
□■□□

print_block(zip(*data[::-1]))
□□□□
□■■■
□□■□
□□□□

関数にまとめると

def rotate(data,num):
    tmp = data.copy()
    if num==0:
        return tmp
    rotated = list(map(list, zip(*tmp[::-1])))
    for i in range(num//90-1):
        rotated = list(map(list, zip(*rotated[::-1])))
    return rotated
​
data = [[0,1,0,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]]
block = rotate(data,270)
print_block(block)
□□□□
□■□□
■■■□
□□□□

2つ目:
numpyライブラリを用いる方法

numpy.rot90(データ , 回転数)

数値の配列を反時計回りに90度回転させる
回転数は0~を指定 , 1で90度回転
回転数を-の値にすると時計回り

import numpy as np

def rotate2(data,num):
    return np.rot90(data.copy(),-num//90)

data = [[0,1,0,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]]
block = rotate2(data,90)
print_block(block)

□□□□
□■■■
□□■□
□□□□

こちらは2行で出来てしまいますね。

データ構造の考え方と
データの操作方法は
パズルゲームがおすすめです。

今後もやっていきますので
色々解いてみて下さい。

それでは。


このページのトップヘ