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

Feature/game controls #7

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
99 changes: 92 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,26 @@ import gameSlice, { selectIsRunning, selectIterationCounter } from './store/slic
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import PatternList from './components/PatternList'
import useDebounce from './hooks/useDebounce'

function App () {
const dispatch = useDispatch()
const gameIsRunning = useSelector(selectIsRunning)
const iterationCounter = useSelector(selectIterationCounter)
const [intervalId, setIntervalId] = useState<number | null>(null)
const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null)
const [widthInput, setWidthInput] = useState(50)
const [heightInput, setHeightInput] = useState(50)
const [width, setWidth] = useState(50)
const [height, setHeight] = useState(50)

useEffect(() => {
if (gameIsRunning) {
const id = setInterval(
() => dispatch(gameSlice.actions.tick()),
1000
500
)

setIntervalId(id as unknown as number)
setIntervalId(id)
} else if (intervalId !== null) {
clearInterval(intervalId)
setIntervalId(null)
Expand All @@ -32,22 +37,92 @@ function App () {
}
}, [gameIsRunning])

useDebounce(() => {
setWidthInput(validateWidth(widthInput))
setHeightInput(validateHeight(heightInput))
setWidth(validateWidth(widthInput))
setHeight(validateHeight(heightInput))
}, 500, [widthInput, heightInput])

function handleStarGame () {
dispatch(gameSlice.actions.startGame())
}

function handleClearGame () {
dispatch(gameSlice.actions.clearGame())
}

function handleStopGame () {
dispatch(gameSlice.actions.stopGame())
}

const handleWidthChange = (newWidth: number) => {
setWidthInput(isNaN(newWidth) ? 0 : newWidth)
}

const handleHeightChange = (newHeight: number) => {
setHeightInput(isNaN(newHeight) ? 0 : newHeight)
}

const validateDimension = (dimension: number) => {
if (dimension < 4) {
return 4
}

if (dimension > 100) {
return 100
}

return dimension
}

const validateWidth = (widthInput: number) => {
return validateDimension(widthInput)
}

const validateHeight = (heightInput: number) => {
return validateDimension(heightInput)
}

return (
<AppContainer>
<Aside>
<Header>
Jeu de la vie
</Header>
<ScaleFieldset>
<legend>Dimensions</legend>
<input
data-testid="grid-width-input"
type="number"
value={widthInput}
step='1'
min='4'
max='100'
onChange={
(event) => handleWidthChange(parseInt(event.target.value))
}
/>
<ScaleSeparator>x</ScaleSeparator>
<input
data-testid="grid-height-input"
type="number"
value={heightInput}
step='1'
min='4'
max='100'
onChange={
(event) => handleHeightChange(parseInt(event.target.value))
}
/>
</ScaleFieldset>
<List>
<li>Nombre d&apos;itérations : {iterationCounter}</li>
<li>
<Button onClick={handleClearGame} role="button">
Reset
</Button>
</li>
<li>
<Button onClick={gameIsRunning ? handleStopGame : handleStarGame} role="button">
{gameIsRunning ? 'Stop' : 'Start'}
Expand All @@ -57,7 +132,7 @@ function App () {
<PatternList></PatternList>
</Aside>
<Container>
<Grid width={10} height={10} />
<Grid width={width} height={height} />
</Container>
</AppContainer>
)
Expand All @@ -69,19 +144,29 @@ const Container = styled.div`
`
const Header = styled.header`
margin-bottom: 1.5em;
padding: 1em;
border-bottom: solid 1px grey;
`
const AppContainer = styled.div`
display: flex;
`
const Aside = styled.aside`
width: 15%;
padding: 1.5em 0.5em;
width: 300px;
text-align: center;
border-right: solid 1px grey;
`
const ScaleSeparator = styled.span`
margin: 0 1em;
`
const ScaleFieldset = styled.fieldset`
border: none;
padding-top: 1em;
margin-bottom: 1em;
`
export const List = styled.ul`
list-style-type: none;
padding: 0;
padding: 1em;
margin: 0 0 1em 0em;
`
const Button = styled.button`
margin-top: 1em
Expand Down
8 changes: 5 additions & 3 deletions src/components/Cell.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import styled from 'styled-components'
type Props = {
isAlive: boolean
gridWidth: number
gridHeight: number
}

const Cell = styled.div<Props>`
width: 1em;
height: 1em;
width: ${({ gridWidth, gridHeight }) => 900 / Math.max(gridWidth, gridHeight)}px;
height: ${({ gridWidth, gridHeight }) => 900 / Math.max(gridWidth, gridHeight)}px;
margin: auto;
border: 1px solid black;
box-sizing: border-box;
background-color: ${(props) => props.isAlive ? 'white' : 'grey'};
background-color: ${({ isAlive }) => isAlive ? 'white' : 'grey'};
`

export default Cell
4 changes: 2 additions & 2 deletions src/components/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const Grid = ({ width, height }: GridProps) => {
<Cell
id={`cell-${colIndex}-${lineIndex}`}
key={`cell-${colIndex}-${lineIndex}`}
gridWidth={grid[0].length}
gridHeight={grid.length}
isAlive={isAlive}
onClick={() => handleCellClick({ x: colIndex, y: lineIndex })}
role="gridcell"
Expand All @@ -62,8 +64,6 @@ type WrapperProps = {
}

const Wrapper = styled.div<WrapperProps>`
width:${({ width }) => width * 1}em;
height:${({ height }) => height * 1}em;
margin: auto;
margin-bottom: 1.5em;
border: solid 1px black;
Expand Down
25 changes: 25 additions & 0 deletions src/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect, useState } from 'react'

function useDebounce (func: () => void, delay: number, deps: unknown[]) {
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null)

useEffect(() => {
if (timeoutId === null) {
func()
} else {
clearTimeout(timeoutId)
}

setTimeoutId(
setTimeout(func, delay)
)

return () => {
if (timeoutId !== null) {
clearTimeout(timeoutId)
}
}
}, deps)
}

