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", 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..9b8c9da --- /dev/null +++ b/src/components/Main/GameOfLifeWithRedux.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { GameOfLifeState } from '@/rdx/reducer'; +import { + setFill, + clearBoard, + updateBoard, + changeSpeed, + gameRun, + isGame, +} 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, + 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 }); + }; + + speedChange = (ev: React.ChangeEvent) => { + 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 ( + + + + + ); + } +} + +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/rdx/actions.ts b/src/rdx/actions.ts new file mode 100644 index 0000000..9432f03 --- /dev/null +++ b/src/rdx/actions.ts @@ -0,0 +1,47 @@ +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 const IS_GAME = 'IS_GAME'; + +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, + }; +} + +export function isGame() { + return { + type: IS_GAME, + }; +} diff --git a/src/rdx/reducer/field.ts b/src/rdx/reducer/field.ts new file mode 100644 index 0000000..cd0f55f --- /dev/null +++ b/src/rdx/reducer/field.ts @@ -0,0 +1,109 @@ +import { Action } from 'redux'; +import * as actionTypes from '@/rdx/actions'; + +type FieldState = boolean[][]; + +const cellGridFillRandom = ( + rows: number, + columns: number, + cellStatus = () => Math.random() < 0.3, +) => { + const grid: FieldState = []; + for (let y = 0; y < rows; y++) { + grid[y] = []; + for (let x = 0; x < columns; x++) { + grid[y][x] = cellStatus(); + } + } + return grid; +}; + +const defaultState: FieldState = cellGridFillRandom(20, 20); + +export function field( + state: FieldState = defaultState, + action: Action & { payload?: any }, +): FieldState { + 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; + } + + 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; +} 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..9c81216 --- /dev/null +++ b/src/rdx/reducer/speed.ts @@ -0,0 +1,25 @@ +import { Action } from 'redux'; +import * as actionTypes from '@/rdx/actions'; + +export type SpeedState = { + value: number; +}; + +const defaultState: SpeedState = { + value: 500, +}; + +export function speed( + state: SpeedState = defaultState, + action: Action & { payload?: any }, +): SpeedState { + switch (action.type) { + case actionTypes.CHANGE_SPEED: { + return { + value: action.payload, + }; + } + } + + return state; +} 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__(), +); 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 ( + <> + + + + ); +});