Se63/typescript/zombie
เนื้อหา
โค้ดเริ่มต้น
ใส่ใน https://playcode.io/ (ไม่ควรใช้ jsfiddle เพราะ jsfiddle ใช้ typescript รุ่นเก่า)
ส่วน HTML
<pre id="gameBoard"></pre>
<button onClick="gameBoard.step()">step</button>
แก้ css ใน style.css ลบการจัดหน้าทิ้ง
body {
background: white; /* try type yellow */
color: #323232;
}
ส่วน Typescript (เลือก Javascript เปลี่ยนภาษาเป็น Typescript)
const boardHeight = 30;
const boardWidth = 50;
class GameBoard {
constructor() {
this.pX = 25;
this.pY = 15;
this.botCount = 5;
this.botXs = [10,20,30,40,50];
this.botYs = [5,10,20,15,25];
}
showBoard() {
let boardRows = [];
for (let i = 0; i < boardHeight; i++) {
let rowChars = [];
for (let j = 0; j < boardWidth; j++) {
rowChars.push(' ');
}
boardRows.push(rowChars);
}
boardRows[this.pY][this.pX] = '@';
for (let i = 0; i < this.botCount; i++) {
boardRows[this.botYs[i]][this.botXs[i]] = 'B';
}
let boardStr = boardRows.map((row) => {
return row.join('') + "\n";
}).join('');
let boardElement = document.getElementById('gameBoard');
boardElement.innerHTML = boardStr;
}
moveBots() {
for (let i=0; i < this.botCount; i++) {
this.botXs[i]--;
}
}
step() {
this.moveBots();
this.showBoard();
}
}
gameBoard = new GameBoard();
gameBoard.showBoard();
ทำความเข้าใจกันก่อน
- gameBoard เป็น global ทำให้เมื่อเวลาเรากดปุ่ม step ในหน้าจอจะสามารถเรียก gameBoard.step() ให้เกมทำงานทีละขั้นได้
clean showBoard กันก่อน
เมทอด showBoard นั้นค่อนข้างยาวและอ่านยาก เราจะแยกเป็น method ย่อย ๆ โดยเราจะแยกเป็น initBoardArray, boardArrayToStr กับ showBoardArray
initBoardArray(): char[][] {
let boardRows = [];
for (let i = 0; i < boardHeight; i++) {
let rowChars = [];
for (let j = 0; j < boardWidth; j++) {
rowChars.push(' ');
}
boardRows.push(rowChars);
}
return boardRows;
}
boardArrayToStr(boardRows: char[][]): string {
return boardRows.map((row) => {
return row.join('') + "\n";
}).join('');
}
showBoardArray(boardRows: char[][]) {
let boardElement = document.getElementById('gameBoard');
boardElement.innerHTML = this.boardArrayToStr(boardRows);;
}
showBoard() {
let boardRows = this.initBoardArray();
boardRows[this.pY][this.pX] = '@';
for (let i = 0; i < this.botCount; i++) {
boardRows[this.botYs[i]][this.botXs[i]] = 'B';
}
this.showBoardArray(boardRows);
}
แกะ player และ bots
เราจะทำให้โค้ดอ่านง่ายขึ้นและแก้ไขได้ง่ายขึ้น ในแนวคิดแบบ object-oriented เราจะพยายามจับกลุ่ม "ของ" ที่อยู่ด้วยกันบ่อย ๆ ให้กลายเป็นชิ้นขึ้นมา (เป็นวัตถุ) สังเกตว่าในระบบของเรามีข้อมูลที่ต้องอยู่ด้วยกันอยู่สองชุดคือ
ตำแหน่งของ bot และ
this.botXs = [10,20,30,40,50];
this.botYs = [5,10,20,15,25,12]
ตำแหน่งของผู้เล่น
this.pX = 25;
this.pY = 15;
และทั้งสองกลุ่มมีการทำงานใกล้เคียงกันเมื่อพิจารณาในส่วนของการแสดงข้อมูล เราจะสร้างคลาส Bot และ Player เพื่อเก็บตำแหน่งของ bot และ player ไว้ดังนี้ (เพิ่มไว้ก่อนคลาส GameBoard)
class Player {
constructor(public x: number, public y: number) {}
}
class Bot {
constructor(public x: number, public y: number) {}
}
เราจะสร้าง player ใน constructor ของ GameBoard (อีกหน่อยเราจะ inject เข้ามาได้) ส่วน bot เราจะเรียกเมทอด addBot
แก้ constructor และเพิ่ม addBot ดังนี้
class GameBoard {
bots: Bot[];
constructor() {
this.player = new Player(25,15);
this.botCount = 0;
this.bots = [];
}
addBot(b: Bot) {
this.bots.push(b);
this.botCount++;
}
// ...
}
จากนั้นแก้ส่วน showBoard
showBoard() {
// ...
boardRows[this.player.y][this.player.x] = '@';
for (let i = 0; i < this.botCount; i++) {
boardRows[this.bots[i].y][this.bots[i].x] = 'B';
}
// ...
}
เพิ่ม bot เข้าไปหลังจากสร้าง GameBoard
gameBoard = new GameBoard();
gameBoard.addBot(new Bot(10,5));
gameBoard.addBot(new Bot(20,10));
gameBoard.addBot(new Bot(30,20));
gameBoard.addBot(new Bot(40,15));
gameBoard.addBot(new Bot(50,12));
สังเกตว่าส่วน showBoard โค้ดยังดูซ้ำ ๆ กันอยู่ ขั้นตอนไปเราจะสร้าง abstraction ไปอีกชั้นหนึ่ง เพื่อให้ครอบทั้ง player และ bot
GamePiece
สร้างคลาส GamePiece
class GamePiece {
constructor(public x: number, public y: number) {}
getPieceChar(): string { return '*'; }
}
ปรับ Player และ Bot
class Player extends GamePiece {
getPieceChar(): string { return '@'; }
}
class Bot extends GamePiece {
getPieceChar(): string { return 'B'; }
}
เพื่อจะวาด ชิ้นเกมต่าง ๆ เราจะใช้ method drawPiece ซึ่งแกะมาจากโค้ด showBoard (เขียนในคลาส GameBoard)
class GameBoard {
//...
drawPiece(boardRows: char[][], piece: GamePiece) {
boardRows[piece.y][piece.x] = piece.getPieceChar();
}
//...
}
จากนั้นแก้เมท็อด showBoard ให้เรียกใช้ drawPiece โดยวาดของลงไปด้วยโค้ดเดียวกัน (สร้างอาร์เรย์ elements ขึ้นมาก่อน โดยรวม this.bots และ this.player
class GameBoard {
//...
showBoard() {
// ...
let elements = [...this.bots, this.player];
let board = this;
elements.forEach((piece) => {
board.drawPiece(boardRows, piece);
});
}
// ...
}
หมายเหตุ 1: สังเกตการใช้ [...this.bots, this.player] ในการสร้าง array ที่นำค่ามาจาก this.bots จะพบใน Javascript สามารถทำได้กับ object ด้วย
หมายเหตุ 2: ในฟังก์ชันใน forEach เราใช้ this แทน gameBoard ไม่ได้ เพราะว่าจะมีการตีความเป็นอย่างอื่น เลยต้องสร้างตัวแปรพิเศษมาเก็บค่าไว้
move bots
เราทิ้งโค้ดเมทอด moveBots ไว้ เป็นโค้ดที่เรียกแล้วจะมีปัญหา (เพราะว่ายังใช้ botXs อยู่เลย) เราจะเขียนเมท็อดในการเดินไว้ในคลาส Bot เลย ดังนี้
class Bot extends GamePiece {
getPieceChar(): string { return 'B'; }
move() {
this.x--;
}
}
จากนั้นไปแก้เมทอด moveBot ใน GameBoard ให้เรียกใช้เมทอดดังกล่าว
moveBots() {
for (let i=0; i < this.botCount; i++) {
this.bots[i].move();
}
}
จริง ๆ zombie จะเดินเข้าหา player ดังนั้นจริง ๆ แล้ว การ move ควรจะต้องขึ้นกับตำแหน่งของ player ด้วย เราจะแก้เมทอด moveBots ให้เป็นดังนี้
moveBots() {
for (let i=0; i < this.botCount; i++) {
this.bots[i].moveTo(this.player);
}
}
ให้เปลี่ยนชื่อเมทอด move เป็น moveTo (ปรับให้รับ player ด้วย)
งานของคุณ: ให้แก้ให้ bot วิ่งเข้าหา player โดยวิ่งได้ 4 ทิศเท่านั้น แก้ฟังก์ชัน moveTo ในคลาส Bot
class Bot extends GamePiece {
// ...
moveTo(p: Player) {
// *** YOUR JOB HERE ***
}
}
พิเศษ: SuperBot
งานพิเศษ: ให้เพิ่มคลาส SuperBot ที่เดินหาผู้ใช้ได้โดยเดินได้ 8 ทิศ (เดินแทยงได้ด้วย) แล้วเปลี่ยนบาง bot เป็น SuperBot
interface
เพิ่มปุ่ม ใน HTML
<pre id="gameBoard"></pre>
<br />
<button onclick="gameBoard.movePlayer('left');">H</button>
<button onclick="gameBoard.movePlayer('up');">J</button>
<button onclick="gameBoard.movePlayer('down');">K</button>
<button onclick="gameBoard.movePlayer('right');">L</button>
เพิ่ม method movePlayer ใน GameBoard
movePlayer(direction: string) {
this.player.move(direction);
this.step();
}
งานของคุณ: เขียนเมทอด move ใน Player
class Player extends GamePiece {
// ...
move(direction: string) {
// *** YOUR JOB ***
}
}
สุ่มตำแหน่ง bot
เพื่อความสะดวกในการเทสขั้นตอนถัด ๆ ไป เราจะเพิ่มการสุ่มตำแหน่งเริ่มต้นให้ zombie bot
งานของคุณ เขียนเมทอด randomBots สุ่มเพิ่ม bot จำนวน n ตัว (สามารถใช้ Math.random ได้ อ่านเอกสาร)
class GameBoard {
// ..
randomBots(n: number) {
// ** YOUR JOB HERE **
}
// ..
}
gameBoard = new GameBoard();
gameBoard.randomBots(5);
สถานะเป็นตาย และการชนกัน
เราจะเพิ่ม property isAlive ให้กับ bot นอกจากนี้เราจะแก้ให้ moveTo ทำงานก็ต่อเมื่อ this.isAlive เป็นจริง
โค้ดด้านล่าง สังเกตว่าเราจะกำหนดค่า this.isAlive ให้เป็นจริงใน constructure เราเรียก super เพื่อเรียก constructor ของ superclass GamePiece
class Bot extends GamePiece {
isAlive: boolean;
constructor(public x: number, public y: number) {
super(x,y);
this.isAlive = true;
}
// ...
moveTo(p: Player) {
if (this.isAlive) {
// ... (your old code)
}
}
}
เราสามารถแก้ให้ bot ถ้าตายแสดงสัญลักษณ์เป็น # แทนที่จะเป็น B ได้ด้วย โดยแก้ getPieceChar
getPieceChar(): string {
if (this.isAlive) {
return 'B';
} else {
return '#';
}
}
เราจะเพิ่มเมทอด isHit ให้กับ GamePiece ดังนี้ (เมื่อเพิ่มแล้วทั้ง Bot และ Player จะใช้ได้ด้วย)
class GamePiece {
// ...
isHit(other: GamePiece): boolean {
return ((this.x == other.x) &&
(this.y == other.y));
}
}
เราจะใช้ isHit ในการตรวจสอบการวิ่งชนกัน และปรับค่า isAlive ให้กับ bot นั่นคือถ้า bot วิ่งชนกันก็จะตาย และถ้า bot ตัวอื่นมาชนช่องนี้อีกก็จะตายไปด้วย
งานของคุณ: เขียนเมทอด checkHit ที่ตรวจสอบว่า bot อยู่ในตำแหน่งเดียวกันหรือไม่ ถ้าอยู่ช่องเดียวกันก็ให้ปรับค่า isAlive เป็นเท็จไปเลย เราจะเรียกฟังก์ชันนี้หลังการเดินของ bot
class GameBoard {
// ...
checkHit() {
// ** YOUR JOB HERE **
}
step() {
this.moveBots();
this.checkHit();
this.showBoard();
}
// ...
}
Discussions
- การตรวจสอบ isAlive ควรอยู่ใน bot หรืออยู่ที่ gameboard
- โค้ดตรวสอบ isAlive ซ้ำกันใน bot กับ superbot (และถ้ามี bot รูปแบบอื่น ๆ จะทำอย่างไร?)