From ec1c9bdf61ab9bef374181fade9e17c1e291995d Mon Sep 17 00:00:00 2001 From: Maxim Kremnev Date: Thu, 10 Sep 2020 14:02:27 +0300 Subject: [PATCH 1/6] Installing redux in project --- package.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6c0faed..ad44ec6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "app-calc", "version": "1.0.0", - "description": "Set of calculation", + "description": "A project developed in the process of learning at the OTUS school", "main": "index.js", "scripts": { "start": "npx webpack-dev-server --mode development --open --hot", @@ -17,10 +17,16 @@ "keywords": [ "calculation", "factorial", - "sum" + "sum", + "gameoflife" ], "author": "Maxim Kremnev", "license": "ISC", + "repository": { + "type": "git", + "url": "git+https://github.com/mkremnev/app-calc.git" + }, + "homepage": "https://github.com/mkremnev/app-calc#readme", "dependencies": { "@emotion/core": "^10.0.34", "@emotion/styled": "^10.0.27", @@ -30,7 +36,9 @@ "react-bootstrap": "^1.3.0", "react-dom": "^16.13.1", "react-loader-spinner": "^3.1.14", - "react-router-dom": "^5.2.0" + "react-redux": "^7.2.1", + "react-router-dom": "^5.2.0", + "redux": "^4.0.5" }, "devDependencies": { "@babel/core": "^7.10.3", @@ -55,6 +63,7 @@ "@types/jest": "^26.0.5", "@types/react": "^16.9.42", "@types/react-dom": "^16.9.8", + "@types/react-redux": "^7.1.9", "@types/react-router-dom": "^5.1.5", "@types/react-test-renderer": "^16.9.3", "@typescript-eslint/eslint-plugin": "^2.34.0", From 14f7f318b4a0c54b6b2e3abb0f893df1d5557fd9 Mon Sep 17 00:00:00 2001 From: Maxim Kremnev Date: Fri, 11 Sep 2020 23:17:27 +0300 Subject: [PATCH 2/6] Create store --- src/rdx/store.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/rdx/store.ts diff --git a/src/rdx/store.ts b/src/rdx/store.ts new file mode 100644 index 0000000..c6376da --- /dev/null +++ b/src/rdx/store.ts @@ -0,0 +1,9 @@ +import { createStore } from 'redux'; +import { reducer } from './reducer/index'; + +export const store = createStore( + reducer, + // https://github.com/zalmoxisus/redux-devtools-extension + (window as any).__REDUX_DEVTOOLS_EXTENSION__ && + (window as any).__REDUX_DEVTOOLS_EXTENSION__(), +); From ec5f39ad6d668aed3d4d9fe642437072525b0942 Mon Sep 17 00:00:00 2001 From: Maxim Kremnev Date: Fri, 11 Sep 2020 23:18:02 +0300 Subject: [PATCH 3/6] Making actions --- src/rdx/actions.ts | 40 ++++++++++++++++++++++++++++++++++ src/rdx/reducer/field.ts | 46 ++++++++++++++++++++++++++++++++++++++++ src/rdx/reducer/game.ts | 25 ++++++++++++++++++++++ src/rdx/reducer/index.ts | 12 +++++++++++ src/rdx/reducer/speed.ts | 19 +++++++++++++++++ 5 files changed, 142 insertions(+) create mode 100644 src/rdx/actions.ts create mode 100644 src/rdx/reducer/field.ts create mode 100644 src/rdx/reducer/game.ts create mode 100644 src/rdx/reducer/index.ts create mode 100644 src/rdx/reducer/speed.ts diff --git a/src/rdx/actions.ts b/src/rdx/actions.ts new file mode 100644 index 0000000..1fdbdd3 --- /dev/null +++ b/src/rdx/actions.ts @@ -0,0 +1,40 @@ +export const SET_CELL = 'SET_CELL'; +export const CLEAR_BOARD = 'CLEAR_BOARD'; +export const UPDATE_BOARD = 'UPDATE_BOARD'; +export const CHANGE_SPEED = 'CHANGE_SPEED'; +export const GAME_RUN = 'GAME_RUN'; + +export type Coordinates = { x: number; y: number }; +export type SpeedState = number | string; + +export function setFill(payload: Coordinates) { + return { + type: SET_CELL, + payload, + }; +} + +export function clearBoard() { + return { + type: CLEAR_BOARD, + }; +} + +export function updateBoard() { + return { + type: UPDATE_BOARD, + }; +} + +export function changeSpeed(payload: SpeedState) { + return { + type: CHANGE_SPEED, + payload, + }; +} + +export function gameRun() { + return { + type: GAME_RUN, + }; +} diff --git a/src/rdx/reducer/field.ts b/src/rdx/reducer/field.ts new file mode 100644 index 0000000..15c44f9 --- /dev/null +++ b/src/rdx/reducer/field.ts @@ -0,0 +1,46 @@ +import { Action } from 'redux'; +import * as actionTypes from '@/rdx/actions'; + +type GameFieldState = boolean[][]; +const cellGridFillRandom = ( + rows: number, + columns: number, + cellStatus = () => Math.random() < 0.3, +) => { + const grid: GameFieldState = []; + for (let y = 0; y < rows; y++) { + grid[y] = []; + for (let x = 0; x < columns; x++) { + grid[y][x] = cellStatus(); + } + } + return grid; +}; + +const defaultState: GameFieldState = cellGridFillRandom(20, 20); + +export function field( + state: GameFieldState = defaultState, + action: Action & { payload?: any }, +): GameFieldState { + switch (action.type) { + case actionTypes.SET_CELL: { + const { x, y } = action.payload; + const newState = state.map((row) => [...row]); + newState[x][y] = !newState[x][y]; + return newState; + } + + case actionTypes.CLEAR_BOARD: { + const newState = cellGridFillRandom(20, 20, () => false); + return newState; + } + + case actionTypes.UPDATE_BOARD: { + const newState = cellGridFillRandom(20, 20); + return newState; + } + } + + return state; +} diff --git a/src/rdx/reducer/game.ts b/src/rdx/reducer/game.ts new file mode 100644 index 0000000..0c2367f --- /dev/null +++ b/src/rdx/reducer/game.ts @@ -0,0 +1,25 @@ +import { Action } from 'redux'; +import * as actionTypes from '@/rdx/actions'; + +export type GameState = { + gameRun: boolean; +}; + +const defaultState: GameState = { + gameRun: false, +}; + +export function game( + state: GameState = defaultState, + action: Action & { payload?: any }, +): GameState { + switch (action.type) { + case actionTypes.GAME_RUN: { + return { + gameRun: !state.gameRun, + }; + } + } + + return state; +} diff --git a/src/rdx/reducer/index.ts b/src/rdx/reducer/index.ts new file mode 100644 index 0000000..47ef01b --- /dev/null +++ b/src/rdx/reducer/index.ts @@ -0,0 +1,12 @@ +import { field } from './field'; +import { speed } from './speed'; +import { game } from './game'; +import { combineReducers } from 'redux'; + +export const reducer = combineReducers({ + field, + speed, + game, +}); + +export type GameOfLifeState = ReturnType; diff --git a/src/rdx/reducer/speed.ts b/src/rdx/reducer/speed.ts new file mode 100644 index 0000000..18c7aad --- /dev/null +++ b/src/rdx/reducer/speed.ts @@ -0,0 +1,19 @@ +import { Action } from 'redux'; +import * as actionTypes from '@/rdx/actions'; + +export type SpeedState = number; + +const defaultState: SpeedState = 500; + +export function speed( + state: SpeedState = defaultState, + action: Action & { payload?: any }, +): SpeedState { + switch (action.type) { + case actionTypes.CHANGE_SPEED: { + return action.payload as number; + } + } + + return state; +} From 0a38ac38f58038064202317567fb89e9cb698529 Mon Sep 17 00:00:00 2001 From: Maxim Kremnev Date: Fri, 11 Sep 2020 23:19:18 +0300 Subject: [PATCH 4/6] Making component with redux --- src/App.tsx | 14 ++-- src/AppContainer.tsx | 18 ++--- src/components/Main/GameOfLifeWithRedux.tsx | 86 +++++++++++++++++++++ src/components/View/Navigation.tsx | 3 + src/screens/GameOfLifeWithRedux.tsx | 13 ++++ 5 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 src/components/Main/GameOfLifeWithRedux.tsx create mode 100644 src/screens/GameOfLifeWithRedux.tsx diff --git a/src/App.tsx b/src/App.tsx index c29e364..1b9c64b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,15 @@ import React from 'react'; -import { Router } from 'react-router-dom'; -import { createMemoryHistory } from 'history'; +import { BrowserRouter as Router } from 'react-router-dom'; import { AppContainer } from './AppContainer'; +import { Provider } from 'react-redux'; +import { store } from './rdx/store'; export const App: React.FC<{}> = () => { - const history = createMemoryHistory(); return ( - - - + + + + + ); }; diff --git a/src/AppContainer.tsx b/src/AppContainer.tsx index 0840b74..f3dc6f1 100644 --- a/src/AppContainer.tsx +++ b/src/AppContainer.tsx @@ -4,6 +4,7 @@ import { Home } from '@/screens/Home'; import { Login } from '@/screens/Login'; import { Rules } from '@/screens/Rules'; import { GameOfLife } from '@/screens/GameOfLife'; +import { GameOfLifeWithReduxScreen } from '@/screens/GameOfLifeWithRedux'; import { NotFound } from '@/screens/NotFound'; export const LocationDisplay = withRouter(({ location }) => ( @@ -12,14 +13,13 @@ export const LocationDisplay = withRouter(({ location }) => ( export const AppContainer: React.FC<{}> = () => { return ( - <> - - - - - - - - + + + + + + + + ); }; diff --git a/src/components/Main/GameOfLifeWithRedux.tsx b/src/components/Main/GameOfLifeWithRedux.tsx new file mode 100644 index 0000000..8c6e70a --- /dev/null +++ b/src/components/Main/GameOfLifeWithRedux.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { GameOfLifeState } from '@/rdx/reducer'; +import { + setFill, + clearBoard, + updateBoard, + changeSpeed, + gameRun, +} from '@/rdx/actions'; +import { Field } from '@/components/Field/Field'; +import { connect } from 'react-redux'; +import { InterfaceLayout } from './Interfaces/Interfaces'; + +const GameOfLifeProtoWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-right: 10px; +`; + +function mapStateToProps(state: GameOfLifeState) { + return { + gameField: state.field, + speed: state.speed, + run: state.game, + }; +} + +const mapDispatchToProps = { + setFill, + clearBoard, + updateBoard, + changeSpeed, + gameRun, +}; + +type GameOfLifeWithReduxProps = ReturnType & + typeof mapDispatchToProps; + +export class GameOfLife extends React.Component { + onClick = (x: number, y: number) => { + this.props['setFill']({ x, y }); + }; + + speedChange = (ev: React.ChangeEvent) => { + this.props.changeSpeed((ev.target as HTMLInputElement).value); + }; + + render() { + return ( + + + + + ); + } +} + +export const GameOfLifeWithRedux = connect( + mapStateToProps, + mapDispatchToProps, +)(GameOfLife); diff --git a/src/components/View/Navigation.tsx b/src/components/View/Navigation.tsx index 61bb4c0..6e4dbc2 100644 --- a/src/components/View/Navigation.tsx +++ b/src/components/View/Navigation.tsx @@ -52,6 +52,9 @@ export const Navigation: React.FC<{}> = () => {
  • Game
  • +
  • + Gamewithredux +
  • ); diff --git a/src/screens/GameOfLifeWithRedux.tsx b/src/screens/GameOfLifeWithRedux.tsx new file mode 100644 index 0000000..442bcb6 --- /dev/null +++ b/src/screens/GameOfLifeWithRedux.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { authorizedCheck } from '@/common/authorizedCheck'; +import { GameOfLifeWithRedux } from '@/components/Main/GameOfLifeWithRedux'; +import { Navigation } from '@/components/View/Navigation'; + +export const GameOfLifeWithReduxScreen: React.FC<{}> = authorizedCheck(() => { + return ( + <> + + + + ); +}); From a2cd205028bf04f5a82e09d04800e1d891250a0a Mon Sep 17 00:00:00 2001 From: Maxim Kremnev Date: Fri, 11 Sep 2020 23:54:47 +0300 Subject: [PATCH 5/6] Edit files with concept Redux --- src/components/Main/GameOfLifeWithRedux.tsx | 2 +- src/rdx/reducer/field.ts | 10 +++++----- src/rdx/reducer/speed.ts | 12 +++++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/components/Main/GameOfLifeWithRedux.tsx b/src/components/Main/GameOfLifeWithRedux.tsx index 8c6e70a..0344fda 100644 --- a/src/components/Main/GameOfLifeWithRedux.tsx +++ b/src/components/Main/GameOfLifeWithRedux.tsx @@ -67,7 +67,7 @@ export class GameOfLife extends React.Component { }} input={{ onChange: this.speedChange, - value: this.props.speed, + value: this.props.speed.value, name: 'speed', type: 'range', min: '50', diff --git a/src/rdx/reducer/field.ts b/src/rdx/reducer/field.ts index 15c44f9..370a467 100644 --- a/src/rdx/reducer/field.ts +++ b/src/rdx/reducer/field.ts @@ -1,13 +1,13 @@ import { Action } from 'redux'; import * as actionTypes from '@/rdx/actions'; -type GameFieldState = boolean[][]; +type FieldState = boolean[][]; const cellGridFillRandom = ( rows: number, columns: number, cellStatus = () => Math.random() < 0.3, ) => { - const grid: GameFieldState = []; + const grid: FieldState = []; for (let y = 0; y < rows; y++) { grid[y] = []; for (let x = 0; x < columns; x++) { @@ -17,12 +17,12 @@ const cellGridFillRandom = ( return grid; }; -const defaultState: GameFieldState = cellGridFillRandom(20, 20); +const defaultState: FieldState = cellGridFillRandom(20, 20); export function field( - state: GameFieldState = defaultState, + state: FieldState = defaultState, action: Action & { payload?: any }, -): GameFieldState { +): FieldState { switch (action.type) { case actionTypes.SET_CELL: { const { x, y } = action.payload; diff --git a/src/rdx/reducer/speed.ts b/src/rdx/reducer/speed.ts index 18c7aad..9c81216 100644 --- a/src/rdx/reducer/speed.ts +++ b/src/rdx/reducer/speed.ts @@ -1,9 +1,13 @@ import { Action } from 'redux'; import * as actionTypes from '@/rdx/actions'; -export type SpeedState = number; +export type SpeedState = { + value: number; +}; -const defaultState: SpeedState = 500; +const defaultState: SpeedState = { + value: 500, +}; export function speed( state: SpeedState = defaultState, @@ -11,7 +15,9 @@ export function speed( ): SpeedState { switch (action.type) { case actionTypes.CHANGE_SPEED: { - return action.payload as number; + return { + value: action.payload, + }; } } From 2eba2a191b43282792a709af46cf92bca0e0f8c8 Mon Sep 17 00:00:00 2001 From: Maxim Kremnev Date: Sat, 12 Sep 2020 19:23:12 +0300 Subject: [PATCH 6/6] Creating logics gameoflife used in Redux --- src/components/Main/GameOfLifeWithRedux.tsx | 20 +++++++ src/rdx/actions.ts | 7 +++ src/rdx/reducer/field.ts | 63 +++++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/src/components/Main/GameOfLifeWithRedux.tsx b/src/components/Main/GameOfLifeWithRedux.tsx index 0344fda..9b8c9da 100644 --- a/src/components/Main/GameOfLifeWithRedux.tsx +++ b/src/components/Main/GameOfLifeWithRedux.tsx @@ -7,6 +7,7 @@ import { updateBoard, changeSpeed, gameRun, + isGame, } from '@/rdx/actions'; import { Field } from '@/components/Field/Field'; import { connect } from 'react-redux'; @@ -34,12 +35,15 @@ const mapDispatchToProps = { updateBoard, changeSpeed, gameRun, + isGame, }; type GameOfLifeWithReduxProps = ReturnType & typeof mapDispatchToProps; export class GameOfLife extends React.Component { + private timerID!: NodeJS.Timeout; + onClick = (x: number, y: number) => { this.props['setFill']({ x, y }); }; @@ -48,6 +52,22 @@ export class GameOfLife extends React.Component { this.props.changeSpeed((ev.target as HTMLInputElement).value); }; + componentDidUpdate(prevProps: typeof mapDispatchToProps) { + const isRunningGame = this.props.run.gameRun; + const speed = this.props.speed.value; + const gameStarted = !prevProps.gameRun && isRunningGame; + const gameStopped = prevProps.gameRun && !isRunningGame; + if (isRunningGame || gameStopped) { + clearInterval(this.timerID); + } + + if (isRunningGame || gameStarted) { + this.timerID = setInterval(() => { + this.props.isGame(); + }, speed); + } + } + render() { return ( diff --git a/src/rdx/actions.ts b/src/rdx/actions.ts index 1fdbdd3..9432f03 100644 --- a/src/rdx/actions.ts +++ b/src/rdx/actions.ts @@ -3,6 +3,7 @@ export const CLEAR_BOARD = 'CLEAR_BOARD'; export const UPDATE_BOARD = 'UPDATE_BOARD'; export const CHANGE_SPEED = 'CHANGE_SPEED'; export const GAME_RUN = 'GAME_RUN'; +export const IS_GAME = 'IS_GAME'; export type Coordinates = { x: number; y: number }; export type SpeedState = number | string; @@ -38,3 +39,9 @@ export function gameRun() { type: GAME_RUN, }; } + +export function isGame() { + return { + type: IS_GAME, + }; +} diff --git a/src/rdx/reducer/field.ts b/src/rdx/reducer/field.ts index 370a467..cd0f55f 100644 --- a/src/rdx/reducer/field.ts +++ b/src/rdx/reducer/field.ts @@ -2,6 +2,7 @@ import { Action } from 'redux'; import * as actionTypes from '@/rdx/actions'; type FieldState = boolean[][]; + const cellGridFillRandom = ( rows: number, columns: number, @@ -40,6 +41,68 @@ export function field( const newState = cellGridFillRandom(20, 20); return newState; } + + case actionTypes.IS_GAME: { + const nextStep = (prevState: FieldState) => { + const prevBoard = prevState; + const cloneBoard = state.map((row) => [...row]); + + const amountAliveNeighbors = (x: number, y: number) => { + const eightNeighbors = [ + [-1, -1], + [-1, 0], + [-1, 1], + [0, 1], + [1, 1], + [1, 0], + [1, -1], + [0, -1], + ]; + + return eightNeighbors.reduce((aliveNeighbors, neighbor) => { + const xCell = x + neighbor[0]; + const yCell = y + neighbor[1]; + const endBoard = + xCell >= 0 && + xCell < 20 && + yCell >= 0 && + yCell < 20; + if ( + aliveNeighbors < 4 && + endBoard && + prevBoard[xCell][yCell] + ) { + return aliveNeighbors + 1; + } else { + return aliveNeighbors; + } + }, 0); + }; + + for (let rows = 0; rows < 20; rows++) { + for (let columns = 0; columns < 20; columns++) { + const totalAliveNeighbors = amountAliveNeighbors( + rows, + columns, + ); + + if (!prevBoard[rows][columns]) { + if (totalAliveNeighbors === 3) + cloneBoard[rows][columns] = true; + } else { + if ( + totalAliveNeighbors < 2 || + totalAliveNeighbors > 3 + ) + cloneBoard[rows][columns] = false; + } + } + } + + return cloneBoard; + }; + return nextStep(state); + } } return state;