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 (
+ <>
+
+
+ >
+ );
+});