├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── components │ ├── Food.tsx │ ├── Ghost.tsx │ ├── Header.tsx │ ├── Pacman.tsx │ └── Scene.tsx ├── context │ └── GameContext.tsx ├── hooks │ └── useInterval.ts ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts ├── styles │ └── Colors.ts └── types │ ├── character.ts │ ├── color.ts │ ├── difficulty.ts │ ├── direction.ts │ ├── gameStatus.ts │ └── position.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pacman-react 2 | Is a game created as a showcase to understand how to use `react + typescript + hooks + react context`. 3 | 4 | The project was boilerplated with create-react-app. 5 | 6 | Play the game [here](https://mbfassnacht.github.io/pacman-react/)! 7 | 8 | [![pacman](https://github.com/mbfassnacht/assets/raw/master/images/pacman-react/react-pacman.png)](https://github.com/mbfassnacht/assets/raw/master/images/pacman-react/react-pacman.png) 9 | 10 | 11 | ## INSTALLATION DEPENDENCES 12 | ```javascript 13 | yarn install 14 | ``` 15 | ### USAGE 16 | The following commands are available in your project: 17 | ```bash 18 | # Start for development 19 | yarn start 20 | 21 | # Just build the dist version and copy static files 22 | yarn build 23 | 24 | # Run unit tests 25 | yarn test 26 | ``` 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pacman-react", 3 | "version": "3.0.0", 4 | "description": "Pacman game created with react", 5 | "author": "mbfassnacht", 6 | "homepage": "https://mbfassnacht.de/pacman-react/", 7 | "dependencies": { 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "@types/jest": "^27.5.2", 12 | "@types/node": "^16.18.11", 13 | "@types/react": "^18.0.26", 14 | "@types/react-dom": "^18.0.10", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-scripts": "5.0.1", 18 | "react-svg-inline": "^2.1.1", 19 | "styled-components": "^5.3.6", 20 | "typescript": "^4.9.4", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "@types/react-svg-inline": "^2.1.3", 49 | "@types/styled-components": "^5.1.26" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllTwinkleStars/react_pacman/eca817f9b7741f26dfde4280f26a2c3c04a07077/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | pacman-react 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllTwinkleStars/react_pacman/eca817f9b7741f26dfde4280f26a2c3c04a07077/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllTwinkleStars/react_pacman/eca817f9b7741f26dfde4280f26a2c3c04a07077/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import Header from "./components/Header"; 4 | import Scene from "./components/Scene"; 5 | import { GameProvider } from "./context/GameContext"; 6 | 7 | function App() { 8 | return ( 9 |
10 | 11 |
12 | 13 | 14 |
15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /src/components/Food.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Position } from "../types/position"; 4 | import colors from "../styles/Colors"; 5 | import { useGameContext } from "../context/GameContext"; 6 | import { GAME_STATUS } from "../types/gameStatus"; 7 | 8 | interface StyledFoodProps { 9 | position: Position; 10 | hidden: boolean; 11 | } 12 | 13 | const eatPrecision = 18; 14 | 15 | export type FoodProps = { 16 | name: string; 17 | position: Position; 18 | hidden: boolean; 19 | pacmanSize: number; 20 | }; 21 | 22 | const Food = (props: FoodProps) => { 23 | const position = props.position; 24 | const [isHidden, setIsHidden] = React.useState(false); 25 | const { pacmanPosition, setPoints, points, foodAmount, setGameStatus } = 26 | useGameContext(); 27 | 28 | function eaten() { 29 | setIsHidden(true); 30 | } 31 | 32 | React.useEffect(() => { 33 | function gameRestarted() { 34 | setIsHidden(false); 35 | } 36 | 37 | document.addEventListener("restart-game", gameRestarted); 38 | return () => document.removeEventListener("restart-game", gameRestarted); 39 | }, []); 40 | 41 | React.useEffect(() => { 42 | if ( 43 | !isHidden && 44 | pacmanPosition.left + (props.pacmanSize - eatPrecision) / 2 >= 45 | position.left && 46 | pacmanPosition.left - (props.pacmanSize - eatPrecision) / 2 < 47 | position.left && 48 | pacmanPosition.top + (props.pacmanSize - eatPrecision) / 2 >= 49 | position.top && 50 | pacmanPosition.top - (props.pacmanSize - eatPrecision) / 2 < position.top 51 | ) { 52 | eaten(); 53 | if (foodAmount === points + 1) { 54 | setGameStatus(GAME_STATUS.WON); 55 | } 56 | setPoints(points + 1); 57 | } 58 | }, [pacmanPosition, position]); 59 | 60 | return ( 61 | 64 | ); 65 | }; 66 | 67 | const StyledFood = styled.div` 68 | width: 60px; 69 | height: 60px; 70 | position: absolute; 71 | display: ${(props) => (props.hidden ? "none" : "block")}; 72 | top: ${(props) => props.position.top}px; 73 | left: ${(props) => props.position.left}px; 74 | 75 | .effective-food { 76 | border-radius: 50px; 77 | width: 10px; 78 | height: 10px; 79 | background-color: ${colors.color2}; 80 | margin: 20px; 81 | } 82 | `; 83 | 84 | export default Food; 85 | -------------------------------------------------------------------------------- /src/components/Ghost.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Position, ghostStartPosition } from "../types/position"; 4 | import { DIRECTION, Direction } from "../types/direction"; 5 | import { Character } from "../types/character"; 6 | import { useGameContext } from "../context/GameContext"; 7 | import { useInterval } from "../hooks/useInterval"; 8 | import { COLOR } from "../types/color"; 9 | import { GAME_STATUS } from "../types/gameStatus"; 10 | 11 | interface StyledGhostProps { 12 | position: Position; 13 | color: string; 14 | } 15 | 16 | const GhostIcon = () => ( 17 | 25 | 26 | 27 | 36 | 37 | 38 | 39 | ); 40 | 41 | const Ghost = (props: Character) => { 42 | const { pacmanPosition, setGameStatus, gameStatus } = useGameContext(); 43 | const [position, setPosition] = React.useState(ghostStartPosition); 44 | const [direction, setDirection] = React.useState(DIRECTION.LEFT); 45 | const [color, setColor] = React.useState(props.color!); 46 | const [changeDirectionWaitingTime, setChangeDirectionWaitingTime] = 47 | React.useState(0); 48 | useInterval(move, 100); 49 | 50 | React.useEffect(() => { 51 | document.addEventListener("restart-game", gameRestarted); 52 | return () => document.removeEventListener("restart-game", gameRestarted); 53 | }, []); 54 | 55 | function gameRestarted() { 56 | setColor(props.color); 57 | setPosition(ghostStartPosition); 58 | } 59 | 60 | function move() { 61 | if (gameStatus === GAME_STATUS.IN_PROGRESS) { 62 | if (changeDirectionWaitingTime > 4) { 63 | const movement = Math.floor(Math.random() * 4) + 0; 64 | const arrayOfMovement: Direction[] = [ 65 | DIRECTION.LEFT, 66 | DIRECTION.UP, 67 | DIRECTION.DOWN, 68 | DIRECTION.RIGHT, 69 | ]; 70 | setDirection(arrayOfMovement[movement]); 71 | setChangeDirectionWaitingTime(0); 72 | } else { 73 | setChangeDirectionWaitingTime((oldChangeDirectionWaitingTime) => { 74 | return oldChangeDirectionWaitingTime + 1; 75 | }); 76 | } 77 | 78 | setPosition((oldPosition: Position) => { 79 | const currentLeft = position.left; 80 | const currentTop = position.top; 81 | let newPosition: Position = { top: 0, left: 0 }; 82 | 83 | switch (direction) { 84 | case DIRECTION.LEFT: 85 | newPosition = { 86 | top: currentTop, 87 | left: Math.max(currentLeft - props.velocity, 0), 88 | }; 89 | break; 90 | case DIRECTION.UP: 91 | newPosition = { 92 | top: Math.max(currentTop - props.velocity, 0), 93 | left: currentLeft, 94 | }; 95 | break; 96 | case DIRECTION.RIGHT: 97 | newPosition = { 98 | top: currentTop, 99 | left: Math.min( 100 | currentLeft + props.velocity, 101 | window.innerWidth - props.border - props.size 102 | ), 103 | }; 104 | break; 105 | 106 | default: 107 | newPosition = { 108 | top: Math.min( 109 | currentTop + props.velocity, 110 | window.innerHeight - 111 | props.size - 112 | props.border - 113 | props.topScoreBoard 114 | ), 115 | left: currentLeft, 116 | }; 117 | } 118 | if ( 119 | pacmanPosition.left > newPosition.left - props.size && 120 | pacmanPosition.left < newPosition.left + props.size && 121 | pacmanPosition.top > newPosition.top - props.size && 122 | pacmanPosition.top < newPosition.top + props.size 123 | ) { 124 | setGameStatus(GAME_STATUS.LOST); 125 | } 126 | 127 | return newPosition; 128 | }); 129 | } 130 | if ( 131 | gameStatus !== GAME_STATUS.PAUSED && 132 | gameStatus !== GAME_STATUS.IN_PROGRESS 133 | ) { 134 | setColor(COLOR.GHOST_DEAD); 135 | } 136 | } 137 | 138 | return ( 139 | 140 | 141 | 142 | ); 143 | }; 144 | 145 | const StyledGhost = styled.div` 146 | width: 60px; 147 | height: 63px; 148 | position: absolute; 149 | top: ${(props) => props.position.top}px; 150 | left: ${(props) => props.position.left}px; 151 | 152 | svg { 153 | fill: ${(props) => { 154 | switch (props.color) { 155 | case COLOR.RED: 156 | return COLOR.RED; 157 | case COLOR.BLUE: 158 | return COLOR.BLUE; 159 | case COLOR.ORANGE: 160 | return COLOR.ORANGE; 161 | case COLOR.GREEN: 162 | return COLOR.GREEN; 163 | default: 164 | return COLOR.GHOST_DEAD; 165 | } 166 | }}; 167 | } 168 | `; 169 | 170 | export default Ghost; 171 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import colors from "../styles/Colors"; 3 | import styled from "styled-components"; 4 | import { useGameContext } from "../context/GameContext"; 5 | import { useInterval } from "../hooks/useInterval"; 6 | import { GAME_STATUS } from "../types/gameStatus"; 7 | 8 | const Header = () => { 9 | const { points, foodAmount, gameStatus } = useGameContext(); 10 | const [timeElapsed, setTimeElapsed] = React.useState(0); 11 | 12 | React.useEffect(() => { 13 | document.addEventListener("restart-game", gameRestarted); 14 | return () => document.removeEventListener("restart-game", gameRestarted); 15 | }, []); 16 | 17 | function gameRestarted() { 18 | setTimeElapsed(0); 19 | } 20 | 21 | useInterval(() => { 22 | if (gameStatus === GAME_STATUS.IN_PROGRESS) { 23 | setTimeElapsed((previuosTime) => { 24 | return previuosTime + 1; 25 | }); 26 | } 27 | }, 1000); 28 | 29 | return ( 30 | 31 | PACMAN 32 |
33 |
34 | Score: 35 | 36 | {points} / {foodAmount} 37 | 38 |
39 |
40 | Time elapsed: 41 | {timeElapsed} 42 |
43 |
44 |
45 | ); 46 | }; 47 | 48 | const StyledHeader = styled.div` 49 | height: 100px; 50 | background-color: ${colors.color3}; 51 | color: ${colors.color2}; 52 | display: flex; 53 | padding-left: 10px; 54 | padding-right: 10px; 55 | justify-content: space-between; 56 | 57 | .title { 58 | font-size: 80px; 59 | text-align: left; 60 | margin-top: 10px; 61 | } 62 | 63 | .score { 64 | font-size: 34px; 65 | text-align: right; 66 | margin-top: 10px; 67 | } 68 | `; 69 | 70 | export default Header; 71 | -------------------------------------------------------------------------------- /src/components/Pacman.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { keyframes } from "styled-components"; 3 | import { Position } from "../types/position"; 4 | import { ARROW, DIRECTION, Direction } from "../types/direction"; 5 | import { Character } from "../types/character"; 6 | import { useGameContext } from "../context/GameContext"; 7 | import { useInterval } from "../hooks/useInterval"; 8 | import { COLOR } from "../types/color"; 9 | import { GAME_STATUS } from "../types/gameStatus"; 10 | import colors from "../styles/Colors"; 11 | 12 | interface StyledPacmanProps { 13 | direction: Direction; 14 | position: Position; 15 | isAlive: boolean; 16 | } 17 | 18 | type PacmanMouthProps = { 19 | moving: boolean; 20 | }; 21 | 22 | const Pacman = (props: Character) => { 23 | const { 24 | pacmanPosition: position, 25 | setPacmanPosition, 26 | gameStatus, 27 | } = useGameContext(); 28 | const [direction, setDirection] = React.useState(DIRECTION.RIGHT); 29 | const [color, setColor] = React.useState(props.color); 30 | useInterval(move, 100); 31 | 32 | React.useEffect(() => { 33 | function rotate(keypressed: number) { 34 | switch (keypressed) { 35 | case ARROW.LEFT: 36 | setDirection(DIRECTION.LEFT); 37 | break; 38 | case ARROW.UP: 39 | setDirection(DIRECTION.UP); 40 | break; 41 | case ARROW.RIGHT: 42 | setDirection(DIRECTION.RIGHT); 43 | break; 44 | default: 45 | setDirection(DIRECTION.DOWN); 46 | } 47 | } 48 | 49 | function handleKeyDown(e: any) { 50 | const arrows = [ARROW.LEFT, ARROW.UP, ARROW.RIGHT, ARROW.DOWN]; 51 | 52 | if (arrows.indexOf(e.keyCode) >= 0) { 53 | rotate(e.keyCode); 54 | } 55 | } 56 | 57 | document.addEventListener("keydown", handleKeyDown, false); 58 | document.addEventListener("restart-game", gameRestarted); 59 | 60 | return () => { 61 | document.removeEventListener("restart-game", gameRestarted); 62 | document.removeEventListener("keydown", handleKeyDown); 63 | }; 64 | }, []); 65 | 66 | function gameRestarted() { 67 | setColor(props.color); 68 | } 69 | 70 | function move() { 71 | if (gameStatus === GAME_STATUS.IN_PROGRESS) { 72 | const currentLeft = position.left; 73 | const currentTop = position.top; 74 | let newPosition: Position = { top: 0, left: 0 }; 75 | switch (direction) { 76 | case DIRECTION.LEFT: 77 | newPosition = { 78 | top: currentTop, 79 | left: Math.max(currentLeft - props.velocity, 0), 80 | }; 81 | break; 82 | case DIRECTION.UP: 83 | newPosition = { 84 | top: Math.max(currentTop - props.velocity, 0), 85 | left: currentLeft, 86 | }; 87 | break; 88 | case DIRECTION.RIGHT: 89 | newPosition = { 90 | top: currentTop, 91 | left: Math.min( 92 | currentLeft + props.velocity, 93 | window.innerWidth - props.border - props.size 94 | ), 95 | }; 96 | break; 97 | 98 | default: 99 | newPosition = { 100 | top: Math.min( 101 | currentTop + props.velocity, 102 | window.innerHeight - 103 | props.size - 104 | props.border - 105 | props.topScoreBoard 106 | ), 107 | left: currentLeft, 108 | }; 109 | } 110 | setPacmanPosition(newPosition); 111 | } 112 | if (gameStatus === GAME_STATUS.LOST) { 113 | setColor(COLOR.PACMAN_DEAD); 114 | } 115 | } 116 | 117 | return ( 118 | 125 | 126 | 127 | 128 | ); 129 | }; 130 | 131 | const eat = keyframes` 132 | 0% { 133 | clip-path: polygon(100% 74%, 44% 48%, 100% 21%); 134 | } 135 | 25% { 136 | clip-path: polygon(100% 60%, 44% 48%, 100% 35%); 137 | } 138 | 50% { 139 | clip-path: polygon(100% 50%, 44% 48%, 100% 60%); 140 | } 141 | 75% { 142 | clip-path: polygon(100% 59%, 44% 48%, 100% 35%); 143 | } 144 | 100% { 145 | clip-path: polygon(100% 74%, 44% 48%, 100% 21%); 146 | } 147 | `; 148 | 149 | const StyledPacman = styled.div` 150 | width: 60px; 151 | height: 63px; 152 | position: absolute; 153 | top: ${(props) => props.position.top}px; 154 | left: ${(props) => props.position.left}px; 155 | transform: ${(props) => { 156 | switch (props.direction) { 157 | case DIRECTION.LEFT: 158 | return "rotateY(180deg)"; 159 | case DIRECTION.UP: 160 | return "rotate(-90deg)"; 161 | case DIRECTION.DOWN: 162 | return "rotate(90deg)"; 163 | default: 164 | return "rotate(0deg)"; 165 | } 166 | }}; 167 | width: 60px; 168 | height: 60px; 169 | border-radius: 50%; 170 | background: ${(props) => (props.isAlive ? colors.color2 : "white")}; 171 | position: relative; 172 | `; 173 | 174 | const PacmanEye = styled.div` 175 | position: absolute; 176 | width: 8px; 177 | height: 8px; 178 | border-radius: 50%; 179 | top: 10px; 180 | right: 26px; 181 | background: ${colors.color1}; 182 | `; 183 | 184 | const PacmanMouth = styled.div` 185 | animation-name: ${eat}; 186 | animation-duration: 0.7s; 187 | animation-iteration-count: ${(props) => 188 | props.moving ? "infinite" : "initial"}; 189 | background: ${colors.color1}; 190 | position: absolute; 191 | width: 100%; 192 | height: 100%; 193 | clip-path: polygon(100% 74%, 44% 48%, 100% 21%); 194 | `; 195 | 196 | export default Pacman; 197 | -------------------------------------------------------------------------------- /src/components/Scene.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Pacman from "./Pacman"; 3 | import Ghost from "./Ghost"; 4 | import Food from "./Food"; 5 | import styled from "styled-components"; 6 | import colors from "../styles/Colors"; 7 | import { useGameContext } from "../context/GameContext"; 8 | import { GAME_STATUS } from "../types/gameStatus"; 9 | import { COLOR } from "../types/color"; 10 | import { DIFFICULTY, Difficulty } from "../types/difficulty"; 11 | 12 | type SceneProps = { 13 | foodSize: number; 14 | border: number; 15 | topScoreBoard: number; 16 | }; 17 | 18 | const pacmanSize = 60; 19 | const pacmanVelocity = 30; 20 | const ghostSize = 60; 21 | const topScoreBoardHeight = 100; 22 | 23 | const generateFoodMatrix = (props: SceneProps, amountOfFood: number) => { 24 | let currentTop = 0; 25 | let currentLeft = 0; 26 | const foods = []; 27 | 28 | for (let i = 0; i <= amountOfFood; i++) { 29 | if (currentLeft + props.foodSize >= window.innerWidth - props.border) { 30 | currentTop += props.foodSize; 31 | currentLeft = 0; 32 | } 33 | if ( 34 | currentTop + props.foodSize >= 35 | window.innerHeight - props.border - props.topScoreBoard 36 | ) { 37 | break; 38 | } 39 | const position = { left: currentLeft, top: currentTop }; 40 | currentLeft = currentLeft + props.foodSize; 41 | foods.push( 42 |