Skip to content

Commit ca59bd9

Browse files
authored
Add files via upload
1 parent 377b301 commit ca59bd9

41 files changed

Lines changed: 10022 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Mateusz Sokola
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,53 @@
1-
# vibecodingames.github.io
2-
wawa
1+
# 2048-in-react
2+
3+
[![Open issues][issues-badge]][issues-url]
4+
[![CI][lint-badge]][lint-url]
5+
[![CI][test-badge]][test-url]
6+
[![TypeScript][typescript-badge]][typescript-url]
7+
8+
This is a fully functional clone of the popular 2048 game, built using React and Next.js. Not only does it offer smooth animations and works on mobile devices, but it's also a fantastic learning resource for developers. Whether you're here to play, contribute, or learn, this project has something for everyone.
9+
10+
If you're interested in mastering React by building this game step-by-step, check out the course linked below!
11+
12+
[![](.docs/demo.gif)](https://mateuszsokola.github.io/2048-in-react/)
13+
14+
## [Play 2048 💥](https://mateuszsokola.github.io/2048-in-react/)
15+
16+
## Features
17+
18+
- Fully-functional 2048 clone
19+
- Animations
20+
- Supports **keyboard** and **touch** events
21+
22+
## Development
23+
24+
_Easily set up a local development environment!_
25+
26+
Just start dev server on [localhost](http://localhost:3000):
27+
28+
- clone
29+
- `npm install`
30+
- `npm run dev`
31+
32+
**Start coding!** 🎉
33+
34+
## Build your own 2048 Game! 🚀
35+
36+
Want to learn how to build this game from scratch using React & Next.js? I've got you covered! This project is part of an online course where I guide you through the entire process, step-by-step.
37+
38+
Whether you're a beginner looking to enhance your skills or an experienced developer seeking a fun project, this course will take you through the core concepts of React while building a fully functional game.
39+
40+
[![Build 2048 Game in React](https://assets.mateu.sh/assets/github-2048-in-react-readme)](https://assets.mateu.sh/r/github-2048-in-react-readme)
41+
42+
## Support
43+
44+
If you encounter any issues or have suggestions, feel free to open an issue. Your feedback is always appreciated!
45+
46+
[lint-badge]: https://github.com/mateuszsokola/2048-in-react/actions/workflows/lint.yml/badge.svg
47+
[lint-url]: https://github.com/mateuszsokola/2048-in-react/actions/workflows/actions/workflows/lint.yml
48+
[test-badge]: https://github.com/mateuszsokola/2048-in-react/actions/workflows/test.yml/badge.svg
49+
[test-url]: https://github.com/mateuszsokola/2048-in-react/actions/workflows/test.yml
50+
[issues-badge]: https://img.shields.io/github/issues/mateuszsokola/2048-in-react
51+
[issues-url]: https://github.com/mateuszsokola/2048-in-react/issues
52+
[typescript-badge]: https://badges.frapsoft.com/typescript/code/typescript.svg?v=101
53+
[typescript-url]: https://github.com/microsoft/TypeScript
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { render } from "@testing-library/react";
2+
import Board from "@/components/board";
3+
import GameProvider from "@/context/game-context";
4+
5+
describe("Board", () => {
6+
it("should render board with 16 cells", () => {
7+
const { container } = render(
8+
<GameProvider>
9+
<Board />
10+
</GameProvider>,
11+
);
12+
const cellElements = container.querySelectorAll(".cell");
13+
14+
expect(cellElements.length).toEqual(16);
15+
});
16+
17+
it("should render board with 2 tiles", async () => {
18+
const { container } = render(
19+
<GameProvider>
20+
<Board />
21+
</GameProvider>,
22+
);
23+
const tiles = container.querySelectorAll(".tile");
24+
25+
expect(tiles.length).toEqual(2);
26+
});
27+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from "react";
2+
import { render, fireEvent } from "@testing-library/react";
3+
import MobileSwiper, { SwipeInput } from "@/components/mobile-swiper";
4+
5+
describe("MobileSwiper", () => {
6+
it("should trigger onSwipe with correct input on touch end", () => {
7+
const onSwipeMock = jest.fn();
8+
const { container } = render(
9+
<MobileSwiper onSwipe={onSwipeMock}>
10+
<div data-testid="swipe-target">Swipe me!</div>
11+
</MobileSwiper>,
12+
);
13+
14+
const swipeTarget = container.querySelector(
15+
"[data-testid='swipe-target']",
16+
) as HTMLElement;
17+
18+
fireEvent.touchStart(swipeTarget, {
19+
touches: [{ clientX: 0, clientY: 0 }],
20+
});
21+
fireEvent.touchEnd(swipeTarget, {
22+
changedTouches: [{ clientX: 50, clientY: 0 }],
23+
});
24+
25+
expect(onSwipeMock).toHaveBeenCalledWith({ deltaX: 50, deltaY: 0 });
26+
});
27+
28+
it("should not trigger onSwipe if touch is outside the component", () => {
29+
const onSwipeMock = jest.fn();
30+
const { container } = render(
31+
<MobileSwiper onSwipe={onSwipeMock}>
32+
<div data-testid="swipe-target">Swipe me!</div>
33+
</MobileSwiper>,
34+
);
35+
36+
const swipeTarget = container.querySelector(
37+
"[data-testid='swipe-target']",
38+
) as HTMLElement;
39+
40+
fireEvent.touchStart(swipeTarget, {
41+
touches: [{ clientX: 0, clientY: 0 }],
42+
});
43+
fireEvent.touchEnd(document.body, {
44+
changedTouches: [{ clientX: 50, clientY: 0 }],
45+
});
46+
47+
expect(onSwipeMock).not.toHaveBeenCalled();
48+
});
49+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { render } from "@testing-library/react";
2+
import GameProvider from "@/context/game-context";
3+
import Board from "@/components/board";
4+
import Score from "@/components/score";
5+
6+
describe("Score", () => {
7+
it("should display score", () => {
8+
const { container } = render(
9+
<GameProvider>
10+
<Score />
11+
<Board />
12+
</GameProvider>,
13+
);
14+
15+
expect(container.querySelector(".score > div")?.textContent).toEqual("0");
16+
});
17+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { render, waitFor } from "@testing-library/react";
2+
import Tile from "@/components/tile";
3+
4+
describe("Tile", () => {
5+
beforeEach(() => {
6+
jest.useFakeTimers();
7+
});
8+
9+
it("should render with correct position and value", () => {
10+
// Arrange
11+
const position: [number, number] = [1, 2];
12+
const value = 2048;
13+
14+
// Act
15+
const { container } = render(<Tile position={position} value={value} />);
16+
17+
const tile: HTMLDivElement = container.firstChild as HTMLDivElement;
18+
expect(tile.textContent).toEqual("2048");
19+
expect(tile.className).toEqual("tile tile2048");
20+
expect(tile).toHaveStyle({
21+
left: `72px`,
22+
top: `144px`,
23+
zIndex: `2048`,
24+
});
25+
});
26+
27+
it("should apply animation when value changes", async () => {
28+
// Arrange
29+
const position: [number, number] = [0, 0];
30+
const initialValue = 2;
31+
const updatedValue = 4;
32+
33+
// Act
34+
const { getByText, rerender } = render(
35+
<Tile position={position} value={initialValue} />,
36+
);
37+
38+
// Assert initial state
39+
const tileElement = getByText(`${initialValue}`);
40+
expect(tileElement).toBeInTheDocument();
41+
42+
await waitFor(() => {
43+
expect(tileElement).toHaveStyle({
44+
transform: "scale(1)",
45+
});
46+
});
47+
rerender(<Tile position={position} value={updatedValue} />);
48+
49+
await waitFor(() => {
50+
expect(tileElement).toHaveStyle({
51+
transform: "scale(1.1)",
52+
});
53+
});
54+
55+
await waitFor(() => {
56+
expect(tileElement).toHaveStyle({
57+
transform: "scale(1)",
58+
});
59+
});
60+
});
61+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { fireEvent, render } from "@testing-library/react";
2+
import GameProvider from "@/context/game-context";
3+
import Board from "@/components/board";
4+
import Score from "@/components/score";
5+
6+
describe("GameProvider", () => {
7+
describe("startGame", () => {
8+
it("should start the game with two tiles", () => {
9+
const { container } = render(
10+
<GameProvider>
11+
<Board />
12+
</GameProvider>,
13+
);
14+
15+
expect(container.querySelectorAll(".tile")).toHaveLength(2);
16+
});
17+
});
18+
19+
describe("getTiles", () => {
20+
it("should return tiles", () => {
21+
const { container } = render(
22+
<GameProvider>
23+
<Board />
24+
</GameProvider>,
25+
);
26+
27+
expect(container.querySelectorAll(".tile")).toHaveLength(2);
28+
});
29+
});
30+
31+
describe("moveTiles", () => {
32+
it("should move tiles and merge them together", () => {
33+
const { container } = render(
34+
<GameProvider>
35+
<Board />
36+
</GameProvider>,
37+
);
38+
39+
expect(container.querySelectorAll(".tile4")).toHaveLength(0);
40+
expect(container.querySelectorAll(".tile2")).toHaveLength(2);
41+
42+
fireEvent.keyDown(container, {
43+
key: "ArrowUp",
44+
code: "ArrowUp",
45+
});
46+
47+
expect(container.querySelectorAll(".tile4")).toHaveLength(1);
48+
expect(container.querySelectorAll(".tile2")).toHaveLength(1);
49+
});
50+
});
51+
52+
describe("score", () => {
53+
it("should return score", () => {
54+
const { container } = render(
55+
<GameProvider>
56+
<Score />
57+
<Board />
58+
</GameProvider>,
59+
);
60+
61+
expect(container.querySelector(".score > div")?.textContent).toEqual("0");
62+
});
63+
64+
it("should refresh score after move", () => {
65+
const { container } = render(
66+
<GameProvider>
67+
<Score />
68+
<Board />
69+
</GameProvider>,
70+
);
71+
72+
expect(container.querySelector(".score > div")?.textContent).toEqual("0");
73+
74+
fireEvent.keyDown(container, {
75+
key: "ArrowUp",
76+
code: "ArrowUp",
77+
});
78+
79+
expect(container.querySelector(".score > div")?.textContent).toEqual("4");
80+
});
81+
});
82+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { renderHook } from "@testing-library/react";
2+
import usePreviousProps from "@/hooks/use-previous-props";
3+
4+
describe("usePreviousProps", () => {
5+
it("should return undefined for the first render", () => {
6+
const { result } = renderHook(() => usePreviousProps("initial value"));
7+
8+
expect(result.current).toBeUndefined();
9+
});
10+
11+
it("should return the previous value of the prop", () => {
12+
const { result, rerender } = renderHook(
13+
({ value }) => usePreviousProps(value),
14+
{ initialProps: { value: "initial value" } },
15+
);
16+
17+
rerender({ value: "updated value" });
18+
19+
expect(result.current).toEqual("initial value");
20+
});
21+
});

0 commit comments

Comments
 (0)