Πώς να κωδικοποιήσετε το Game of Life με το React

Το παιχνίδι της ζωής περιλαμβάνει ένα δισδιάστατο ορθογώνιο πλέγμα τετραγωνικών κυττάρων, καθένα από τα οποία βρίσκεται σε μία από τις δύο πιθανές καταστάσεις, ζωντανά ή νεκρά. Σε κάθε βήμα, κάθε κύτταρο αλληλεπιδρά με τους οκτώ γειτονικούς του γείτονες ακολουθώντας ένα απλό σύνολο κανόνων που οδηγούν σε γεννήσεις και θανάτους.

Είναι ένα παιχνίδι μηδενικών παικτών. Η εξέλιξή του καθορίζεται από την αρχική του κατάσταση, χωρίς να απαιτείται περαιτέρω είσοδος από παίκτες. Κάποιος αλληλεπιδρά με το παιχνίδι δημιουργώντας μια αρχική διαμόρφωση και παρατηρώντας πώς εξελίσσεται, ή, για προχωρημένους παίκτες, δημιουργώντας μοτίβα με συγκεκριμένες ιδιότητες.

Κανόνες

  1. Οποιοδήποτε ζωντανό κελί με λιγότερους από δύο ζωντανούς γείτονες πεθαίνει, σαν να είναι υποπληθυσμός
  2. Κάθε ζωντανό κελί με δύο ή τρεις ζώντες γείτονες ζει στην επόμενη γενιά
  3. Οποιοδήποτε ζωντανό κύτταρο με περισσότερους από τρεις ζωντανούς γείτονες πεθαίνει, λες και από υπερπληθυσμό
  4. Κάθε νεκρό κελί με ακριβώς τρεις ζωντανούς γείτονες γίνεται ζωντανό κελί, σαν να γίνεται με αναπαραγωγή

Αν και το παιχνίδι μπορεί να κωδικοποιηθεί τέλεια με JavaScript βανίλιας, ήμουν ευτυχής που πέρασα από την πρόκληση με το React. Ας ξεκινήσουμε λοιπόν.

Ρύθμιση React

Υπάρχουν διάφοροι τρόποι για να ρυθμίσετε το React, αλλά αν είστε νέοι σε αυτό, σας συνιστώ να δείτε τα έγγραφα Δημιουργία εφαρμογής React και το github, καθώς και τη λεπτομερή επισκόπηση του React από την Tania Rascia.

Σχεδιάζοντας το παιχνίδι

Η κύρια εικόνα στην κορυφή είναι η εφαρμογή του παιχνιδιού. Το πλέγμα του πίνακα που περιέχει φως (ζωντανά) και σκοτεινά (νεκρά) κελιά εμφανίζει την εξέλιξη του παιχνιδιού. Οι ελεγκτές σάς επιτρέπουν να ξεκινήσετε / να σταματήσετε, να κάνετε ένα βήμα τη φορά, να δημιουργήσετε έναν νέο πίνακα ή να τον καθαρίσετε για να πειραματιστείτε με τα δικά σας μοτίβα κάνοντας κλικ στα μεμονωμένα κελιά. Το ρυθμιστικό ελέγχει την ταχύτητα και η παραγωγή ενημερώνει τον αριθμό των ολοκληρωμένων επαναλήψεων.

Εκτός από το κύριο στοιχείο που διατηρεί την κατάσταση, θα δημιουργήσω ξεχωριστά μια λειτουργία για να δημιουργήσω όλη την κατάσταση του κελιού της πλακέτας από το μηδέν, ένα στοιχείο για το πλέγμα της πλακέτας και ένα άλλο για το ρυθμιστικό.

Ρύθμιση του App.js

Κατ 'αρχάς, ας εισαγάγουμε το React and React. Component από το "react". Στη συνέχεια, καθορίστε πόσες σειρές και στήλες έχει το πλέγμα του πίνακα. Πηγαίνω με 40 με 60 αλλά αισθάνομαι ελεύθερος να παίξω με διαφορετικούς αριθμούς. Στη συνέχεια, έρθει η ξεχωριστή συνάρτηση συνάρτησης και λειτουργίας (παρατηρήστε το πρώτο γράμμα με κεφαλαία γράμματα) που περιγράφεται παραπάνω, καθώς και το στοιχείο κλάσης που κρατά την κατάσταση και τις μεθόδους, συμπεριλαμβανομένης της απόδοσης. Τέλος, ας εξάγουμε το κύριο συστατικό App.

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;

Δημιουργία της κατάστασης του κελιού ενός νέου πίνακα

