Skip to content
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
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
36 changes: 36 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"useTabs": true,
"tabWidth": 4
}
129 changes: 89 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,106 @@
# OpenDevEd-Wordle
## Requirements:
Your task is to create a web-based Wordle game using React that adheres to the following specifications:
## seiWordle

### User Interface (UI):
seiWordle is a word-guessing game inspired by Wordle built with TypeScript and Next.js.

Design a clean and intuitive UI for the game that includes:
## Thought Process

- Input field for guessing words.
- Submit button to submit the guess.
- Display area for previous guesses.
= Indication of correct letters in correct positions (right letter, right position).
- Indication of correct letters in the wrong position.
- Display remaining attempts.
- End game state UI (upon winning or losing).
State management was handled using React's built-in `useState`, `useEffect`, and `useMemo` hooks spread across `useGameState` and `useGridState` hooks, and the most important state variables are:

### State Management:
* `randomWord` is a state that is initialized to a random word with a `useEffect`,
* `attempts` is an array of strings that contains the user's previous attempts at guessing the random word, the length of which directly translates to the number of remaining attempts,
* `currentString` is a string that contains the user's current input, appended to the `attempts` array when the user presses the enter key,
* `attemptColors` is 2D array of strings that contains the color of each letter in each attempt, set by a `useMemo` hook that calculates the colors at the same time as the `attempts` array is updated.
* `useAudio` is a hook that provides a function to play sound effects, and is used to play sound effects on user interaction. The `useAudio` hook returns an object with the following type:
```typescript
type AudioContextReturn = {
playSound: (
sound: keyof typeof SFX,
volume?: number,
skipAhead?: number,
) => void;
};
```
* `resetGame` is a function stored in a state variable set by a `useMemo` hook that resets the game state to its initial state. Since resetting the game requires state from `useGridState`, and the grid is a child of a component that also needs to reset the game, the `setResetGame` function is passed down to to `<Grid />` as a prop, allowing the app to reset the game's state on the end game state UI.

Implement a robust state management system to handle:
The latter could've been improved upon by using `useContext` or a state management library like Redux, but this was deemed unnecessary especially given the small size of the app.

- Target word selection (randomly generate a word at the start of the game).
- Storing user guesses and their results.
- Tracking remaining attempts.
Many decisions were made with the goal of creating a smooth and intuitive user experience, such as capturing user input and storing it in state rather than using a traditional `input` field, and opting for a single page application to achieve a more seamless experience. The initial word matching logic did not account for duplicate same correct letter guesses in words with only one instance of that letter and was later updated to account for this edge case. There were also some issues with playing multiple audio instances at the same time due the initial implementation of the audio system as well as audio files not being preloaded, which was later resolved by using the Audio API to create a new audio instance for each sound effect and using the `preload` attribute to ensure the audio files are loaded before they are played.

### User Interaction:
Much like the original Wordle, seiWordle uses two dictionaries:
* a large set of only valid words that will never be used as the target word, and
* a smaller set of common words.

- Capture user input for word guesses.
- Validate input (alphabetic characters, word length, etc.).
- Handle the submission of guesses and update the game state accordingly.
The valid words dictionary is used to check the validity of the user's guess, while the common words dictionary is used to generate the target random word.

## Features

### Game Logic:
### User Interface

- Compare the user's guessed word against the target word.
- Provide feedback to the user about the correctness of the guessed word.
- End the game when the correct word is guessed or when the attempts reach zero.
* Intuitive user interface with pixel perfect spacing.
* Smooth animations to elevate interactivity and feedback.
* Built to be completely mobile friendly.

## Code Quality:
### User Experience

- Write clean, readable, and maintainable code.
- Implement best practices for React development.
- Ensure error handling for edge cases (invalid input, unexpected behavior).
* A clear display of correctly positioned letters making it easier to focus on the task at hand.
* An on-screen keyboard not only enabling mobile play, but also clearly displaying used and unused characters.
* Color coding for right letters in their correct positions, correct letters in wrong positions and incorrect letters.
* The ability to start a new round without taking your hands off the keyboard.

## Submission Guidelines:
### Gameplay

- Fork this [repository](https://github.com/OpenDevEd/OpenDevEd-wordle/)) and create a new branch named `wordle-[YOUR NAME]`.
- Provide clear instructions on how to run the application locally.
- Include a README file explaining your approach, decisions made, and any additional features implemented.
- Open a PR.
* A variety of sound effects for enhanced immersion.
* The ability to request hints on a letter by letter basis.
* Each attempt is saved in localStorage and cleanly displayed, providing a simple but effective history system.

## Evaluation Criteria:
### Theming

- UI/UX design and functionality.
- Code quality, structure, and maintainability.
- State management and logic implementation.
- Handling of edge cases and error scenarios.
- Bonus points for additional features or optimizations.
* Automatic light & dark theme switching based on system preferences.
* Harmonious color palette with a focus on contrast and readability.

## Technologies

* TypeScript
* Next.js 14
* React 18
* Framer Motion
* Tailwind CSS
* Jest

## Installation

1. Clone the repository

```bash
git clone https://github.com/sickerine/wordle-sickerine.git
```

2. Navigate to the project directory

```bash
cd wordle-sickerine
```

3. Switch to the wordle-sickerine branch

```bash
git checkout wordle-sickerine
```

4. Install dependencies

```bash
npm install
```

5. Build the app

```bash
npm run build
```

6. Start a production server

```bash
npm run start
```
52 changes: 52 additions & 0 deletions __tests__/Audio.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe } from "node:test";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
import MainContainer from "@/components/MainContainer";

describe("Audio", () => {
let audioMock: typeof Audio;

beforeEach(() => {
audioMock = global.Audio;
});

afterEach(() => {
global.Audio = audioMock;
});

const correctWord = "tests";

test("should play the correct sound when there's no hint to reveal anymore", async () => {
const ret = render(<MainContainer presetWord={correctWord} />);
for (let i = 0; i < correctWord.length; i++)
await userEvent.click(screen.getByTestId("hint-button"));
global.Audio = jest.fn().mockImplementation((v) => ({
play: () => {
expect(v).toBe("/deny.mp3");
},
}));
await userEvent.click(screen.getByTestId("hint-button"));
});

test("should play the correct sound when typing", async () => {
render(<MainContainer presetWord={correctWord} />);
global.Audio = jest.fn().mockImplementation((v) => ({
play: () => {
expect(v).toBe("/typing.wav");
},
}));
await userEvent.keyboard("a");
});

test("should play the correct sound when backspacing", async () => {
render(<MainContainer presetWord={correctWord} />);
await userEvent.keyboard("a");
global.Audio = jest.fn().mockImplementation((v) => ({
play: () => {
expect(v).toBe("/backspace.wav");
},
}));
await userEvent.keyboard("{Backspace}");
});
});
20 changes: 20 additions & 0 deletions __tests__/CharacterBox.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe } from "node:test";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import CharacterBox from "@/components/CharacterBox";

describe("CharacterBox", () => {
test("should render the correct letter and the correct color", () => {
render(<CharacterBox
attemptColors={[["yellow", "yellow", "gray", "gray", "green"]]}
attempts={["balls"]}
randomWord="abyss"
columnIndex={2}
rowIndex={0}
currentString=""
keysDown={new Map()}
/>)
expect(screen.getByTestId(`column-2`)).toHaveTextContent("l");
expect(screen.getByTestId(`column-2`)).toHaveClass("!bg-card-400");
})
});
Loading