ผลต่างระหว่างรุ่นของ "Se63/typescript/zombie"

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
 
(ไม่แสดง 18 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน)
แถว 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,12]
+
     this.botYs = [5,10,20,15,25];
 
   }
 
   }
  
แถว 62: แถว 70:
  
 
ทำความเข้าใจกันก่อน
 
ทำความเข้าใจกันก่อน
* <tt>gameBoard</tt> เป็น global ทำให้เราเรียก <tt>gameBoard.step()</tt> ใน console ให้เกมทำงานทีละขั้นได้
+
* <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) {}
  
  getX(): number { return this.x; }
 
  getY(): number { return this.y; }
 
 
   getPieceChar(): string { return '*'; }
 
   getPieceChar(): string { return '*'; }
 
}
 
}
แถว 208: แถว 214:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
แกะ method <tt>drawPiece</tt>
+
เพื่อจะวาด ชิ้นเกมต่าง ๆ เราจะใช้ 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.getY()][piece.getX()] = piece.getPieceChar();
+
     boardRows[piece.y][piece.x] = piece.getPieceChar();
 
   }
 
   }
 +
 +
  //...
 +
}
 
</syntaxhighlight>
 
</syntaxhighlight>
  
แล้ววาดของลงไปด้วยโค้ดเดียวกัน (สร้างอาร์เรย์ <tt>elements</tt> ขึ้นมาก่อน โดยรวม this.bots และ this.player
+
จากนั้นแก้เมท็อด 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">
แถว 321: แถว 369:
 
   // ...
 
   // ...
  
   move() {
+
   moveTo(p: Player) {
 
     if (this.isAlive) {
 
     if (this.isAlive) {
 
       // ...  (your old code)
 
       // ...  (your old code)
แถว 329: แถว 377:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
เราสามารถแก้ให้ bot ถ้าตายแสดงสัญลักษณ์เป็น # แทนที่จะเป็น B ได้ด้วย โดยแก้ <tt>getPieceChar</tt>
 +
 +
<syntaxhighlight lang="typescript">
 +
  getPieceChar(): string {
 +
    if (this.isAlive) {
 +
      return 'B';
 +
    } else {
 +
      return '#';
 +
    }
 +
  }
 +
</syntaxhighlight>
 +
 +
เราจะเพิ่มเมทอด <tt>isHit</tt> ให้กับ GamePiece ดังนี้ (เมื่อเพิ่มแล้วทั้ง Bot และ Player จะใช้ได้ด้วย)
  
 
<syntaxhighlight lang="typescript">
 
<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>
  
แถว 342: แถว 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 รูปแบบอื่น ๆ จะทำอย่างไร?)

Bots รูปแบบอื่น ๆ

BotFactory