Conway's Game of Life as a React web app
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Board.tsx 4.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import React from 'react';
  2. import Cell from './Cell';
  3. import Controls from './Controls';
  4. import { generateNewGrid, updateGridDimensions } from './lib';
  5. const DEFAULT_WIDTH = 30;
  6. const DEFAULT_HEIGHT = 20;
  7. const DEFAULT_SPEED = 100;
  8. const DEFAULT_CELL_SIZE = 30;
  9. const INITIAL_GRID = Array(DEFAULT_HEIGHT).fill(
  10. Array(DEFAULT_WIDTH).fill(false),
  11. );
  12. const INITIAL_STATE: State = {
  13. grid: [...INITIAL_GRID],
  14. running: false,
  15. generations: 0,
  16. cellSize: DEFAULT_CELL_SIZE,
  17. speed: DEFAULT_SPEED,
  18. width: DEFAULT_WIDTH,
  19. height: DEFAULT_HEIGHT,
  20. };
  21. interface State {
  22. grid: boolean[][];
  23. running: boolean;
  24. cellSize: number;
  25. speed: number;
  26. width: number;
  27. height: number;
  28. generations: number;
  29. }
  30. class Board extends React.Component<{}, State> {
  31. timer: NodeJS.Timeout | null;
  32. constructor(props: {}) {
  33. super(props);
  34. this.state = INITIAL_STATE;
  35. this.timer = null;
  36. }
  37. start = () => {
  38. this.timer = setInterval(() => {
  39. const grid = generateNewGrid(this.state.grid);
  40. this.setState({ grid, generations: this.state.generations + 1 });
  41. if (!grid.map((row) => row.includes(true)).includes(true)) {
  42. this.stop();
  43. alert(`No more living cells, stopping`);
  44. }
  45. }, this.state.speed);
  46. };
  47. stop = () => {
  48. if (this.timer) clearInterval(this.timer);
  49. };
  50. reset = () => {
  51. if (window.confirm('Are you sure?')) {
  52. this.stop();
  53. this.setState(INITIAL_STATE);
  54. }
  55. };
  56. toggleCell = (x: number, y: number) => {
  57. const newGrid = [...this.state.grid];
  58. const newRow = [...newGrid[y]];
  59. newRow[x] = !newGrid[y][x];
  60. newGrid[y] = newRow;
  61. this.setState({ grid: newGrid });
  62. };
  63. toggleRunning = () => {
  64. if (this.state.running) this.stop();
  65. if (!this.state.running) this.start();
  66. this.setState({ running: !this.state.running });
  67. };
  68. updateSpeed = (speed: number) => {
  69. this.setState({ speed });
  70. if (this.state.running) {
  71. this.stop();
  72. this.start();
  73. }
  74. };
  75. updateCellSize = (f: (size: number) => number) => {
  76. this.setState({ cellSize: f(this.state.cellSize) });
  77. };
  78. updateDimensions = (dimensions: { width?: number; height?: number }) => {
  79. if (dimensions.width) {
  80. this.setState({ width: dimensions.width });
  81. }
  82. if (dimensions.height) {
  83. this.setState({ height: dimensions.height });
  84. }
  85. const grid = updateGridDimensions(
  86. { width: this.state.width, height: this.state.height },
  87. dimensions,
  88. this.state.grid,
  89. );
  90. this.setState({ grid });
  91. };
  92. download = () => {
  93. const state = JSON.stringify(this.state);
  94. const element = document.createElement('a');
  95. const file = new Blob([state], { type: 'text/plain' });
  96. element.href = URL.createObjectURL(file);
  97. element.download = 'export.json';
  98. document.body.appendChild(element);
  99. element.click();
  100. };
  101. upload = () => {
  102. if (
  103. window.confirm('This will replace any changes you have made. Continue?')
  104. ) {
  105. const element = document.createElement('input');
  106. element.type = 'file';
  107. element.click();
  108. element.addEventListener('change', (e) => {
  109. const files = (e.currentTarget as HTMLInputElement).files;
  110. if (files && files.length > 0) {
  111. const file = files[0];
  112. file.text().then((text) => {
  113. const newState = JSON.parse(text);
  114. this.setState(newState);
  115. });
  116. }
  117. });
  118. }
  119. };
  120. render() {
  121. const {
  122. cellSize,
  123. generations,
  124. grid,
  125. height,
  126. running,
  127. speed,
  128. width,
  129. } = this.state;
  130. return (
  131. <div className="container">
  132. <Controls
  133. running={running}
  134. speed={speed}
  135. width={width}
  136. height={height}
  137. toggle={this.toggleRunning}
  138. updateCellSize={this.updateCellSize}
  139. updateDimensions={this.updateDimensions}
  140. updateSpeed={this.updateSpeed}
  141. reset={this.reset}
  142. download={this.download}
  143. upload={this.upload}
  144. />
  145. <div className="board-container">
  146. <div className="board">
  147. {grid.map((row, y) => (
  148. <div className="row" key={y} style={{ height: cellSize }}>
  149. {row.map((cell, x) => (
  150. <Cell
  151. disabled={running}
  152. x={x}
  153. y={y}
  154. cellSize={cellSize}
  155. alive={cell}
  156. toggle={this.toggleCell}
  157. key={x}
  158. />
  159. ))}
  160. </div>
  161. ))}
  162. </div>
  163. </div>
  164. <div className="counter">
  165. <span>
  166. {generations} generation{generations === 1 ? '' : 's'}
  167. </span>
  168. </div>
  169. </div>
  170. );
  171. }
  172. }
  173. export default Board;