Skip to content

Commit 4c68f8c

Browse files
author
Jonay Pelluz
committed
feature/accent-fixer-game feature: add accentFixer game (Corregidor de acentos)
New ES-only game where players tap the vowel that should carry the accent mark, or indicate the word has none. Adds the AccentGameProcessor util, useAccentFixer hook + UI, route, styles, translations, and the availableLocales mechanism to restrict games to specific locales.
1 parent 13f3500 commit 4c68f8c

15 files changed

Lines changed: 865 additions & 7 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import AccentFixerUI from '@games/accentFixer/UI';
5+
import useAccentFixer from '@games/accentFixer/useAccentFixer';
6+
import { createGamesConfig } from '@hooks/useGamesConfig';
7+
import MainLayout from '@layouts/MainLayout';
8+
import { useWordsContext } from '@store/WordsContext';
9+
import '@styles/AccentFixer.scss';
10+
11+
const AccentFixerPage: React.FC = () => {
12+
const { locale } = useWordsContext();
13+
14+
const gameLogic = useAccentFixer();
15+
const gameConfig = createGamesConfig(locale, 'accentFixer');
16+
17+
return (
18+
<MainLayout>
19+
{gameConfig && <AccentFixerUI gameConfig={gameConfig} {...gameLogic} />}
20+
</MainLayout>
21+
);
22+
};
23+
24+
export default AccentFixerPage;

config/translations/Games.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const GamesRoutes: Translations = {
88
wordFinder: '/juegos/buscador-de-palabras',
99
definitionMaster: '/juegos/maestro-de-las-definiciones',
1010
crossWordPuzzle: '/juegos/crucigramas',
11+
accentFixer: '/juegos/corregidor-de-acentos',
1112
},
1213
en: {
1314
spellTower: '/en/games/spell-tower',
@@ -16,6 +17,8 @@ const GamesRoutes: Translations = {
1617
wordFinder: '/en/games/word-finder',
1718
definitionMaster: '/en/games/definition-master',
1819
crossWordPuzzle: '/en/games/crossword-puzzles',
20+
// ES-only game: no EN page exists, route points at the ES page and is unreachable via the EN UI.
21+
accentFixer: '/juegos/corregidor-de-acentos',
1922
},
2023
};
2124

@@ -325,6 +328,53 @@ const GamesTranslations: GamePreConfig[] = [
325328
},
326329
},
327330
},
331+
{
332+
id: 'accentFixer',
333+
imgSrc: '/images/games/accentFixer.png',
334+
availableLocales: ['es'],
335+
title: {
336+
en: 'Accent Fixer',
337+
es: 'Corregidor de acentos',
338+
},
339+
description: {
340+
en: 'A word is shown without its accent mark and you must tap the vowel that should carry it, or indicate that it has none. Put the rules of agudas, llanas, and esdrújulas to the test.',
341+
es: 'Se muestra una palabra sin tilde y debes pulsar la vocal que debería llevarla, o indicar que no lleva. Pon a prueba las reglas de agudas, llanas y esdrújulas.',
342+
},
343+
subtitle: {
344+
en: 'A game where you decide which vowel carries the accent mark.',
345+
es: 'Un juego donde decides qué vocal lleva la tilde.',
346+
},
347+
gameRules: {
348+
en: {
349+
gameGoal: 'Guess where the accent mark goes in as many words as possible.',
350+
howToPlay: [
351+
'A word without its accent mark will appear.',
352+
'Tap the vowel that should carry the accent mark.',
353+
'If the word has no accent mark, tap the "No accent mark" button.',
354+
'Each correct answer adds one point; each mistake subtracts one.',
355+
],
356+
additionalRules: [],
357+
tips: [
358+
'Remember: agudas words are accented if they end in a vowel, n, or s.',
359+
'Esdrújulas words always carry an accent mark.',
360+
],
361+
},
362+
es: {
363+
gameGoal: 'Acierta dónde va la tilde en el mayor número de palabras posible.',
364+
howToPlay: [
365+
'Aparecerá una palabra sin tilde.',
366+
'Pulsa la vocal que debería llevar tilde.',
367+
'Si la palabra no lleva tilde, pulsa el botón "No lleva tilde".',
368+
'Cada acierto suma un punto; cada fallo resta uno.',
369+
],
370+
additionalRules: [],
371+
tips: [
372+
'Recuerda: las agudas se acentúan si terminan en vocal, n o s.',
373+
'Las esdrújulas siempre llevan tilde.',
374+
],
375+
},
376+
},
377+
},
328378
];
329379

330380
export { GamesTranslations, GamesRoutes };

