Skip to content

Commit

Permalink
PR: Implement sudoku game (#87)
Browse files Browse the repository at this point in the history
Implement sudoku game with timer and check counter 🧩

![image](https://github.com/zpi-2023/senso-frontend/assets/86623851/09afad75-88e0-4046-8683-0b3d220a888e)
  • Loading branch information
kubaczerwinski77 authored Dec 4, 2023
1 parent 9dd6ae5 commit f0b3b2c
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 3 deletions.
218 changes: 218 additions & 0 deletions app/games/sudoku.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { router } from "expo-router";
import { useEffect, useRef, useState } from "react";
import { Alert, Dimensions, TextInput, View } from "react-native"; // eslint-disable-line senso-import-sources
import { Text, Button } from "react-native-paper";

import { actions } from "@/common/actions";
import { useMutation } from "@/common/api";
import { AppRoutes } from "@/common/constants";
import { useI18n } from "@/common/i18n";
import { calculateScore } from "@/common/score";
import { sty } from "@/common/styles";
import { Header } from "@/components";
import {
checkBoard,
emptyCellCount,
generateSolvedSudoku,
generateSudoku,
} from "@/logic/sudoku";

const screen = {
width: Dimensions.get("window").width,
height: Dimensions.get("window").height,
};

const calculateBorder = (rowIndex: number, colIndex: number) => {
let borderStyle = {};

if (rowIndex >= 0 && rowIndex <= 7) {
borderStyle = {
...borderStyle,
borderBottomWidth: rowIndex === 2 || rowIndex === 5 ? 2 : 1,
};
}

if (colIndex >= 0 && colIndex <= 7) {
borderStyle = {
...borderStyle,
borderRightWidth: colIndex === 2 || colIndex === 5 ? 3 : 1,
};
}

return borderStyle;
};

const SudokuGame = () => {
const { t } = useI18n();
const styles = useStyles();
const timer = useRef<NodeJS.Timeout | null>(null);
const [seconds, setSeconds] = useState(0);
const [checks, setChecks] = useState(0);
const [gameStarted, setGameStarted] = useState(true);
const [solvedBoard] = useState<number[][]>(() => generateSolvedSudoku());
const [[cellsToFill, board], setBoard] = useState<
[cellsToFill: { row: number; col: number }[], board: number[][]]
>(() => generateSudoku(emptyCellCount, solvedBoard));
const postNewScore = useMutation("post", "/api/v1/games/{gameName}/score");

// Function to handle digit input
const handleDigitInput = (row: number, col: number, value: number) => {
setBoard((prevBoard) => {
const [prevCellsToFill, prevSudokuBoard] = prevBoard;
const newBoard = [...prevSudokuBoard];
newBoard[row]![col] = value < 1 || value > 9 ? 0 : value;
return [prevCellsToFill, newBoard];
});
};

const showResult = () => {
const result = checkBoard(board);
setChecks((prevChecks) => prevChecks + 1);
if (result) {
setGameStarted(false);
void postNewScore({
params: {
path: {
gameName: "sudoku",
},
},
body: {
score: calculateScore(checks, seconds),
},
});
}
Alert.alert(
result
? t("games.sudoku.alertTitlePositive")
: t("games.sudoku.alertTitleNegative"),
result ? t("games.sudoku.alertDescription", { time: seconds }) : "",
[
{
text: result
? t("games.sudoku.backToMenu")
: t("games.sudoku.continue"),
onPress: () => (result ? router.replace(AppRoutes.Dashboard) : null),
},
],
);
};

useEffect(() => {
if (gameStarted) {
timer.current = setInterval(() => {
setSeconds((prevSeconds) => prevSeconds + 1);
}, 1000);
}

return () => {
if (timer.current) {
clearInterval(timer.current);
}
};
}, [gameStarted]);

return (
<View style={styles.gameWrapper}>
<Header title={t("games.sudoku.pageTitle")} left={actions.goBack} />
<View style={styles.statsLabel}>
<Text style={styles.statsText}>
{t("games.sudoku.timer", { time: seconds })}
</Text>
<Text style={styles.statsText}>
{t("games.sudoku.checkCounter", { checks })}
</Text>
<Button
onPress={showResult}
disabled={board.some((row) => row.some((digit) => digit === 0))}
>
{t("games.sudoku.check")}
</Button>
</View>
<View style={styles.sudokuWrapper}>
{board.map((row, rowIndex) => (
<View
key={rowIndex}
style={{ ...styles.boardRow, ...calculateBorder(rowIndex, 0) }}
>
{row.map((digit, colIndex) => (
<View
key={colIndex}
style={{
...styles.boardCell,
...calculateBorder(rowIndex, colIndex),
}}
>
{cellsToFill.some(
(cell) => cell.row === rowIndex && cell.col === colIndex,
) ? (
<TextInput
style={styles.digitInput}
keyboardType="numeric"
value={
digit === 0 ? "" : board[rowIndex]![colIndex]?.toString()
}
onChangeText={(value) =>
handleDigitInput(
rowIndex,
colIndex,
parseInt(value, 10) || 0,
)
}
/>
) : (
<Text style={styles.digit}>{digit}</Text>
)}
</View>
))}
</View>
))}
</View>
</View>
);
};

const useStyles = sty.themedHook(({ colors }) => ({
gameWrapper: {
flex: 1,
gap: 10,
},
digit: {
fontSize: 30,
color: colors.text,
},
digitInput: {
width: screen.width / 9 - 9,
aspectRatio: 1,
textAlign: "center",
backgroundColor: "lightgray",
borderRadius: 4,
fontSize: 30,
color: "black",
},
statsText: {
fontSize: 18,
},
sudokuWrapper: {
flexDirection: "row",
flexWrap: "wrap",
},
boardRow: {
flexDirection: "row",
borderColor: colors.tertiary,
},
boardCell: {
width: screen.width / 9,
aspectRatio: 1,
justifyContent: "center",
alignItems: "center",
borderColor: colors.tertiary,
},
statsLabel: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginHorizontal: 20,
},
}));

export default SudokuGame;
3 changes: 1 addition & 2 deletions app/games/wordle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const Page = () => {
const lastGuessRef = useRef<View>(null);
const scrollViewRef = useRef<ScrollView>(null);
const [seconds, setSeconds] = useState<number>(0);
const [gameStarted, setGameStarted] = useState<boolean>(false);
const [gameStarted, setGameStarted] = useState<boolean>(true);
const [previousGuesses, setPreviousGuesses] = useState<string[]>([]);
const [currentGuess, setCurrentGuess] = useState<string[]>(
new Array(5).fill(""),
Expand Down Expand Up @@ -122,7 +122,6 @@ const Page = () => {
};

useEffect(() => {
setGameStarted(true);
textInputRefs.current[0]?.focus();
}, []);

Expand Down
40 changes: 40 additions & 0 deletions assets/i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@
"en": "Graydle",
"pl": "Graydle"
},
"actions.playSudokuGame": {
"en": "Sudoku",
"pl": "Sudoku"
},
"actions.manageNotes": {
"en": "Notes",
"pl": "Notatki"
Expand Down Expand Up @@ -525,6 +529,42 @@
"en": "Play again",
"pl": "Zagraj ponownie"
},
"games.sudoku.pageTitle": {
"en": "Sudoku",
"pl": "Sudoku"
},
"games.sudoku.timer": {
"en": "Time: {time}",
"pl": "Czas: {time}"
},
"games.sudoku.checkCounter": {
"en": "Checks: {checks}",
"pl": "Sprawdzenia: {checks}"
},
"games.sudoku.alertTitlePositive": {
"en": "Congratulations!",
"pl": "Gratulacje!"
},
"games.sudoku.alertTitleNegative": {
"en": "Solution is incorrect",
"pl": "Rozwiązanie jest niepoprawne"
},
"games.sudoku.alertDescription": {
"en": "You have completed the game in time {time}.",
"pl": "Ukończyłeś grę w czasie {time}."
},
"games.sudoku.continue": {
"en": "Continue",
"pl": "Kontynuuj"
},
"games.sudoku.backToMenu": {
"en": "Back to menu",
"pl": "Powrót do menu"
},
"games.sudoku.check": {
"en": "Check sudoku",
"pl": "Sprawdź sudoku"
},
"medication.pills": {
"en": "{count} pills",
"en_1": "{count} pill",
Expand Down
6 changes: 6 additions & 0 deletions common/actions/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ export const actions = {
handler: ({ router }) => router.push(AppRoutes.WordleGame),
hidden: ({ identity }) => isCaretaker(identity.profile),
},
playSudokuGame: {
displayName: (t) => t("actions.playSudokuGame"),
icon: "apps",
handler: ({ router }) => router.push(AppRoutes.SudokuGame),
hidden: ({ identity }) => isCaretaker(identity.profile),
},
manageNotes: {
displayName: (t) => t("actions.manageNotes"),
icon: "note",
Expand Down
1 change: 1 addition & 0 deletions common/constants/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const availableGadgets = [
"playGames",
"playMemoryGame",
"playWordleGame",
"playSudokuGame",
"manageNotes",
"quickCreateNote",
"switchProfile",
Expand Down
1 change: 1 addition & 0 deletions common/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const enum AppRoutes {
Games = "/games",
MemoryGame = "/games/memory",
WordleGame = "/games/wordle",
SudokuGame = "/games/sudoku",
NoteList = "/notes",
NoteDetails = "/notes/[noteId]",
CreateNote = "/notes/create",
Expand Down
1 change: 0 additions & 1 deletion common/score.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export const calculateScore = (moves: number, seconds: number) => {
const score = Math.round(1 / (moves * seconds * 1000000));
console.log("score", score);
return score;
};
Loading

0 comments on commit f0b3b2c

Please sign in to comment.