テトリス風ゲーム ソースプログラム

まとめてダウンロードするならこちらから。

Githubにも上げてありますが、

https://github.com/kmaebashi/jstetris

別段動かしたいわけでもないけれど、ソースをざっと眺めたい、という人は以下を参照してください。

生のES6で記述しています。jQueryとかReact.jsとかVue.jsとか一切使っていません。 画面描画にはCanvasを使用しています。

HTML

ゲームのステージになるCanvasと、Next表示用のCanvasを配置しています。

39~40行目がゲーム開始の起点となりますので、ここからtetris.jsを読んでいくとよいかと。

./tetris.html

  1: <!DOCTYPE html>
  2: <html>
  3:   <head>
  4:     <meta charset="UTF-8">
  5:     <title>テトリス風ゲーム</title>
  6:     <meta name="viewport" content="width=device-width,initial-scale=1">
  7:     <link rel="stylesheet" type="text/css" href="css/tetris.css">
  8:     <script type="text/javascript" src="js/tetris.js"></script>
  9:   </head>
 10:   <body>
 11:     <h1>テトリス風ゲーム</h1>
 12:     <div class="wrapper-container">
 13:         <span class="tetris-container">
 14:             <canvas id="stage" width="250px" height="500px" style="background-color:black;">
 15:             </canvas>
 16:             <span class="tetris-panel-container">
 17:               <p>Next:</p>
 18:                 <canvas id="next" width="150px" height="150px" style="background-color:black;">
 19:                 </canvas>
 20:               <p>LINES:<span id="lines">0</span></p>
 21:               <p><span id="message"></span></p>
 22:               <div class="tetris-panel-container-padding"></div>
 23:               <table class="tetris-button-panel">
 24:                 <tr>
 25:                   <td></td>
 26:                   <td id="tetris-rotate-button" class="tetris-button">↻</td>
 27:                   <td></td>
 28:                 </tr>
 29:                 <tr>
 30:                   <td id="tetris-move-left-button"class="tetris-button">←</td>
 31:                   <td id="tetris-fall-button"class="tetris-button">↓</td>
 32:                   <td id="tetris-move-right-button"class="tetris-button">→</td>
 33:                 </tr>
 34:               </table>
 35:             </span>
 36:         </span>
 37:     </div>
 38:     <script>
 39:       var tetris = new Tetris();
 40:       tetris.startGame();
 41:     </script>
 42:   </body>
 43: </html>

CSS

CSSです。Flexboxを初めて使った。

./css/tetris.css

  1: html {
  2:     touch-action: manipulation;
  3: }
  4: 
  5: .wrapper-container {
  6:     display: inline-block;
  7: }
  8: 
  9: .tetris-container {
 10:     display: flex;
 11:     flex-direction: row;
 12:     margin: 10px;
 13:     background-color: #333333;
 14: }
 15: 
 16: .tetris-panel-container {
 17:     display: flex;
 18:     padding-left: 10px;
 19:     padding-right: 10px;
 20:     flex-direction: column;
 21:     color: white;
 22:     background-color: #333333;
 23: }
 24: 
 25: .tetris-panel-container-padding {
 26:     flex-grow: 1;
 27: }
 28: 
 29: .tetris-panel-container p {
 30:    margin: 0;
 31:    padding: 0;
 32: }
 33: 
 34: .tetris-button-panel {
 35:     border-style: none;
 36:     width: 100%;
 37: }
 38: 
 39: .tetris-button {
 40:     padding-top: 10px;
 41:     padding-bottom: 10px;
 42:     text-align: center;
 43:     background: #444444;
 44: }

JavaScript

これが本体です。

ソースを読むにあたり気を付けるところがあるとすれば、 ブロックの形状は色を含め41行目からのcreateBlocks()で定義していること、 および仮想画面virtualStageが現在のステージの状態を示していますが、 この2次元配列に入るのは最後まで落ち切ってfixBlock()が呼ばれたブロックのみである、 ということぐらいでしょうか。

