Skip to content

Commit 35e41bc

Browse files
committed
tic-tac-toe
1 parent 3232ffe commit 35e41bc

18 files changed

Lines changed: 375 additions & 2 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Adapted from https://www.linkedin.com/feed/update/urn:li:activity:73308248843165
4646
| Image Carousel | [question](https://www.greatfrontend.com/interviews/study/gfe75/questions/user-interface/image-carousel) | [solution](./src/react/image-carousel/src/ImageCarousel.tsx) |
4747
| Data Table | [question](https://www.greatfrontend.com/interviews/study/gfe75/questions/user-interface/data-table) | [solution](./src/react/data-table/src/DataTable.tsx) |
4848
| File Explorer | [question](https://www.greatfrontend.com/interviews/study/gfe75/questions/user-interface/file-explorer) | [solution](./src/react/file-explorer/src/FileExplorer.tsx) |
49-
| Tic-tac-toe | [question](https://lnkd.in/gc7upPZS) | |
49+
| Tic-tac-toe | [question](https://www.greatfrontend.com/interviews/study/gfe75/questions/user-interface/tic-tac-toe) | [solution](./src/react/tic-tac-toe/src/TicTacToe.tsx) |
5050
| Nested Checkboxes | [question](https://lnkd.in/gfgtJHue) | |
5151

5252
## Demos

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"scripts": {
2222
"test:javascript": "jest --passWithNoTests src/javascript/",
2323
"test:react": "vitest run --passWithNoTests src/react/",
24-
"test": "npm run test:javascript && npm run test:react"
24+
"test": "npm run test:javascript & npm run test:react"
2525
}
2626
}

src/react/tic-tac-toe/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

src/react/tic-tac-toe/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Tic-tac-toe is a game for two players who take turns making space in a three-by-three grid with X or O. The player who succeeds in playing three of their marks in a horizontal, vertical, or diagonal row is the winner.
2+
3+
4+
Run: `npx vite src/react/tic-tac-toe/`
5+
6+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import js from '@eslint/js'
2+
import globals from 'globals'
3+
import reactHooks from 'eslint-plugin-react-hooks'
4+
import reactRefresh from 'eslint-plugin-react-refresh'
5+
import tseslint from 'typescript-eslint'
6+
7+
export default tseslint.config(
8+
{ ignores: ['dist'] },
9+
{
10+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
11+
files: ['**/*.{ts,tsx}'],
12+
languageOptions: {
13+
ecmaVersion: 2020,
14+
globals: globals.browser,
15+
},
16+
plugins: {
17+
'react-hooks': reactHooks,
18+
'react-refresh': reactRefresh,
19+
},
20+
rules: {
21+
...reactHooks.configs.recommended.rules,
22+
'react-refresh/only-export-components': [
23+
'warn',
24+
{ allowConstantExport: true },
25+
],
26+
},
27+
},
28+
)

src/react/tic-tac-toe/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React + TS</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Lines changed: 1 addition & 0 deletions
Loading

src/react/tic-tac-toe/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { TicTacToe } from "./TicTacToe";
2+
3+
export default function App() {
4+
return <TicTacToe matrix_size={3} />
5+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
.matrix {
2+
display: inline-block;
3+
margin: 2rem auto;
4+
padding: 1.5rem 2rem;
5+
background: #f7fafd;
6+
border-radius: 12px;
7+
box-shadow: 0 2px 16px rgba(0,0,0,0.08);
8+
font-family: 'Segoe UI', Arial, sans-serif;
9+
text-align: center;
10+
}
11+
12+
.row {
13+
display: flex;
14+
justify-content: center;
15+
}
16+
17+
.cell {
18+
width: 48px;
19+
height: 48px;
20+
margin: 4px;
21+
font-size: 2rem;
22+
font-weight: bold;
23+
background: #fff;
24+
border: 2px solid #b3c6e0;
25+
border-radius: 8px;
26+
cursor: pointer;
27+
transition: background 0.2s, border 0.2s;
28+
outline: none;
29+
}
30+
31+
.cell:hover:not(:disabled) {
32+
background: #eaf2fb;
33+
border-color: #4f8cff;
34+
}
35+
36+
.cell:disabled {
37+
cursor: not-allowed;
38+
opacity: 0.7;
39+
}
40+
41+
.restart-button {
42+
margin-top: 1.5rem;
43+
padding: 0.6rem 1.5rem;
44+
font-size: 1.1rem;
45+
font-weight: 600;
46+
background: linear-gradient(90deg, #4f8cff 0%, #6ed0ff 100%);
47+
color: #fff;
48+
border: none;
49+
border-radius: 8px;
50+
cursor: pointer;
51+
transition: background 0.2s;
52+
}
53+
54+
.restart-button:hover {
55+
background: #4f8cff;
56+
}
57+
58+
.tic-tac-toe-root {
59+
min-height: 100vh;
60+
display: flex;
61+
align-items: center;
62+
justify-content: center;
63+
background: #f4f7fa;
64+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { render, screen, fireEvent } from '@testing-library/react';
2+
import '@testing-library/jest-dom';
3+
import { TicTacToe } from './TicTacToe';
4+
5+
describe('TicTacToe', () => {
6+
it('renders the board and player turn', () => {
7+
render(<TicTacToe matrix_size={3} />);
8+
expect(screen.getByText('Player X:')).toBeInTheDocument();
9+
// Should render 9 cells for 3x3
10+
expect(screen.getAllByRole('button')).toHaveLength(9);
11+
});
12+
13+
it('alternates turns between X and O', () => {
14+
render(<TicTacToe matrix_size={3} />);
15+
const cells = screen.getAllByRole('button');
16+
fireEvent.click(cells[0]);
17+
expect(cells[0]).toHaveTextContent('X');
18+
expect(screen.getByText('Player O:')).toBeInTheDocument();
19+
fireEvent.click(cells[1]);
20+
expect(cells[1]).toHaveTextContent('O');
21+
expect(screen.getByText('Player X:')).toBeInTheDocument();
22+
});
23+
24+
it('prevents overwriting a cell', () => {
25+
render(<TicTacToe matrix_size={3} />);
26+
const cells = screen.getAllByRole('button');
27+
fireEvent.click(cells[0]);
28+
fireEvent.click(cells[0]);
29+
expect(cells[0]).toHaveTextContent('X');
30+
});
31+
32+
it('detects a win and highlights the winning line', () => {
33+
render(<TicTacToe matrix_size={3} />);
34+
const cells = screen.getAllByRole('button');
35+
// X O X
36+
// O X
37+
//
38+
fireEvent.click(cells[0]); // X
39+
fireEvent.click(cells[3]); // O
40+
fireEvent.click(cells[1]); // X
41+
fireEvent.click(cells[4]); // O
42+
fireEvent.click(cells[2]); // X wins
43+
expect(screen.getByText('X wins!')).toBeInTheDocument();
44+
// Winning cells should have lightblue background
45+
const winningCells = [cells[0], cells[1], cells[2]];
46+
winningCells.forEach(cell => {
47+
expect(cell).toHaveStyle({ backgroundColor: 'rgb(173, 216, 230)' });
48+
});
49+
});
50+
51+
it('shows restart button and resets the game', () => {
52+
render(<TicTacToe matrix_size={3} />);
53+
const cells = screen.getAllByRole('button');
54+
fireEvent.click(cells[0]);
55+
fireEvent.click(cells[3]);
56+
fireEvent.click(cells[1]);
57+
fireEvent.click(cells[4]);
58+
fireEvent.click(cells[2]); // X wins
59+
const restartBtn = screen.getByText('Restart');
60+
expect(restartBtn).toBeInTheDocument();
61+
fireEvent.click(restartBtn);
62+
// All cells should be empty
63+
screen.getAllByRole('button').forEach(cell => {
64+
expect(cell).toHaveTextContent('');
65+
});
66+
expect(screen.getByText('Player X:')).toBeInTheDocument();
67+
});
68+
});

0 commit comments

Comments
 (0)