Cara membuat kode Game of Life dengan React

Permainan Kehidupan melibatkan kisi ortogonal dua dimensi dari sel-sel persegi, yang masing-masing berada dalam salah satu dari dua kemungkinan keadaan, hidup atau mati. Pada setiap langkah, setiap sel berinteraksi dengan delapan tetangganya yang berdekatan dengan mengikuti seperangkat aturan sederhana yang mengakibatkan kelahiran dan kematian.

Ini adalah permainan tanpa pemain. Evolusinya ditentukan oleh keadaan awalnya, tidak memerlukan masukan lebih lanjut dari pemain. Seseorang berinteraksi dengan game dengan membuat konfigurasi awal dan mengamati perkembangannya, atau, untuk pemain tingkat lanjut, dengan membuat pola dengan properti tertentu.

Aturan

  1. Setiap sel hidup dengan kurang dari dua tetangga hidup akan mati, seolah-olah karena kekurangan populasi
  2. Setiap sel hidup dengan dua atau tiga tetangga hidup akan terus hidup hingga generasi berikutnya
  3. Setiap sel hidup dengan lebih dari tiga tetangga hidup akan mati, seolah-olah karena kelebihan populasi
  4. Setiap sel mati dengan tepat tiga tetangga hidup menjadi sel hidup, seolah-olah melalui reproduksi

Meskipun gim ini dapat dikodekan secara sempurna dengan vanilla JavaScript, saya senang melewati tantangan dengan React. Jadi ayo mulai.

Menyiapkan React

Ada beberapa cara untuk menyiapkan React, tetapi jika Anda baru mengenalnya, saya sarankan untuk memeriksa dokumen dan github Buat Aplikasi React , serta gambaran umum React yang terperinci oleh Tania Rascia.

Merancang game

Gambar utama di atas adalah implementasi game saya. Grid papan yang berisi sel terang (hidup) dan gelap (mati) menampilkan evolusi game. Pengontrol memungkinkan Anda untuk memulai / menghentikan, melangkah satu demi satu, menyiapkan papan baru atau menghapusnya untuk bereksperimen dengan pola Anda sendiri dengan mengklik sel individu. Penggeser mengontrol kecepatan, dan pembuatan menginformasikan jumlah iterasi yang diselesaikan.

Selain komponen utama yang menahan status, saya akan secara terpisah membuat fungsi untuk menghasilkan semua status sel papan dari awal, komponen untuk kisi papan dan satu lagi untuk slider.

Menyiapkan App.js

Pertama, impor React dan React.Component dari "react". Kemudian tentukan berapa banyak baris dan kolom yang dimiliki kisi papan. Saya memilih 40 x 60 tetapi merasa bebas untuk bermain dengan nomor yang berbeda. Kemudian muncul komponen fungsi dan fungsi yang terpisah (perhatikan huruf pertama yang dikapitalisasi) yang dijelaskan di atas serta komponen kelas yang menyimpan status dan metode, termasuk render. Terakhir, mari ekspor Aplikasi komponen utama.