export default useDebounce
14 changes: 12 additions & 2 deletions src/store/slices/gameSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const getCellNeighboursValues = (grid: boolean[][], { x, y }: Coord) => {
]
}

function computeNextGrid (grid: boolean[][]): boolean[][] {
const computeNextGrid = (grid: boolean[][]): boolean[][] => {
return grid.map((line, lineIndex) =>
line.map((cellIsAlive, colIndex) => {
const alivedNeighboursCount = getCellNeighboursValues(
Expand All @@ -94,7 +94,7 @@ function computeNextGrid (grid: boolean[][]): boolean[][] {
)
}

function insertPattern (grid: boolean[][], pattern: Pattern, coord: Coord): boolean[][] {
const insertPattern = (grid: boolean[][], pattern: Pattern, coord: Coord): boolean[][] => {
const patternWidth = pattern.size.width
const patternHeight = pattern.size.height
const availableWidth = grid[0].length - coord.x
Expand Down Expand Up @@ -164,8 +164,18 @@ export const gameSlice = createSlice({
...state,
isRunning: true
}),
clearGame: (state) => ({
...state,
grid: Array.from(Array(state.grid.length).keys()).map(() => {
return Array.from(Array(state.grid[0].length).keys()).map(() => false)
}),
isRunning: false,
iterationCounter: 0
}),
initialize: (state, action: PayloadAction<GridProps>) => ({
...state,
isRunning: false,
iterationCounter: 0,
grid: Array.from(Array(action.payload.height).keys()).map(() => {
return Array.from(Array(action.payload.width).keys()).map(() => false)
})
Expand Down
81 changes: 81 additions & 0 deletions src/tests/components/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import App from '../../App'
import wrapWithReduxProvider from '../utils/reduxProviderWrapper'

describe('components/App.tsx', () => {
it('starts the game on start game button click and stops the game on stop game button click', async () => {
render(wrapWithReduxProvider(<App />))

const startGameBtn = screen.getByRole('button', { name: 'Start' })
await userEvent.click(startGameBtn)
expect(startGameBtn).toHaveTextContent('Stop')
await userEvent.click(startGameBtn)
expect(startGameBtn).toHaveTextContent('Start')
})

it('clears the game on clear button click', async () => {
render(wrapWithReduxProvider(<App />))

const cells = screen.getAllByRole('gridcell')

// Change grid content with stable pattern
for (let x = 0; x < 2; x++) {
for (let y = 0; y < 2; y++) {
await userEvent.click(cells[x + y * 49])
}
}

// Start game iteration
const startGameBtn = screen.getByRole('button', { name: 'Start' })
await userEvent.click(startGameBtn)

// Await first iteration to be process
await waitFor(() =>
expect(
screen.getByText('Nombre d\'itérations : 1')
).toBeInTheDocument()
)

// Reset game
const clearGameBtn = screen.getByRole('button', { name: 'Reset' })
await userEvent.click(clearGameBtn)

// Expect iteration to be reset & game to be stop
expect(screen.getByText('Nombre d\'itérations : 0')).toBeInTheDocument()
expect(startGameBtn).toHaveTextContent('Start')
})

it('changes grid size with width and height inputs', async () => {
render(wrapWithReduxProvider(<App />))

const widthInput = screen.getByTestId('grid-width-input')
const heightInput = screen.getByTestId('grid-height-input')

await userEvent.clear(widthInput)

await waitFor(async () => {
expect(screen.getByTestId('grid-width-input')).toHaveValue(0)
})

await userEvent.type(widthInput, '10')

await waitFor(async () => {
expect(screen.getByTestId('grid-width-input')).toHaveValue(10)
})

await userEvent.clear(heightInput)

await waitFor(async () => {
expect(screen.getByTestId('grid-height-input')).toHaveValue(0)
})

await userEvent.type(heightInput, '10')

await waitFor(async () => {
expect(screen.getByTestId('grid-height-input')).toHaveValue(10)
expect(screen.getAllByRole('row')).toHaveLength(10)
expect(screen.getAllByRole('gridcell')).toHaveLength(100)
})
})
})
11 changes: 0 additions & 11 deletions src/tests/components/Grid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { render, screen } from '@testing-library/react'
import Grid from '../../components/Grid'
import wrapWithReduxProvider from '../utils/reduxProviderWrapper'
import userEvent from '@testing-library/user-event'
import App from '../../App'

describe('components/Grid.tsx', () => {
it('displays a grid with desired dimensions', () => {
Expand Down Expand Up @@ -37,14 +36,4 @@ describe('components/Grid.tsx', () => {
await userEvent.click(cell)
expect(cell).toHaveStyle('background-color:grey')
})

it('starts the game on start game button click and stops the game on stop game button click', async () => {
render(wrapWithReduxProvider(<App />))

const startGameBtn = screen.getByRole('button', { name: 'Start' })
await userEvent.click(startGameBtn)
expect(startGameBtn).toHaveTextContent('Stop')
await userEvent.click(startGameBtn)
expect(startGameBtn).toHaveTextContent('Start')
})
})
Loading