./js/tetris.html

  1: class Tetris {
  2:     constructor() {
  3:         this.stageWidth = 10;
  4:         this.stageHeight = 20;
  5:         this.stageCanvas = document.getElementById("stage");
  6:         this.nextCanvas = document.getElementById("next");
  7:         let cellWidth = this.stageCanvas.width / this.stageWidth;
  8:         let cellHeight = this.stageCanvas.height / this.stageHeight;
  9:         this.cellSize = cellWidth < cellHeight ? cellWidth : cellHeight;
 10:         this.stageLeftPadding = (this.stageCanvas.width - this.cellSize * this.stageWidth) / 2;
 11:         this.stageTopPadding = (this.stageCanvas.height - this.cellSize * this.stageHeight) / 2;
 12:         this.blocks = this.createBlocks();
 13:         this.deletedLines = 0;
 14: 
 15:         window.onkeydown = (e) => {
 16:             if (e.keyCode === 37) {
 17:                 this.moveLeft();
 18:             } else if (e.keyCode === 38) {
 19:                 this.rotate();
 20:             } else if (e.keyCode === 39) {
 21:                 this.moveRight();
 22:             } else if (e.keyCode === 40) {
 23:                 this.fall();
 24:             }
 25:         }
 26: 
 27:         document.getElementById("tetris-move-left-button").onmousedown = (e) => {
 28:             this.moveLeft();
 29:         }
 30:         document.getElementById("tetris-rotate-button").onmousedown = (e) => {
 31:             this.rotate();
 32:         }
 33:         document.getElementById("tetris-move-right-button").onmousedown = (e) => {
 34:             this.moveRight();
 35:         }
 36:         document.getElementById("tetris-fall-button").onmousedown = (e) => {
 37:             this.fall();
 38:         }
 39:     }
 40: 
 41:     createBlocks() {
 42:         let blocks = [
 43:             {
 44:                 shape: [[[-1, 0], [0, 0], [1, 0], [2, 0]],
 45:                         [[0, -1], [0, 0], [0, 1], [0, 2]],
 46:                         [[-1, 0], [0, 0], [1, 0], [2, 0]],
 47:                         [[0, -1], [0, 0], [0, 1], [0, 2]]],
 48:                 color: "rgb(0, 255, 255)",
 49:                 highlight: "rgb(255, 255, 255)",
 50:                 shadow: "rgb(0, 128, 128)"
 51:             },
 52:             {
 53:                 shape: [[[0, 0], [1, 0], [0, 1], [1, 1]],
 54:                         [[0, 0], [1, 0], [0, 1], [1, 1]],
 55:                         [[0, 0], [1, 0], [0, 1], [1, 1]],
 56:                         [[0, 0], [1, 0], [0, 1], [1, 1]]],
 57:                 color: "rgb(255, 255, 0)",
 58:                 highlight: "rgb(255, 255, 255)",
 59:                 shadow: "rgb(128, 128, 0)"
 60:             },
 61:             {
 62:                 shape: [[[0, 0], [1, 0], [-1, 1], [0, 1]],
 63:                         [[-1, -1], [-1, 0], [0, 0], [0, 1]],
 64:                         [[0, 0], [1, 0], [-1, 1], [0, 1]],
 65:                         [[-1, -1], [-1, 0], [0, 0], [0, 1]]],
 66:                 color: "rgb(0, 255, 0)",
 67:                 highlight: "rgb(255, 255, 255)",
 68:                 shadow: "rgb(0, 128, 0)"
 69:             },
 70:             {
 71:                 shape: [[[-1, 0], [0, 0], [0, 1], [1, 1]],
 72:                         [[0, -1], [-1, 0], [0, 0], [-1, 1]],
 73:                         [[-1, 0], [0, 0], [0, 1], [1, 1]],
 74:                         [[0, -1], [-1, 0], [0, 0], [-1, 1]]],
 75:                 color: "rgb(255, 0, 0)",
 76:                 highlight: "rgb(255, 255, 255)",
 77:                 shadow: "rgb(128, 0, 0)"
 78:             },
 79:             {
 80:                 shape: [[[-1, -1], [-1, 0], [0, 0], [1, 0]],
 81:                         [[0, -1], [1, -1], [0, 0], [0, 1]],
 82:                         [[-1, 0], [0, 0], [1, 0], [1, 1]],
 83:                         [[0, -1], [0, 0], [-1, 1], [0, 1]]],
 84:                 color: "rgb(0, 0, 255)",
 85:                 highlight: "rgb(255, 255, 255)",
 86:                 shadow: "rgb(0, 0, 128)"
 87:             },
 88:             {
 89:                 shape: [[[1, -1], [-1, 0], [0, 0], [1, 0]],
 90:                         [[0, -1], [0, 0], [0, 1], [1, 1]],
 91:                         [[-1, 0], [0, 0], [1, 0], [-1, 1]],
 92:                         [[-1, -1], [0, -1], [0, 0], [0, 1]]],
 93:                 color: "rgb(255, 165, 0)",
 94:                 highlight: "rgb(255, 255, 255)",
 95:                 shadow: "rgb(128, 82, 0)"
 96:             },
 97:             {
 98:                 shape: [[[0, -1], [-1, 0], [0, 0], [1, 0]],
 99:                         [[0, -1], [0, 0], [1, 0], [0, 1]],
100:                         [[-1, 0], [0, 0], [1, 0], [0, 1]],
101:                         [[0, -1], [-1, 0], [0, 0], [0, 1]]],
102:                 color: "rgb(255, 0, 255)",
103:                 highlight: "rgb(255, 255, 255)",
104:                 shadow: "rgb(128, 0, 128)"
105:             }
106:         ];
107:         return blocks;
108:     }
109: 
110:     drawBlock(x, y, type, angle, canvas) {
111:         let context = canvas.getContext("2d");
112:         let block = this.blocks[type];
113:         for (let i = 0; i < block.shape[angle].length; i++) {
114:             this.drawCell(context,
115:                      x + (block.shape[angle][i][0] * this.cellSize),
116:                      y + (block.shape[angle][i][1] * this.cellSize),
117:                      this.cellSize,
118:                      type);
119:         }
120:     }
121: 
122:     drawCell(context, cellX, cellY, cellSize, type) {
123:         let block = this.blocks[type];
124:         let adjustedX = cellX + 0.5;
125:         let adjustedY = cellY + 0.5;
126:         let adjustedSize = cellSize - 1;
127:         context.fillStyle = block.color;
128:         context.fillRect(adjustedX, adjustedY, adjustedSize, adjustedSize);
129:         context.strokeStyle = block.highlight;
130:         context.beginPath();
131:         context.moveTo(adjustedX, adjustedY + adjustedSize);
132:         context.lineTo(adjustedX, adjustedY);
133:         context.lineTo(adjustedX + adjustedSize, adjustedY);
134:         context.stroke();
135:         context.strokeStyle = block.shadow;
136:         context.beginPath();
137:         context.moveTo(adjustedX, adjustedY + adjustedSize);
138:         context.lineTo(adjustedX + adjustedSize, adjustedY + adjustedSize);
139:         context.lineTo(adjustedX + adjustedSize, adjustedY);
140:         context.stroke();
141:     }
142: 
143:     startGame() {
144:         let virtualStage = new Array(this.stageWidth);
145:         for (let i = 0; i < this.stageWidth; i++) {
146:             virtualStage[i] = new Array(this.stageHeight).fill(null);
147:         }
148:         this.virtualStage = virtualStage;
149:         this.currentBlock = null;
150:         this.nextBlock = this.getRandomBlock();
151:         this.mainLoop();
152:     }
153: 
154:     mainLoop() {
155:         if (this.currentBlock == null) {
156:             if (!this.createNewBlock()) {
157:                 return;
158:             }
159:         } else {
160:             this.fallBlock();
161:         }
162:         this.drawStage();
163:         if (this.currentBlock != null) {
164:             this.drawBlock(this.stageLeftPadding + this.blockX * this.cellSize,
165:                 this.stageTopPadding + this.blockY * this.cellSize,
166:                 this.currentBlock, this.blockAngle, this.stageCanvas);
167:         }
168:         setTimeout(this.mainLoop.bind(this), 500);
169:     }
170: 
171:     createNewBlock() {
172:         this.currentBlock = this.nextBlock;
173:         this.nextBlock = this.getRandomBlock();
174:         this.blockX = Math.floor(this.stageWidth / 2 - 2);
175:         this.blockY = 0;
176:         this.blockAngle = 0;
177:         this.drawNextBlock();
178:         if (!this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, this.blockAngle)) {
179:             let messageElem = document.getElementById("message");
180:             messageElem.innerText = "GAME OVER";
181:             return false;
182:         }
183:         return true;
184:     }
185: 
186:     drawNextBlock() {
187:         this.clear(this.nextCanvas);
188:         this.drawBlock(this.cellSize * 2, this.cellSize, this.nextBlock,
189:             0, this.nextCanvas);
190:     }
191: 
192:     getRandomBlock() {
193:         return  Math.floor(Math.random() * 7);
194:     }
195: 
196:     fallBlock() {
197:         if (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) {
198:             this.blockY++;
199:         } else {
200:             this.fixBlock(this.blockX, this.blockY, this.currentBlock, this.blockAngle);
201:             this.currentBlock = null;
202:         }
203:     }
204: 
205:     checkBlockMove(x, y, type, angle) {
206:         for (let i = 0; i < this.blocks[type].shape[angle].length; i++) {
207:             let cellX = x + this.blocks[type].shape[angle][i][0];
208:             let cellY = y + this.blocks[type].shape[angle][i][1];
209:             if (cellX < 0 || cellX > this.stageWidth - 1) {
210:                 return false;
211:             }
212:             if (cellY > this.stageHeight - 1) {
213:                 return false;
214:             }
215:             if (this.virtualStage[cellX][cellY] != null) {
216:                 return false;
217:             }
218:         }
219:         return true;
220:     }
221: 
222:     fixBlock(x, y, type, angle) {
223:         for (let i = 0; i < this.blocks[type].shape[angle].length; i++) {
224:             let cellX = x + this.blocks[type].shape[angle][i][0];
225:             let cellY = y + this.blocks[type].shape[angle][i][1];
226:             if (cellY >= 0) {
227:                 this.virtualStage[cellX][cellY] = type;
228:             }
229:         }
230:         for (let y = this.stageHeight - 1; y >= 0; ) {
231:             let filled = true;
232:             for (let x = 0; x < this.stageWidth; x++) {
233:                 if (this.virtualStage[x][y] == null) {
234:                     filled = false;
235:                     break;
236:                 }
237:             }
238:             if (filled) {
239:                 for (let y2 = y; y2 > 0; y2--) {
240:                     for (let x = 0; x < this.stageWidth; x++) {
241:                         this.virtualStage[x][y2] = this.virtualStage[x][y2 - 1];
242:                     }
243:                 }
244:                 for (let x = 0; x < this.stageWidth; x++) {
245:                     this.virtualStage[x][0] = null;
246:                 }
247:             let linesElem = document.getElementById("lines");
248:                 this.deletedLines++;
249:                 linesElem.innerText = "" + this.deletedLines;
250:             } else {
251:                 y--;
252:             }
253:         }
254:     }
255: 
256:     drawStage() {
257:         this.clear(this.stageCanvas);
258: 
259:         let context = this.stageCanvas.getContext("2d");
260:         for (let x = 0; x < this.virtualStage.length; x++) {
261:             for (let y = 0; y < this.virtualStage[x].length; y++) {
262:                 if (this.virtualStage[x][y] != null) {
263:                     this.drawCell(context,
264:                         this.stageLeftPadding + (x * this.cellSize),
265:                         this.stageTopPadding + (y * this.cellSize),
266:                         this.cellSize,
267:                         this.virtualStage[x][y]);
268:                 }
269:             }
270:         }
271:     }
272: 
273:     moveLeft() {
274:         if (this.checkBlockMove(this.blockX - 1, this.blockY, this.currentBlock, this.blockAngle)) {
275:             this.blockX--;
276:             this.refreshStage();
277:         }
278:     }
279: 
280:     moveRight() {
281:         if (this.checkBlockMove(this.blockX + 1, this.blockY, this.currentBlock, this.blockAngle)) {
282:             this.blockX++;
283:             this.refreshStage();
284:         }
285:     }
286: 
287:     rotate() {
288:         let newAngle;
289:         if (this.blockAngle < 3) {
290:             newAngle = this.blockAngle + 1;
291:         } else {
292:             newAngle = 0;
293:         }
294:         if (this.checkBlockMove(this.blockX, this.blockY, this.currentBlock, newAngle)) {
295:             this.blockAngle = newAngle;
296:             this.refreshStage();
297:         }
298:     }
299: 
300:     fall() {
301:         while (this.checkBlockMove(this.blockX, this.blockY + 1, this.currentBlock, this.blockAngle)) {
302:             this.blockY++;
303:             this.refreshStage();
304:         }
305:     }
306: 
307:     refreshStage() {
308:         this.clear(this.stageCanvas);
309:         this.drawStage();
310:         this.drawBlock(this.stageLeftPadding + this.blockX * this.cellSize,
311:                 this.stageTopPadding + this.blockY * this.cellSize,
312:                 this.currentBlock, this.blockAngle, this.stageCanvas);
313:     }
314: 
315:     clear(canvas) {
316:         let context = canvas.getContext("2d");
317:         context.fillStyle = "rgb(0, 0, 0)";
318:         context.fillRect(0, 0, canvas.width, canvas.height);
319:     }
320: }

やってみて勉強になったこと


ひとつ上のページへ戻る | トップページへ戻る