config/translations/General.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const GeneralTranslations: Translations = {
7777
gameWordBuilderClearAll: 'Clear',
7878
gameCrossWordGenerating: 'Generating crossword...',
7979
gameCrossWordComplete: 'Congratulations! You completed the crossword!',
80+
gameAccentNoAccent: 'No accent mark',
8081
gameQuizFinishedScore: 'You got {score} out of {total} correct!',
8182
headerHome: 'Home',
8283
headerGames: 'Games',
@@ -138,6 +139,7 @@ const GeneralTranslations: Translations = {
138139
gameWordBuilderClearAll: 'Limpiar',
139140
gameCrossWordGenerating: 'Generando crucigrama...',
140141
gameCrossWordComplete: '¡Enhorabuena! ¡Has completado el crucigrama!',
142+
gameAccentNoAccent: 'No lleva tilde',
141143
gameQuizFinishedScore: '¡Acertaste {score} de {total}!',
142144
headerHome: 'Inicio',
143145
headerGames: 'Juegos',

games/accentFixer/UI.tsx

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { FormattedMessage } from 'react-intl';
5+
import { GameConfig } from '@models/types';
6+
import { AccentChallenge } from '@utils/AccentGameProcessor';
7+
import GameRules from '@components/GameRules';
8+
import Hero from '@components/Hero';
9+
import '@styles/Buttons.scss';
10+
11+
type PendingResult = { clickedIndex: number; correctIndex: number };
12+
13+
type AccentFixerUIProps = {
14+
gameConfig: GameConfig;
15+
error: Error | null;
16+
countdown: number;
17+
gameLevel: string | null;
18+
showButton: boolean;
19+
challenges: AccentChallenge[] | null;
20+
currentIndex: number;
21+
gameStarted: boolean;
22+
hasBeenPlayed: boolean;
23+
correctAnswers: number;
24+
incorrectAnswers: [string, string][];
25+
pendingResult: PendingResult | null;
26+
isLoading: boolean;
27+
handleGameStartClick: () => void;
28+
handleVowelClick: (index: number) => void;
29+
handleNoAccentClick: () => void;
30+
};
31+
32+
const UI: React.FC<AccentFixerUIProps> = ({
33+
gameConfig,
34+
error,
35+
countdown,
36+
gameLevel,
37+
showButton,
38+
challenges,
39+
currentIndex,
40+
gameStarted,
41+
hasBeenPlayed,
42+
correctAnswers,
43+
incorrectAnswers,
44+
pendingResult,
45+
isLoading,
46+
handleGameStartClick,
47+
handleVowelClick,
48+
handleNoAccentClick,
49+
}) => {
50+
const challenge = challenges?.[currentIndex] ?? null;
51+
52+
const noAccentClassName = (() => {
53+
if (pendingResult === null) return 'btn-default accent-fixer-no-accent';
54+
if (pendingResult.correctIndex === -1) return 'btn-default accent-fixer-no-accent correct';
55+
if (pendingResult.clickedIndex === -2) return 'btn-default accent-fixer-no-accent incorrect';
56+
return 'btn-default accent-fixer-no-accent';
57+
})();
58+
59+
return (
60+
<>
61+
<Hero
62+
image={gameConfig.imgSrc}
63+
title={gameConfig.title}
64+
subtitle={gameConfig.description}
65+
>
66+
{!gameLevel ? (
67+
<button className="btn-primary game-btn" disabled>
68+
<FormattedMessage id="gameSelectLevel" />
69+
</button>
70+
) : isLoading ? (
71+
<button className="btn-primary game-btn" disabled>
72+
<FormattedMessage id="gamePlay" />
73+
</button>
74+
) : showButton ? (
75+
<button className="btn-primary game-btn" onClick={handleGameStartClick}>
76+
<FormattedMessage id="gamePlay" />
77+
</button>
78+
) : (
79+
<p className="game-timer">
80+
{countdown} <FormattedMessage id="gameSeconds" />
81+
</p>
82+
)}
83+
</Hero>
84+
<div className="accent-fixer-game">
85+
<div className="accent-fixer-game-inner">
86+
{!hasBeenPlayed || !gameLevel ? (
87+
<GameRules {...gameConfig.gameRules} />
88+
) : gameStarted && challenge ? (
89+
<>
90+
<div className="accent-fixer-score">{correctAnswers}</div>
91+
<div className="accent-fixer-word">
92+
{challenge.displayed.split('').map((char, index) => {
93+
const isVowel = challenge.vowelIndices.includes(index);
94+
let className = 'accent-fixer-letter';
95+
if (isVowel) className += ' vowel';
96+
if (pendingResult !== null) {
97+
if (index === pendingResult.correctIndex) className += ' correct';
98+
else if (index === pendingResult.clickedIndex) className += ' incorrect';
99+
}
100+
return (
101+
<button
102+
key={index}
103+
className={className}
104+
disabled={!isVowel || pendingResult !== null}
105+
onClick={() => handleVowelClick(index)}
106+
>
107+
{char}
108+
</button>
109+
);
110+
})}
111+
</div>
112+
<button
113+
className={noAccentClassName}
114+
disabled={pendingResult !== null}
115+
onClick={handleNoAccentClick}
116+
>
117+
<FormattedMessage id="gameAccentNoAccent" />
118+
</button>
119+
</>
120+
) : (
121+
<div className="results-wrapper">
122+
{error && <p className="load-error">{error.message}</p>}
123+
<div>
124+
<em className="results-title">
125+
<FormattedMessage id="incorrectWords" />
126+
</em>
127+
<strong className="results-title text-danger">
128+
{incorrectAnswers.length}
129+
</strong>
130+
</div>
131+
{incorrectAnswers.map(([displayed, original], index) => (
132+
<div key={index}>
133+
<span className="results-ko text-danger">{displayed}</span>
134+
{' → '}
135+
<strong className="results-ok text-success">{original}</strong>
136+
</div>
137+
))}
138+
</div>
139+
)}
140+
</div>
141+
</div>
142+
</>
143+
);
144+
};
145+
146+
export default UI;

0 commit comments

Comments
 (0)