ผลต่างระหว่างรุ่นของ "Se63/typescript/zombie"
Jittat (คุย | มีส่วนร่วม) |
Jittat (คุย | มีส่วนร่วม) |
||
(ไม่แสดง 17 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน) | |||
แถว 1: | แถว 1: | ||
== โค้ดเริ่มต้น == | == โค้ดเริ่มต้น == | ||
− | ใส่ใน jsfiddle | + | ใส่ใน https://playcode.io/ (ไม่ควรใช้ jsfiddle เพราะ jsfiddle ใช้ typescript รุ่นเก่า) |
ส่วน HTML | ส่วน HTML | ||
<syntaxhighlight lang="html"> | <syntaxhighlight lang="html"> | ||
<pre id="gameBoard"></pre> | <pre id="gameBoard"></pre> | ||
+ | <button onClick="gameBoard.step()">step</button> | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | แก้ css ใน style.css ลบการจัดหน้าทิ้ง | ||
+ | <syntaxhighlight lang="css"> | ||
+ | body { | ||
+ | background: white; /* try type yellow */ | ||
+ | color: #323232; | ||
+ | }</syntaxhighlight> | ||
ส่วน Typescript (เลือก Javascript เปลี่ยนภาษาเป็น Typescript) | ส่วน Typescript (เลือก Javascript เปลี่ยนภาษาเป็น Typescript) | ||
แถว 19: | แถว 27: | ||
this.botCount = 5; | this.botCount = 5; | ||
this.botXs = [10,20,30,40,50]; | this.botXs = [10,20,30,40,50]; | ||
− | this.botYs = [5,10,20,15,25 | + | this.botYs = [5,10,20,15,25]; |
} | } | ||
แถว 62: | แถว 70: | ||
ทำความเข้าใจกันก่อน | ทำความเข้าใจกันก่อน | ||
− | * <tt>gameBoard</tt> เป็น global | + | * <tt>gameBoard</tt> เป็น global ทำให้เมื่อเวลาเรากดปุ่ม step ในหน้าจอจะสามารถเรียก <tt>gameBoard.step()</tt> ให้เกมทำงานทีละขั้นได้ |
== clean showBoard กันก่อน == | == clean showBoard กันก่อน == | ||
แถว 191: | แถว 199: | ||
constructor(public x: number, public y: number) {} | constructor(public x: number, public y: number) {} | ||
− | |||
− | |||
getPieceChar(): string { return '*'; } | getPieceChar(): string { return '*'; } | ||
} | } | ||
แถว 208: | แถว 214: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | เพื่อจะวาด ชิ้นเกมต่าง ๆ เราจะใช้ method <tt>drawPiece</tt> ซึ่งแกะมาจากโค้ด showBoard (เขียนในคลาส GameBoard) | |
<syntaxhighlight lang="typescript"> | <syntaxhighlight lang="typescript"> | ||
+ | class GameBoard { | ||
+ | //... | ||
+ | |||
drawPiece(boardRows: char[][], piece: GamePiece) { | drawPiece(boardRows: char[][], piece: GamePiece) { | ||
− | boardRows[piece. | + | boardRows[piece.y][piece.x] = piece.getPieceChar(); |
} | } | ||
+ | |||
+ | //... | ||
+ | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | จากนั้นแก้เมท็อด showBoard ให้เรียกใช้ drawPiece โดยวาดของลงไปด้วยโค้ดเดียวกัน (สร้างอาร์เรย์ <tt>elements</tt> ขึ้นมาก่อน โดยรวม this.bots และ this.player | |
+ | |||
<syntaxhighlight lang="typescript"> | <syntaxhighlight lang="typescript"> | ||
+ | class GameBoard { | ||
+ | //... | ||
+ | |||
+ | showBoard() { | ||
+ | // ... | ||
+ | |||
let elements = [...this.bots, this.player]; | let elements = [...this.bots, this.player]; | ||
แถว 223: | แถว 242: | ||
board.drawPiece(boardRows, piece); | board.drawPiece(boardRows, piece); | ||
}); | }); | ||
+ | } | ||
+ | |||
+ | // ... | ||
+ | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | '''หมายเหตุ''': ในฟังก์ชันใน <tt>forEach</tt> เราใช้ this แทน gameBoard ไม่ได้ เพราะว่าจะมีการตีความเป็นอย่างอื่น เลยต้องสร้างตัวแปรพิเศษมาเก็บค่าไว้ | + | '''หมายเหตุ 1''': สังเกตการใช้ <tt>[...this.bots, this.player]</tt> ในการสร้าง array ที่นำค่ามาจาก this.bots จะพบใน Javascript สามารถทำได้กับ object ด้วย |
+ | |||
+ | '''หมายเหตุ 2''': ในฟังก์ชันใน <tt>forEach</tt> เราใช้ this แทน gameBoard ไม่ได้ เพราะว่าจะมีการตีความเป็นอย่างอื่น เลยต้องสร้างตัวแปรพิเศษมาเก็บค่าไว้ | ||
== move bots == | == move bots == | ||
แถว 306: | แถว 331: | ||
} | } | ||
} | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == สุ่มตำแหน่ง bot == | ||
+ | เพื่อความสะดวกในการเทสขั้นตอนถัด ๆ ไป เราจะเพิ่มการสุ่มตำแหน่งเริ่มต้นให้ zombie bot | ||
+ | |||
+ | '''งานของคุณ''' เขียนเมทอด randomBots สุ่มเพิ่ม bot จำนวน n ตัว (สามารถใช้ <tt>Math.random</tt> ได้ [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random อ่านเอกสาร]) | ||
+ | <syntaxhighlight lang="typescript"> | ||
+ | class GameBoard { | ||
+ | // .. | ||
+ | |||
+ | randomBots(n: number) { | ||
+ | // ** YOUR JOB HERE ** | ||
+ | } | ||
+ | |||
+ | // .. | ||
+ | } | ||
+ | |||
+ | gameBoard = new GameBoard(); | ||
+ | gameBoard.randomBots(5); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
== สถานะเป็นตาย และการชนกัน == | == สถานะเป็นตาย และการชนกัน == | ||
+ | |||
+ | เราจะเพิ่ม property <tt>isAlive</tt> ให้กับ bot นอกจากนี้เราจะแก้ให้ moveTo ทำงานก็ต่อเมื่อ this.isAlive เป็นจริง | ||
+ | |||
+ | โค้ดด้านล่าง สังเกตว่าเราจะกำหนดค่า this.isAlive ให้เป็นจริงใน constructure เราเรียก <tt>super</tt> เพื่อเรียก constructor ของ superclass GamePiece | ||
<syntaxhighlight lang="typescript"> | <syntaxhighlight lang="typescript"> | ||
แถว 319: | แถว 367: | ||
} | } | ||
+ | // ... | ||
+ | |||
+ | moveTo(p: Player) { | ||
+ | if (this.isAlive) { | ||
+ | // ... (your old code) | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | เราสามารถแก้ให้ bot ถ้าตายแสดงสัญลักษณ์เป็น # แทนที่จะเป็น B ได้ด้วย โดยแก้ <tt>getPieceChar</tt> | ||
+ | |||
+ | <syntaxhighlight lang="typescript"> | ||
getPieceChar(): string { | getPieceChar(): string { | ||
if (this.isAlive) { | if (this.isAlive) { | ||
แถว 326: | แถว 387: | ||
} | } | ||
} | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | เราจะเพิ่มเมทอด <tt>isHit</tt> ให้กับ GamePiece ดังนี้ (เมื่อเพิ่มแล้วทั้ง Bot และ Player จะใช้ได้ด้วย) | ||
+ | <syntaxhighlight lang="typescript"> | ||
+ | class GamePiece { | ||
// ... | // ... | ||
− | + | isHit(other: GamePiece): boolean { | |
− | + | return ((this.x == other.x) && | |
− | + | (this.y == other.y)); | |
− | |||
} | } | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | เราจะใช้ isHit ในการตรวจสอบการวิ่งชนกัน และปรับค่า isAlive ให้กับ bot นั่นคือถ้า bot วิ่งชนกันก็จะตาย และถ้า bot ตัวอื่นมาชนช่องนี้อีกก็จะตายไปด้วย | ||
+ | |||
+ | '''งานของคุณ:''' เขียนเมทอด <tt>checkHit</tt> ที่ตรวจสอบว่า bot อยู่ในตำแหน่งเดียวกันหรือไม่ ถ้าอยู่ช่องเดียวกันก็ให้ปรับค่า isAlive เป็นเท็จไปเลย เราจะเรียกฟังก์ชันนี้หลังการเดินของ bot | ||
<syntaxhighlight lang="typescript"> | <syntaxhighlight lang="typescript"> | ||
− | + | class GameBoard { | |
+ | // ... | ||
+ | |||
+ | checkHit() { | ||
+ | // ** YOUR JOB HERE ** | ||
+ | } | ||
− | + | step() { | |
+ | this.moveBots(); | ||
+ | this.checkHit(); | ||
+ | this.showBoard(); | ||
+ | } | ||
+ | |||
+ | // ... | ||
+ | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
แถว 350: | แถว 430: | ||
== Bots รูปแบบอื่น ๆ == | == Bots รูปแบบอื่น ๆ == | ||
+ | |||
+ | === BotFactory === |
รุ่นแก้ไขปัจจุบันเมื่อ 07:11, 13 กรกฎาคม 2563
เนื้อหา
โค้ดเริ่มต้น
ใส่ใน 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 รูปแบบอื่น ๆ จะทำอย่างไร?)