Skip to content
Snippets Groups Projects
Commit 8d41ca38 authored by JEAN Lucas's avatar JEAN Lucas
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
Pipeline #26456 passed
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
image: node:latest
stages:
- test
cache:
paths:
- node_modules/
test:
stage: test
script:
- npm install
- npm test
# Puissance 4
## Consignes :
Date limite de rendu : 08/12/2023
Evaluation : individuelle
Envoi du repo git vers "ludovic.decampy@gmail.com" avec un lien vers le repo qui doit être en public.
Sujet:
Puissance 4
Grille : 7x6
A B C D E F G
O O O O O O O
O O O O O O O
O O O O O O O
O O O O O O O
O O O O O O O
O O O O O O O
Notion de colonne ou on joue
Quand il met le jeton, il tombe au plus bas possible : Gravité !!!!
Règle de on gagne:
4 jetons alignés (vertical, horizontal, diagonal)
Cas du match nul : La grille est complète, aucun gagnant, le jeu s'arrête
Reverse, si les deux joueurs gagne => match nul ccx
Rendu: 08/12/2023
Bonus:
Reverse d'une colonne
Reverse de colonne : la gravité se réaplique
Bonus 2:
Random sur le reverse d'une colonne
Bonus 3:
Limiter le nombre de reverse possible
Bonus 4:
Multijoueur
## Différentes fonctionnalités intégrées :
- [x] Grille 7x6
- [x] 2 joueurs
- [x] 4 jetons alignés (vertical, horizontal, diagonal) = win
- [x] Match nul en cas d'égalité ou d'impossibilité de victoire
- [x] Reverse de gravité
- [x] Reverse de colonne
- [x] Test unitaire
- [x] CI/CD
### Commandes :
- ```npm install```
- ```npm start```
- ```npm test```
### Tests :
Les tests sont fait avec Jest et permette de tester les fonctionnalitées du puissance 4.
- [x] fonctionnalité de grille 7x6
- [x] fonctionnalité de grille remplie
- [x] fonctionnalité de grille incomplète
- [x] fonctionnalité de victoire
- [x] fonctionnalité de match nul
- [x] fonctionnalité de reverse de gravité
- [x] fonctionnalité de reverse de colonne
### Pipeline :
La pipeline est dans le fichier .gitlab-ci.yml et se lance automatiquement à chaque push sur la branche main
- Test du code et déploiement sur le serveur etulab tout les jours à 9h.
Source diff could not be displayed: it is too large. Options to address this: view the blob.
{
"name": "puissance-4",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
public/favicon.ico

266 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Puissance 4</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
public/logo192.png

5.22 KiB

public/logo512.png

9.44 KiB

{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
import React, { useState } from "react";
import Grid from "./components/Grid";
import Modal from "./components/Modal";
import Header from "./components/Header";
import Player from "./components/Player";
import Switch from "./images/switch.png";
import "./styles/main.css";
import RealoadButton from "./components/ReloadButton";
import { createEmptyGrid, checkForDraw, checkForWin } from "./GameLogic";
function App() {
const [grid, setGrid] = useState(createEmptyGrid());
const [currentPlayer, setCurrentPlayer] = useState("rouge");
const [winner, setWinner] = useState(null);
const [message, setMessage] = useState("");
const [reversalsRemaining, setReversalsRemaining] = useState({
rouge: 3,
jaune: 3,
});
const playTurn = (columnIndex) => {
if (winner) return;
const newGrid = [...grid];
let rowPlaced;
for (let row = newGrid.length - 1; row >= 0; row--) {
if (newGrid[row][columnIndex] === null) {
newGrid[row][columnIndex] = currentPlayer;
rowPlaced = row;
break;
}
}
if (rowPlaced !== undefined) {
setGrid(newGrid);
checkForWinner();
}
};
const checkForWinner = () => {
const currentPlayerWins = checkForWin(grid, currentPlayer);
const otherPlayer = currentPlayer === "rouge" ? "jaune" : "rouge";
const otherPlayerWins = checkForWin(grid, otherPlayer);
if (currentPlayerWins && otherPlayerWins) {
setWinner("Match nul");
setMessage("Match nul. Les deux joueurs ont gagné !");
} else if (currentPlayerWins) {
setWinner(currentPlayer);
setMessage(`Bravo, le joueur ${currentPlayer} a gagné !`);
} else if (otherPlayerWins) {
setWinner(otherPlayer);
setMessage(`Bravo, le joueur ${otherPlayer} a gagné !`);
} else if (checkForDraw(grid)) {
setWinner("Match nul");
setMessage("Match nul. Essayez encore une fois !");
} else {
setCurrentPlayer(currentPlayer === "rouge" ? "jaune" : "rouge");
}
};
const reverseColumnsWithGravity = (player) => {
if (reversalsRemaining[player] > 0) {
const newGrid = [...grid];
for (let col = 0; col < newGrid[0].length; col++) {
const column = newGrid.map((row) => row[col]);
const reversedColumn = column.reverse();
reversedColumn.forEach((value, row) => (newGrid[row][col] = value));
}
setGrid(newGrid);
setReversalsRemaining({
...reversalsRemaining,
[player]: reversalsRemaining[player] - 1,
});
setCurrentPlayer(currentPlayer === "rouge" ? "jaune" : "rouge");
checkForWinner();
}
};
const reverseByColumn = (player, columnIndex) => {
if (reversalsRemaining[player] > 0) {
const newGrid = [...grid];
const column = newGrid.map((row) => row[columnIndex]);
const reversedColumn = column.reverse();
reversedColumn.forEach(
(value, row) => (newGrid[row][columnIndex] = value)
);
setGrid(newGrid);
setReversalsRemaining({
...reversalsRemaining,
[player]: reversalsRemaining[player] - 1,
});
setCurrentPlayer(currentPlayer === "rouge" ? "jaune" : "rouge");
checkForWinner();
}
};
const restartGame = () => {
setGrid(createEmptyGrid());
setCurrentPlayer("rouge");
setWinner(null);
setMessage("");
setReversalsRemaining({ rouge: 3, jaune: 3 });
};
return (
<div className="game-container app">
<Header />
<div className="players">
<Player
player={{
name: "Joueur 1",
color: "red",
reversals: reversalsRemaining.rouge,
}}
isCurrentPlayer={currentPlayer === "rouge"}
onReverse={() => reverseColumnsWithGravity("rouge")}
/>
<div className="grid-prime">
<div className="switch-home">
<RealoadButton onRestart={restartGame} />
</div>
<Grid grid={grid} playTurn={playTurn} />
<div className="reversal-buttons">
<button
className="reverse-button"
onClick={() => reverseByColumn(currentPlayer, 0)}
>
{/* image switch*/}
<img className="switch" src={Switch} alt="switch" />
</button>
<button
className="reverse-button"
onClick={() => reverseByColumn(currentPlayer, 1)}
>
<img className="switch" src={Switch} alt="switch" />
</button>
<button
className="reverse-button"
onClick={() => reverseByColumn(currentPlayer, 2)}
>
<img className="switch" src={Switch} alt="switch" />
</button>
<button
className="reverse-button"
onClick={() => reverseByColumn(currentPlayer, 3)}
>
<img className="switch" src={Switch} alt="switch" />
</button>
<button
className="reverse-button"
onClick={() => reverseByColumn(currentPlayer, 4)}
>
<img className="switch" src={Switch} alt="switch" />
</button>
<button
className="reverse-button"
onClick={() => reverseByColumn(currentPlayer, 5)}
>
<img className="switch" src={Switch} alt="switch" />
</button>
<button
className="reverse-button"
onClick={() => reverseByColumn(currentPlayer, 6)}
>
<img className="switch" src={Switch} alt="switch" />
</button>
</div>
</div>
<Player
player={{
name: "Joueur 2",
color: "#ffe102",
reversals: reversalsRemaining.jaune,
}}
isCurrentPlayer={currentPlayer === "jaune"}
onReverse={() => reverseColumnsWithGravity("jaune")}
/>
</div>
{winner && <Modal message={message} onRestart={restartGame} />}
</div>
);
}
export default App;
import {
createEmptyGrid,
checkForDraw,
checkForWin,
reverseByColumn,
} from "./GameLogic";
import { App } from "./App";
import React from "react";
import { render, fireEvent, waitFor, cleanup } from "@testing-library/react";
describe("createEmptyGrid", () => {
it("Grille taille conforme", () => {
const grid = createEmptyGrid();
expect(grid).toHaveLength(6);
grid.forEach((row) => expect(row).toHaveLength(7));
});
});
describe("checkForDraw", () => {
it("Grille remplie", () => {
const fullGrid = [
["X", "O", "X", "O", "X", "O", "X"],
["X", "O", "X", "O", "X", "O", "X"],
["X", "O", "X", "O", "X", "O", "X"],
["X", "O", "X", "O", "X", "O", "X"],
["X", "O", "X", "O", "X", "O", "X"],
["X", "O", "X", "O", "X", "O", "X"],
];
const result = checkForDraw(fullGrid);
expect(result).toBe(true);
});
it("Grille incomplète", () => {
const incompleteGrid = [
["X", "O", "X", "O", "X", "O", "X"],
["X", "O", "X", "O", "X", "O", "X"],
["X", "O", "X", "O", "X", "O", "X"],
["X", "O", "X", null, "X", "O", "X"],
["X", "O", "X", "O", "X", "O", "X"],
["X", "O", "X", "O", "X", "O", "X"],
];
const result = checkForDraw(incompleteGrid);
expect(result).toBe(false);
});
});
describe("checkForWin", () => {
it("Victoire du joueur rouge", () => {
const grid = [
["rouge", "jaune", "rouge", "jaune", "rouge", "jaune", "rouge"],
["rouge", "jaune", "rouge", "jaune", "rouge", "jaune", "rouge"],
["rouge", "jaune", "rouge", "jaune", "rouge", "rouge", "rouge"],
["jaune", "rouge", "jaune", "rouge", "jaune", "rouge", "jaune"],
["rouge", "jaune", "rouge", "jaune", "rouge", "jaune", "rouge"],
["jaune", "rouge", "jaune", "rouge", "jaune", "rouge", "jaune"],
];
const result = checkForWin(grid, "rouge");
expect(result).toBe(true);
});
});
//Match null
describe("checkForWin", () => {
it("Match nul", () => {
const grid = [
["rouge", "jaune", "rouge", "jaune", "rouge", "jaune", "rouge"],
["rouge", "jaune", "rouge", "jaune", "rouge", "jaune", "rouge"],
["rouge", "jaune", "rouge", "jaune", "rouge", "rouge", "rouge"],
["jaune", "rouge", "jaune", "rouge", "jaune", "rouge", "jaune"],
["rouge", "jaune", "rouge", "jaune", "rouge", "jaune", "rouge"],
["jaune", "rouge", "jaune", "rouge", "jaune", "rouge", "jaune"],
];
const result = checkForWin(grid, "rouge");
expect(result).toBe(true);
const result2 = checkForWin(grid, "jaune");
expect(result2).toBe(true);
if (result && result2) {
expect(result).toBe(true);
}
});
});
describe("reverseByColumn", () => {
it("Grille inversée", () => {
const grid = [
["rouge", "jaune", "rouge", "jaune", "rouge", "jaune", "rouge"],
["rouge", "jaune", "rouge", "jaune", "rouge", "jaune", "rouge"],
["rouge", "jaune", "rouge", "jaune", "rouge", "rouge", "rouge"],
["jaune", "rouge", "jaune", "rouge", "jaune", "rouge", "jaune"],
["rouge", "jaune", "rouge", "jaune", "rouge", "jaune", "rouge"],
["jaune", "rouge", "jaune", "rouge", "jaune", "rouge", "jaune"],
];
const result = reverseByColumn(grid, 0);
expect(result).toBe(true);
console.log(grid);
});
});
export function createEmptyGrid() {
return new Array(6).fill(null).map(() => new Array(7).fill(null));
}
export function checkForDraw(grid) {
for (let row = 0; row < grid.length; row++) {
for (let col = 0; col < grid[row].length; col++) {
if (grid[row][col] === null) {
return false;
}
}
}
return true;
}
export function checkForWin(grid, currentPlayer) {
const rowCount = grid.length;
const columnCount = grid[0].length;
for (let row = 0; row < rowCount; row++) {
for (let col = 0; col <= columnCount - 4; col++) {
if (
grid[row][col] === currentPlayer &&
grid[row][col + 1] === currentPlayer &&
grid[row][col + 2] === currentPlayer &&
grid[row][col + 3] === currentPlayer
) {
return true;
}
}
}
for (let col = 0; col < columnCount; col++) {
for (let row = 0; row <= rowCount - 4; row++) {
if (
grid[row][col] === currentPlayer &&
grid[row + 1][col] === currentPlayer &&
grid[row + 2][col] === currentPlayer &&
grid[row + 3][col] === currentPlayer
) {
return true;
}
}
}
for (let row = 3; row < rowCount; row++) {
for (let col = 0; col <= columnCount - 4; col++) {
if (
grid[row][col] === currentPlayer &&
grid[row - 1][col + 1] === currentPlayer &&
grid[row - 2][col + 2] === currentPlayer &&
grid[row - 3][col + 3] === currentPlayer
) {
return true;
}
}
}
for (let row = 0; row <= rowCount - 4; row++) {
for (let col = 0; col <= columnCount - 4; col++) {
if (
grid[row][col] === currentPlayer &&
grid[row + 1][col + 1] === currentPlayer &&
grid[row + 2][col + 2] === currentPlayer &&
grid[row + 3][col + 3] === currentPlayer
) {
return true;
}
}
}
return false;
}
export function reverseByColumn(grid, columnIndex) {
for (let i = 0; i < grid.length / 2; i++) {
const temp = grid[i][columnIndex];
grid[i][columnIndex] = grid[grid.length - 1 - i][columnIndex];
grid[grid.length - 1 - i][columnIndex] = temp;
}
return true;
}
import React from "react";
function Cell({ color, rowIndex }) {
const cellClass = color ? `cell ${color}` : "cell";
return <div className={cellClass} data-testid={`cell-${rowIndex}`} />;
}
export default Cell;
import React from "react";
import Cell from "./Cell";
function Column({ column, playTurn, columnIndex }) {
return (
<div
className="column"
onClick={playTurn}
data-testid={`column-${columnIndex}`}
>
{column
.map((cell, rowIndex) => (
<Cell key={rowIndex} color={cell} rowIndex={rowIndex} />
))
.reverse()}{" "}
</div>
);
}
export default Column;
import React from "react";
import Column from "./Column";
function Grid({ grid, playTurn }) {
return (
<div className="grid">
{grid[0].map(
(_, columnIndex) =>
columnIndex < 7 && (
<Column
key={columnIndex}
column={grid.map((row) => row[columnIndex])}
playTurn={() => playTurn(columnIndex)}
columnIndex={columnIndex}
/>
)
)}
</div>
);
}
export default Grid;
import React from "react";
import "../styles/main.css";
function Header() {
return (
<div className="header">
<div className="title">
<h1>Puissance 4</h1>
</div>
</div>
);
}
export default Header;
import React from "react";
import RealoadButton from "./ReloadButton";
function Modal({ message, onRestart }) {
return (
<div className="modal">
<div className="modal-content">
<h2>{message}</h2>
<RealoadButton onRestart={onRestart}></RealoadButton>
</div>
</div>
);
}
export default Modal;
import React from "react";
function Player({ player, isCurrentPlayer, onReverse }) {
const handleClick = () => {
if (isCurrentPlayer && onReverse) {
onReverse();
}
};
return (
<div
className={`player ${isCurrentPlayer ? "current-player" : "none"}`}
style={{ border: player.color + " 1px solid" }}
onClick={handleClick}
data-testid="player"
>
<p className="player-name">{player.name}</p>
<div className="jeton" style={{ backgroundColor: player.color }}>
<span className="bonus-count">{player.reversals}</span>
</div>
</div>
);
}
export default Player;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment