Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/392 add memory game as challenge #430

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/react/src/challenges/memory-game/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useSelector } from 'react-redux';

import Home from './components/pages/Home';
import Game from './components/pages/Game';

function MemoryGameApp() {
const state = useSelector((state) => state);

if (state.level === 0) {
return <Home />;
} else {
return <Game />;
}
}

export default MemoryGameApp;
18 changes: 18 additions & 0 deletions apps/react/src/challenges/memory-game/components/atoms/Cell.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import styled from 'styled-components';

const CellDiv = styled.div`
background-color: ${(props) => props.backgroundColor ?? '#FFF'};
cursor: pointer;
border-radius: 8px;
`;

export default function Cell({ rowIndex, colIndex, backgroundColor }) {
return (
<CellDiv
className="cell"
data-row-index={rowIndex}
data-col-index={colIndex}
backgroundColor={backgroundColor}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { useSpring, animated, config } from 'react-spring';
import styled from 'styled-components';

export const GameOverDiv = styled.div`
position: absolute;
top: 50%;
left: 50%;
font-size: 400%;
font-weight: bold;
color: red;
`;

const SpringGameOver = animated(GameOverDiv);

const MemoizedGameOver = React.memo(function GameOver({ isGameOver }) {
const [spring, setSpring] = useSpring(() => ({
transform: 'translate(-50%, -50%) rotate(-45deg) scale(0)',
config: config.wobbly,
}));

if (isGameOver) {
setTimeout(
() => setSpring({ transform: 'translate(-50%, -50%) rotate(-45deg) scale(1)' }),
800
);
}

return <SpringGameOver style={spring}>Game Over</SpringGameOver>;
});
export default MemoizedGameOver;
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useEffect, useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import styled from 'styled-components';

import { setCellOnClick, setUserGridAsync } from '../../store/dataSlice';
import Cell from '../atoms/Cell';

const Grid = styled.div.attrs((props) => ({
style: {
gridTemplateRows: `repeat(${props.rows}, 1fr)`,
gridTemplateColumns: `repeat(${props.cols}, 1fr)`,
width: `${props.width - 20}px`,
height: `${((props.width - 20) / props.cols) * props.rows}px`,
maxHeight: `${props.rows * 100}px`,
maxWidth: `${props.cols * 100}px`,
},
}))`
display: grid;
grid-gap: 4px;
background-color: #329cef;
border: 2px solid #329cef;
transition: all 0.25s;
`;

export default function GridDisplay({ userGrid, windowWidth }) {
const dispatch = useDispatch();
const state = useSelector((state) => state);

// Fit inside the widnow height
const windowHeight = window.innerHeight - 230;
if (windowWidth > windowHeight) {
windowWidth = windowHeight;
}

useEffect(() => {
dispatch(setUserGridAsync());
}, [dispatch, state.level]);

const onCellClick = useCallback(
function (event) {
if (!state.isLevelReadyForClick) {
return;
}

const target = event.target;
if (target.classList.contains('cell')) {
const rowIndex = target.dataset.rowIndex;
const colIndex = target.dataset.colIndex;
dispatch(setCellOnClick({ rowIndex, colIndex }));
}
},
[dispatch, state.isLevelReadyForClick]
);

return (
<Grid
rows={userGrid.length}
cols={userGrid[0].length}
onClick={onCellClick}
width={windowWidth}
>
{userGrid.map((rows, rowIndex) =>
rows.map((cell, colIndex) => (
<Cell
rowIndex={rowIndex}
colIndex={colIndex}
key={rowIndex + '' + colIndex}
backgroundColor={cell ? 'yellow' : undefined}
/>
))
)}
</Grid>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { useTransition, animated } from 'react-spring';
import { ImMan } from 'react-icons/im';
import { FlexRowCenter } from '../styles/common';

const MemoizedLivesManager = React.memo(function LivesManager({ lives }) {
const livesItems = [];
for (let i = 0; i < lives; i++) {
livesItems.push(<ImMan key={i} />);
}

const transitions = useTransition(livesItems, (item) => item.key, {
from: { transform: 'translateX(0px) translateY(0px)', opacity: 1 },
enter: { transform: 'translateX(0px) translateY(0px)', opacity: 1 },
leave: { transform: 'translateX(0px) translateY(-40px)', opacity: 0 },
});

return (
<FlexRowCenter style={{ fontSize: '2rem', padding: '10px' }}>
{transitions.map(({ item, props, key }) => (
<animated.div key={key} style={props}>
{item}
</animated.div>
))}
</FlexRowCenter>
);
});

export default MemoizedLivesManager;
46 changes: 46 additions & 0 deletions apps/react/src/challenges/memory-game/components/pages/Game.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Button } from '../styles/common';
import { onNextLevel, onReset } from '../../store/dataSlice';
import { useMeasure } from 'react-use';

import { FlexColumnCenter } from '../styles/common';
import GridDisplay from '../organisms/GridDisplay';
import LivesManager from '../organisms/LivesManager';
import GameOver from '../atoms/GameOver';

export default function Game() {
const state = useSelector((state) => state);
const dispatch = useDispatch();
const [ref, { width }] = useMeasure();

const nextClickHandler = useCallback(
function () {
dispatch(onNextLevel());
},
[dispatch]
);

const HomeClickHandler = useCallback(
function () {
dispatch(onReset());
},
[dispatch]
);

return (
<FlexColumnCenter ref={ref} style={{ position: 'relative' }}>
<h3>Level {state.level}</h3>
<GridDisplay
userGrid={state.userGrid}
rows={state.rows}
cols={state.cols}
windowWidth={width}
/>
<GameOver isGameOver={state.isGameOver} />
<LivesManager lives={state.life} />
{state.isLevelComplete && <Button onClick={nextClickHandler}>Next</Button>}
{state.isGameOver && <Button onClick={HomeClickHandler}>Home</Button>}
</FlexColumnCenter>
);
}
43 changes: 43 additions & 0 deletions apps/react/src/challenges/memory-game/components/pages/Home.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { onNextLevel } from '../../store/dataSlice';

import { Button } from '../styles/common';

const Container = styled.div`
text-align: center;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
`;

export default function Home() {
const dispatch = useDispatch();

const startClickHandler = useCallback(
function () {
dispatch(onNextLevel());
},
[dispatch]
);

return (
<Container>
<header>
<h1>Memory Game</h1>
</header>
<div>
<div style={{ margin: '50px' }}>
<p>Remember the colored boxes displayed and click on the boxes once the game starts</p>
<p>Difficulty will increase with each level</p>
</div>
<Button large onClick={startClickHandler}>
Start
</Button>
</div>
</Container>
);
}
22 changes: 22 additions & 0 deletions apps/react/src/challenges/memory-game/components/styles/common.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import styled from 'styled-components';

export const Button = styled.button`
padding: 5px 25px;
background-color: black;
color: white;
border-radius: 0.5rem;
box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.25);
cursor: pointer;
font-size: ${(props) => (props.large ? '1.5rem' : '1rem')};
`;

export const FlexColumnCenter = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;

export const FlexRowCenter = styled.div`
display: flex;
align-items: center;
`;
29 changes: 29 additions & 0 deletions apps/react/src/challenges/memory-game/components/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export function getEmptyGrid(rows, cols) {
const grid = [];
for (let i = 0; i < rows; i++) {
grid.push([]);
for (let j = 0; j < cols; j++) {
grid[i].push(0);
}
}
return grid;
}

export function getMemoryGrid(rows, cols) {
const totalCells = rows * cols;
const grid = getEmptyGrid(rows, cols);
const maxActiveCount = Math.ceil(Math.sqrt(totalCells)) + 1;
let activeCount = 0;

let position = 0;
while (activeCount < maxActiveCount) {
position = (position + Math.ceil(Math.random() * totalCells)) % totalCells;
const rowIndex = Math.floor(position / cols);
const colIndex = Math.floor(position % cols);
if (grid[rowIndex][colIndex] === 0) {
grid[rowIndex][colIndex] = 1;
activeCount = activeCount + 1;
}
}
return { grid, activeCount };
}
18 changes: 18 additions & 0 deletions apps/react/src/challenges/memory-game/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import MemoryGameApp from './App';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import dataReducer from './store/dataSlice';

const store = configureStore({
reducer: dataReducer,
});

const MemoryGame = () => {
return (
<Provider store={store}>
<MemoryGameApp />
</Provider>
);
};

export default MemoryGame;
5 changes: 5 additions & 0 deletions apps/react/src/challenges/memory-game/store/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const config = {
rows: 3,
cols: 3,
life: 5,
};
Loading