From acb7d1bd0e3b6f1602a1a0efbbc7f7f753f1a5b6 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira Date: Sat, 12 Aug 2023 12:35:58 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F:=20Refactor=20the=20'high-sc?= =?UTF-8?q?ore'=20feature=20into=20a=20react=20context.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../high-score/high-score.context.tsx | 45 +++++++ .../features/high-score/high-score.hooks.tsx | 15 --- source/features/high-score/high-score.ts | 110 ++++++++---------- .../features/high-score/high-score.types.ts | 5 + source/features/high-score/index.ts | 4 +- source/features/options/options.context.tsx | 9 +- source/features/options/options.ts | 10 +- source/index.tsx | 9 -- source/pages/game-page/game-page.hooks.tsx | 7 +- source/pages/high-score-page.tsx | 8 +- source/pages/root-page.tsx | 11 +- 11 files changed, 122 insertions(+), 111 deletions(-) create mode 100644 source/features/high-score/high-score.context.tsx delete mode 100644 source/features/high-score/high-score.hooks.tsx create mode 100644 source/features/high-score/high-score.types.ts diff --git a/source/features/high-score/high-score.context.tsx b/source/features/high-score/high-score.context.tsx new file mode 100644 index 0000000..0ba9975 --- /dev/null +++ b/source/features/high-score/high-score.context.tsx @@ -0,0 +1,45 @@ +import { createContext, useRef } from "react"; +import { HighScore } from "./high-score"; +import { saveObject } from "@drk4/utilities"; +import { getData } from "../../core/data"; +import { ScoreData } from "./high-score.types"; + +export type HighScoreContextValue = { + getScores: () => ScoreData[]; + addScore: (score: ScoreData) => number | undefined; +}; + +interface HighScoreContextProviderProps { + children: React.ReactNode; +} + +const STORAGE_KEY = "tetris_high_score"; + +export const HighScoreContext = createContext({ + getScores: () => [], + addScore: () => { + return undefined; + }, +}); + +export function HighScoreContextProvider({ + children, +}: HighScoreContextProviderProps) { + const highScore = useRef(new HighScore(getData(STORAGE_KEY))); + + const value = { + addScore: (score: ScoreData) => { + const position = highScore.current.add(score); + saveObject(STORAGE_KEY, highScore.current.getHighScores()); + + return position; + }, + getScores: () => highScore.current.getHighScores(), + }; + + return ( + + {children} + + ); +} diff --git a/source/features/high-score/high-score.hooks.tsx b/source/features/high-score/high-score.hooks.tsx deleted file mode 100644 index a0feefa..0000000 --- a/source/features/high-score/high-score.hooks.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useEffect, useState } from "react"; -import { ScoreData, getHighScores } from "./high-score"; - -export function useHighScore() { - const [scores, setScores] = useState([]); - - useEffect(() => { - const initialScores = getHighScores(); - setScores(initialScores); - }, []); - - return { - scores, - }; -} diff --git a/source/features/high-score/high-score.ts b/source/features/high-score/high-score.ts index d70736a..dbe7e7f 100644 --- a/source/features/high-score/high-score.ts +++ b/source/features/high-score/high-score.ts @@ -1,76 +1,60 @@ -import { getData, setData } from "../../core/data"; - -export interface ScoreData { - score: number; - linesCleared: number; - time: number; -} - -let SCORES: ScoreData[] = []; // sorted by the 'score' property -const MAX_SCORES = 10; - -/** - * Load the given high-scores. - */ -export function load() { - const scores = getData("tetris_high_score"); - - if (scores) { - SCORES = scores; +import { ScoreData } from "./high-score.types"; + +export class HighScore { + private scores: ScoreData[] = []; // sorted by the 'score' property + private maxScores = 10; + + /** + * Load the given high-scores. + */ + constructor(scores: ScoreData[] | undefined | null) { + if (scores) { + this.scores = scores; + } } - return SCORES; -} - -/** - * Try to add a score to the high-score list. Its only added if there's an empty spot or if the new score is higher than existing ones. - * If a score was actually added, it returns the position in the high-score list. - */ -export function add(score: ScoreData) { - let added = false; + /** + * Try to add a score to the high-score list. Its only added if there's an empty spot or if the new score is higher than existing ones. + * If a score was actually added, it returns the position in the high-score list. + */ + add(score: ScoreData) { + let added = false; - // haven't reached the limit yet, so just add the score - if (SCORES.length < MAX_SCORES) { - SCORES.push(score); - added = true; - } + // haven't reached the limit yet, so just add the score + if (this.scores.length < this.maxScores) { + this.scores.push(score); + added = true; + } - // delete an existing score if this score happens to be better than the worse score saved so far - else { - const worseScore = SCORES[SCORES.length - 1]; + // delete an existing score if this score happens to be better than the worse score saved so far + else { + const worseScore = this.scores[this.scores.length - 1]; - if (worseScore.score < score.score) { - SCORES.pop(); - SCORES.push(score); - added = true; + if (worseScore.score < score.score) { + this.scores.pop(); + this.scores.push(score); + added = true; + } } - } - if (added) { - SCORES.sort((a, b) => { - return b.score - a.score; - }); - save(); + if (added) { + this.scores.sort((a, b) => { + return b.score - a.score; + }); - // find the position of the score in the list - for (let a = 0; a < SCORES.length; a++) { - if (SCORES[a].score === score.score) { - return a + 1; + // find the position of the score in the list + for (let a = 0; a < this.scores.length; a++) { + if (this.scores[a].score === score.score) { + return a + 1; + } } } } -} - -/** - * Get the high-scores list. - */ -export function getHighScores() { - return SCORES; -} -/** - * Save the current high-scores to the storage. - */ -function save() { - setData("tetris_high_score", SCORES); + /** + * Get the high-scores list. + */ + getHighScores() { + return this.scores; + } } diff --git a/source/features/high-score/high-score.types.ts b/source/features/high-score/high-score.types.ts new file mode 100644 index 0000000..359019b --- /dev/null +++ b/source/features/high-score/high-score.types.ts @@ -0,0 +1,5 @@ +export interface ScoreData { + score: number; + linesCleared: number; + time: number; +} diff --git a/source/features/high-score/index.ts b/source/features/high-score/index.ts index 26cd291..12b313e 100644 --- a/source/features/high-score/index.ts +++ b/source/features/high-score/index.ts @@ -1,2 +1,2 @@ -export * from "./high-score"; -export * from "./high-score.hooks"; +export * from "./high-score.context"; +export * from "./high-score.types"; diff --git a/source/features/options/options.context.tsx b/source/features/options/options.context.tsx index f58a7ea..7bc3826 100644 --- a/source/features/options/options.context.tsx +++ b/source/features/options/options.context.tsx @@ -1,4 +1,4 @@ -import { createContext, useEffect, useRef } from "react"; +import { createContext, useRef } from "react"; import { Options } from "./options"; import { getData, setData } from "../../core/data"; import { OptionsData } from "./options.types"; @@ -35,7 +35,7 @@ export const OptionsContext = createContext({ export function OptionsContextProvider({ children, }: OptionsContextProviderProps) { - const options = useRef(new Options()); + const options = useRef(new Options(getData(STORAGE_KEY))); const getOption: GetOption = (key) => { return options.current.get(key); }; @@ -47,11 +47,6 @@ export function OptionsContextProvider({ setData(STORAGE_KEY, options.current.getData()); }; - useEffect(() => { - const data = getData(STORAGE_KEY); - options.current.load(data); - }, []); - const value = { getOption, setOption, diff --git a/source/features/options/options.ts b/source/features/options/options.ts index cc6e97e..ff70beb 100644 --- a/source/features/options/options.ts +++ b/source/features/options/options.ts @@ -9,11 +9,7 @@ export class Options { ghostPiece: true, }; - getData() { - return this.data; - } - - load(options: OptionsData | undefined | null) { + constructor(options: OptionsData | undefined | null) { if (options) { if (Number.isInteger(options.numberOfColumns)) { this.data.numberOfColumns = options.numberOfColumns; @@ -37,6 +33,10 @@ export class Options { } } + getData() { + return this.data; + } + get(option: Key): OptionsData[Key] { return this.data[option]; } diff --git a/source/index.tsx b/source/index.tsx index db480ed..38393d0 100644 --- a/source/index.tsx +++ b/source/index.tsx @@ -9,21 +9,12 @@ import { RoutePath } from "./core/routes"; import { OptionsPage } from "./pages/options-page"; import { GamePage } from "./pages/game-page"; import { HighScorePage } from "./pages/high-score-page"; -import * as HighScore from "./features/high-score"; - -function loadData() { - HighScore.load(); -} const router = createBrowserRouter([ { path: RoutePath.root, element: , errorElement: , - loader: () => { - loadData(); - return null; - }, children: [ { path: RoutePath.home, diff --git a/source/pages/game-page/game-page.hooks.tsx b/source/pages/game-page/game-page.hooks.tsx index 46f4c69..822b4b2 100644 --- a/source/pages/game-page/game-page.hooks.tsx +++ b/source/pages/game-page/game-page.hooks.tsx @@ -11,11 +11,11 @@ import { timeToString } from "@drk4/utilities"; import { cardinalToOrdinal } from "../../utilities"; import { useNavigate } from "react-router-dom"; import { useStage } from "../../features/stage"; -import * as HighScore from "../../features/high-score"; import { useReducerWM } from "../../core/use-reducer"; import { RoutePath } from "../../core/routes"; import { CanvasDimensions } from "../../features/canvas"; import { OptionsContext } from "../../features/options"; +import { HighScoreContext } from "../../features/high-score"; export function useGameLogic() { const navigate = useNavigate(); @@ -28,6 +28,7 @@ export function useGameLogic() { }); const { openDialog, closeDialog } = useContext(DialogContext); const { getOption } = useContext(OptionsContext); + const { addScore } = useContext(HighScoreContext); const middleware = useCallback((action: GameAction) => { if (!gameRef.current) { @@ -35,7 +36,7 @@ export function useGameLogic() { } const onEnd = (data: GameEndData) => { - const added = HighScore.add(data); + const added = addScore(data); const endMessage = [ `Level: ${data.level}`, `Lines cleared: ${data.linesCleared}`, @@ -92,7 +93,7 @@ export function useGameLogic() { }, [stageRef.current]); const onQuit = () => { - HighScore.add(game.score); + addScore(game.score); gameRef.current?.clear(); navigate(RoutePath.home); diff --git a/source/pages/high-score-page.tsx b/source/pages/high-score-page.tsx index 2cbb8ea..57dba6f 100644 --- a/source/pages/high-score-page.tsx +++ b/source/pages/high-score-page.tsx @@ -1,7 +1,8 @@ import styled from "@emotion/styled"; -import { BackButton } from "../components/back-button"; -import { useHighScore } from "../features/high-score"; import { timeToString } from "@drk4/utilities"; +import { BackButton } from "../components/back-button"; +import { useContext } from "react"; +import { HighScoreContext } from "../features/high-score"; const Container = styled.div``; const Header = styled.h2``; @@ -16,7 +17,8 @@ const THeader = styled.th` `; export function HighScorePage() { - const { scores } = useHighScore(); + const { getScores } = useContext(HighScoreContext); + const scores = getScores(); return ( diff --git a/source/pages/root-page.tsx b/source/pages/root-page.tsx index 1205f4f..a5d25b5 100644 --- a/source/pages/root-page.tsx +++ b/source/pages/root-page.tsx @@ -3,6 +3,7 @@ import styled from "@emotion/styled"; import { Outlet } from "react-router-dom"; import { DialogContextProvider } from "../features/dialog"; import { OptionsContextProvider } from "../features/options"; +import { HighScoreContextProvider } from "../features/high-score"; const GlobalStyles = css` body { @@ -27,10 +28,12 @@ export function RootPage() { return ( - - - - + + + + + + );