import React, { Component } from 'react'; const totalBoardRows = 40; const totalBoardColumns = 60; const newBoardStatus = () => {}; const BoardGrid = () => {}; const Slider = () => {}; class App extends Component { state = {}; // Methods ... render() { return ( ); } } export default App;

Menghasilkan status sel papan baru

Karena kita perlu mengetahui status setiap sel dan 8 tetangganya untuk setiap iterasi, mari buat fungsi yang mengembalikan array array yang masing-masing berisi sel dengan nilai boolean. Jumlah larik di dalam larik utama akan cocok dengan jumlah baris, dan jumlah nilai dalam setiap larik ini akan cocok dengan jumlah kolom. Jadi setiap nilai boolean akan mewakili status setiap sel, "hidup" atau "mati". Parameter fungsi secara default memiliki peluang kurang dari 30% untuk hidup, tetapi bebas bereksperimen dengan angka lain.

const newBoardStatus = (cellStatus = () => Math.random()  { const grid = []; for (let r = 0; r < totalBoardRows; r++) { grid[r] = []; for (let c = 0; c < totalBoardColumns; c++) { grid[r][c] = cellStatus(); } } return grid; }; /* Returns an array of arrays, each containing booleans values (40) [Array(60), Array(60), ... ] 0: (60) [true, false, true, ... ] 1: (60) [false, false, false, ... ] 2: (60) [false, false, true, ...] ... */

Menghasilkan grid papan

Mari kita definisikan komponen fungsi yang membuat grid papan dan menugaskannya ke variabel. Fungsi tersebut menerima status seluruh papan status dan metode yang memungkinkan pengguna untuk mengubah status sel individu sebagai props. Metode ini didefinisikan pada komponen utama tempat semua status aplikasi diadakan.

Setiap sel diwakili oleh tabel dan memiliki atribut className yang nilainya bergantung pada nilai boolean dari sel papan yang sesuai. Pemain yang mengklik sebuah sel menghasilkan metode yang diteruskan sebagai props yang dipanggil dengan lokasi baris dan kolom sel sebagai argumen.

Lihat Lifting State Up untuk info tambahan tentang metode penerusan sebagai props, dan jangan lupa untuk menambahkan kuncinya.

const BoardGrid = ({ boardStatus, onToggleCellStatus }) => { const handleClick = (r,c) => onToggleCellStatus(r,c); const tr = []; for (let r = 0; r < totalBoardRows; r++) { const td = []; for (let c = 0; c < totalBoardColumns; c++) { td.push(  handleClick(r,c)} /> ); } tr.push({td}); } return {tr}
; };

Membuat penggeser kecepatan

Komponen fungsi ini membuat penggeser untuk memungkinkan pemain mengubah kecepatan iterasi. Ini menerima keadaan kecepatan saat ini dan metode untuk menangani perubahan kecepatan sebagai alat peraga. Anda dapat mencoba nilai minimum, maksimum dan langkah yang berbeda. Hasil perubahan kecepatan dalam metode yang diteruskan sebagai props dipanggil dengan kecepatan yang diinginkan sebagai argumen.

const Slider = ({ speed, onSpeedChange }) => { const handleChange = e => onSpeedChange(e.target.value); return (  ); };

Komponen utama

Karena ini berisi status aplikasi, mari kita jadikan komponen kelas. Perhatikan bahwa saya tidak menggunakan Hooks, tambahan baru di React 16.8 yang memungkinkan Anda menggunakan state dan fitur React lainnya tanpa menulis kelas. Saya lebih suka menggunakan sintaks bidang kelas publik eksperimental, jadi saya tidak mengikat metode dalam konstruktor.

Mari kita membedahnya.

Negara

Saya mendefinisikan status sebagai objek dengan properti untuk status papan, jumlah generasi, permainan berjalan atau berhenti, dan kecepatan iterasi. Saat permainan dimulai, status sel papan akan dikembalikan oleh panggilan ke fungsi yang menghasilkan status papan baru. Generasi dimulai pada 0 dan game hanya akan berjalan setelah pengguna memutuskan. Kecepatan default adalah 500ms.

class App extends Component { state = { boardStatus: newBoardStatus(), generation: 0, isGameRunning: false, speed: 500 }; // Other methods ... }

Tombol Run / Stop

Fungsi yang mengembalikan elemen tombol berbeda tergantung pada status game: berjalan atau berhenti.

class App extends Component { state = {...}; runStopButton = () => { return this.state.isGameRunning ? Stop : Start; } // Other methods ... }

Papan yang jelas dan baru

Methods to handle players request to start with a new random board’s cell status or to clear the board completely so they can then experiment by toggling individual cell status. The difference between them is that the one that clears the board sets the state for all cells to false, while the other doesn’t pass any arguments to the newBoardStatus method so the status of each cell becomes by default a random boolean value.

class App extends Component { state = {...}; runStopButton = () => {...} handleClearBoard = () => { this.setState({ boardStatus: newBoardStatus(() => false), generation: 0 }); } handleNewBoard = () => { this.setState({ boardStatus: newBoardStatus(), generation: 0 }); } // More methods ... }

Toggle cell status

We need a method to handle players’ requests to toggle individual cell status, which is useful to experiment with custom patterns directly on the board. The BoardGrid component calls it every time the player clicks on a cell. It sets the states of the board status by calling a function and passing it the previous state as argument.

The function deep clones the previous board’s status to avoid modifying it by reference when updating an individual cell on the next line. (Using const clonedBoardStatus = […boardStatus] would modify the original status because Spread syntax effectively goes one level deep while copying an array, therefore, it may be unsuitable for copying multidimensional arrays. Note that JSON.parse(JSON.stringify(obj)) doesn’t work if the cloned object uses functions). The function finally returns the updated cloned board status, effectively updating the status of the board.

For deep cloning check out here, here and here.

class App extends Component { state = {...}; runStopButton = () => {...} handleClearBoard = () => {...} handleNewBoard = () => {...} handleToggleCellStatus = (r,c) => { const toggleBoardStatus = prevState => { const clonedBoardStatus = JSON.parse(JSON.stringify(prevState.boardStatus)); clonedBoardStatus[r][c] = !clonedBoardStatus[r][c]; return clonedBoardStatus; }; this.setState(prevState => ({ boardStatus: toggleBoardStatus(prevState) })); } // Other methods ... }

Generating the next step

Here is where the next game iteration is generated by setting the state of the board status to the returned value of a function. It also adds one to the generation’s state to inform the player how many iterations have been produced so far.

The function (“nextStep”) defines two variables: the board status and a deep cloned board status. Then a function calculates the amount of neighbors (within the board) with value true for an individual cell, whenever it is called. Due to the rules, there’s no need to count more than four true neighbors per cell. Lastly, and according to the rules, it updates the cloned board’s individual cell status and return the cloned board status, which is used in the setState.

class App extends Component { state = {...}; runStopButton = () => {...} handleClearBoard = () => {...} handleNewBoard = () => {...} handleToggleCellStatus = () => {...} handleStep = () => { const nextStep = prevState => { const boardStatus = prevState.boardStatus; const clonedBoardStatus = JSON.parse(JSON.stringify(boardStatus)); const amountTrueNeighbors = (r,c) => { const neighbors = [[-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1]]; return neighbors.reduce((trueNeighbors, neighbor) => { const x = r + neighbor[0]; const y = c + neighbor[1]; const isNeighborOnBoard = (x >= 0 && x = 0 && y < totalBoardColumns); /* No need to count more than 4 alive neighbors */ if (trueNeighbors < 4 && isNeighborOnBoard && boardStatus[x][y]) { return trueNeighbors + 1; } else { return trueNeighbors; } }, 0); }; for (let r = 0; r < totalBoardRows; r++) { for (let c = 0; c < totalBoardColumns; c++) { const totalTrueNeighbors = amountTrueNeighbors(r,c); if (!boardStatus[r][c]) { if (totalTrueNeighbors === 3) clonedBoardStatus[r][c] = true; } else { if (totalTrueNeighbors  3) clonedBoardStatus[r][c] = false; } } } return clonedBoardStatus; }; this.setState(prevState => ({ boardStatus: nextStep(prevState), generation: prevState.generation + 1 })); } // Other methods ... } 

Handling the speed change and the start/stop action

These 3 methods only set the state value for the speed and isGameRunning properties.

Then, within the componentDidUpdate Lifecycle method, let’s clear and/or set a timer depending on different combinations of values. The timer schedules a call to the handleStep method at the specified speed intervals.

class App extends Component { state = {...}; runStopButton = () => {...} handleClearBoard = () => {...} handleNewBoard = () => {...} handleToggleCellStatus = () => {...} handleStep = () => {...} handleSpeedChange = newSpeed => { this.setState({ speed: newSpeed }); } handleRun = () => { this.setState({ isGameRunning: true }); } handleStop = () => { this.setState({ isGameRunning: false }); } componentDidUpdate(prevProps, prevState) { const { isGameRunning, speed } = this.state; const speedChanged = prevState.speed !== speed; const gameStarted = !prevState.isGameRunning && isGameRunning; const gameStopped = prevState.isGameRunning && !isGameRunning; if ((isGameRunning && speedChanged) || gameStopped) { clearInterval(this.timerID); } if ((isGameRunning && speedChanged) || gameStarted) { this.timerID = setInterval(() => { this.handleStep(); }, speed); } } // Render method ... }

The render method

The last method within the App component returns the desired structure and information of the page to be displayed. Since the state belongs to the App component, we pass the state and methods to the components that need them as props.

class App extends Component { // All previous methods ... render() { const { boardStatus, isGameRunning, generation, speed } = this.state; return ( 

Game of Life

Exporting the default App

Lastly, let’s export the default App (export default App;), which is imported along with the styles from “index.scss” by “index.js”, and then rendered to the DOM.

And that’s it! ?

Check out the full code on github and play the game here. Try these patterns below or create your own for fun.

Original text


Thanks for reading.