Se63/typescript/zombie

จาก Theory Wiki
รุ่นแก้ไขเมื่อ 07:11, 13 กรกฎาคม 2563 โดย Jittat (คุย | มีส่วนร่วม) (→‎สุ่มตำแหน่ง bot)
(ต่าง) ←รุ่นแก้ไขก่อนหน้า | รุ่นแก้ไขล่าสุด (ต่าง) | รุ่นแก้ไขถัดไป→ (ต่าง)
ไปยังการนำทาง ไปยังการค้นหา

โค้ดเริ่มต้น

ใส่ใน 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