Δεδομένου ότι πρέπει να γνωρίζουμε την κατάσταση κάθε κελιού και των 8 γειτόνων του για κάθε επανάληψη, ας δημιουργήσουμε μια συνάρτηση που επιστρέφει έναν πίνακα συστοιχιών που κάθε ένα περιέχει κελιά με δυαδικές τιμές. Ο αριθμός των συστοιχιών στην κύρια συστοιχία θα ταιριάζει με τον αριθμό των σειρών και ο αριθμός των τιμών σε καθεμία από αυτές τις συστοιχίες θα ταιριάζει με τον αριθμό των στηλών. Έτσι, κάθε boolean τιμή θα αντιπροσωπεύει την κατάσταση κάθε κελιού, «ζωντανό» ή «νεκρό». Η παράμετρος της συνάρτησης είναι προεπιλεγμένη σε λιγότερο από 30% πιθανότητα να είναι ζωντανή, αλλά δεν μπορούσε να πειραματιστεί με άλλους αριθμούς.

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, ...] ... */

Δημιουργία πλέγματος πλακέτας

Ας ορίσουμε ένα στοιχείο συνάρτησης που δημιουργεί το πλέγμα πλακέτας και το αντιστοιχίζει σε μια μεταβλητή. Η συνάρτηση λαμβάνει την κατάσταση της κατάστασης ολόκληρης της πλακέτας και μια μέθοδο που επιτρέπει στους χρήστες να αλλάζουν την κατάσταση των μεμονωμένων κελιών ως στηρίγματα. Αυτή η μέθοδος ορίζεται στο κύριο στοιχείο όπου διατηρείται όλη η κατάσταση της εφαρμογής.

Κάθε κελί αντιπροσωπεύεται από έναν πίνακα και έχει ένα χαρακτηριστικό className του οποίου η τιμή εξαρτάται από τη δυαδική τιμή του αντίστοιχου κελιού πλακέτας. Το πρόγραμμα αναπαραγωγής που κάνει κλικ σε ένα κελί οδηγεί στη μέθοδο που μεταβιβάστηκε καθώς τα στηρίγματα κλήθηκαν με τη θέση της γραμμής και της στήλης του κελιού ως όρισμα

Ρίξτε μια ματιά στο Lifting State Up για περισσότερες πληροφορίες σχετικά με τις μεθόδους μετάδοσης ως στηρίγματα και μην ξεχάσετε να προσθέσετε τα κλειδιά.

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}
     
; };

Δημιουργία ρυθμιστή ταχύτητας

Αυτό το στοιχείο λειτουργίας δημιουργεί ένα ρυθμιστικό που επιτρέπει στους παίκτες να αλλάζουν την ταχύτητα των επαναλήψεων. Λαμβάνει την κατάσταση της τρέχουσας ταχύτητας και μια μέθοδο για τον χειρισμό της αλλαγής ταχύτητας ως στηρίγματα. Μπορείτε να δοκιμάσετε διαφορετικές ελάχιστες, μέγιστες και βηματικές τιμές. Μια αλλαγή ταχύτητας καταλήγει στη μέθοδο που περνάει καθώς τα στηρίγματα καλούνται με την επιθυμητή ταχύτητα ως όρισμα.

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

Κύριο συστατικό

Δεδομένου ότι περιέχει την κατάσταση της εφαρμογής ας το κάνουμε ένα στοιχείο κλάσης Λάβετε υπόψη ότι δεν χρησιμοποιώ το Hooks, μια νέα προσθήκη στο React 16.8 που σας επιτρέπει να χρησιμοποιείτε κατάσταση και άλλες δυνατότητες React χωρίς να γράφετε τάξη. Προτιμώ να χρησιμοποιήσω την πειραματική σύνταξη πεδίων δημόσιας τάξης, επομένως δεν δεσμεύω τις μεθόδους μέσα στον κατασκευαστή.

Ας το αναλύσουμε.

κατάσταση

Ορίζω την κατάσταση ως αντικείμενο με τις ιδιότητες για την κατάσταση του πίνακα, τον αριθμό δημιουργίας, το παιχνίδι που τρέχει ή σταμάτησε και την ταχύτητα των επαναλήψεων. Όταν ξεκινήσει το παιχνίδι, η κατάσταση των κελιών του πίνακα θα είναι αυτή που επιστρέφεται από την κλήση στη συνάρτηση που δημιουργεί μια νέα κατάσταση πίνακα. Η γενιά ξεκινά από το 0 και το παιχνίδι θα τρέξει μόνο αφού ο χρήστης αποφασίσει. Η προεπιλεγμένη ταχύτητα είναι 500ms.

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

Κουμπί Εκτέλεση / Διακοπή

Λειτουργία που επιστρέφει ένα διαφορετικό στοιχείο κουμπιού ανάλογα με την κατάσταση του παιχνιδιού: τρέξιμο ή διακοπή.

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

Καθαρός και νέος πίνακας

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.

Thanks for reading.