Skip to content

Commit

Permalink
♻️: Refactor the 'high-score' feature into a react context.
Browse files Browse the repository at this point in the history
  • Loading branch information
noobiept committed Aug 12, 2023
1 parent 7cea139 commit acb7d1b
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 111 deletions.
45 changes: 45 additions & 0 deletions source/features/high-score/high-score.context.tsx
Original file line number Diff line number Diff line change
@@ -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<HighScoreContextValue>({
getScores: () => [],
addScore: () => {
return undefined;
},
});

export function HighScoreContextProvider({
children,
}: HighScoreContextProviderProps) {
const highScore = useRef<HighScore>(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 (
<HighScoreContext.Provider value={value}>
{children}
</HighScoreContext.Provider>
);
}
15 changes: 0 additions & 15 deletions source/features/high-score/high-score.hooks.tsx

This file was deleted.

110 changes: 47 additions & 63 deletions source/features/high-score/high-score.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 5 additions & 0 deletions source/features/high-score/high-score.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface ScoreData {
score: number;
linesCleared: number;
time: number;
}
4 changes: 2 additions & 2 deletions source/features/high-score/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./high-score";
export * from "./high-score.hooks";
export * from "./high-score.context";
export * from "./high-score.types";
9 changes: 2 additions & 7 deletions source/features/options/options.context.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -35,7 +35,7 @@ export const OptionsContext = createContext<OptionsContextValue>({
export function OptionsContextProvider({
children,
}: OptionsContextProviderProps) {
const options = useRef<Options>(new Options());
const options = useRef<Options>(new Options(getData(STORAGE_KEY)));
const getOption: GetOption = (key) => {
return options.current.get(key);
};
Expand All @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions source/features/options/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,6 +33,10 @@ export class Options {
}
}

getData() {
return this.data;
}

get<Key extends keyof OptionsData>(option: Key): OptionsData[Key] {
return this.data[option];
}
Expand Down
9 changes: 0 additions & 9 deletions source/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: <RootPage />,
errorElement: <ErrorPage />,
loader: () => {
loadData();
return null;
},
children: [
{
path: RoutePath.home,
Expand Down
7 changes: 4 additions & 3 deletions source/pages/game-page/game-page.hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -28,14 +28,15 @@ export function useGameLogic() {
});
const { openDialog, closeDialog } = useContext(DialogContext);
const { getOption } = useContext(OptionsContext);
const { addScore } = useContext(HighScoreContext);

const middleware = useCallback((action: GameAction) => {
if (!gameRef.current) {
return;
}

const onEnd = (data: GameEndData) => {
const added = HighScore.add(data);
const added = addScore(data);
const endMessage = [
`Level: ${data.level}`,
`Lines cleared: ${data.linesCleared}`,
Expand Down Expand Up @@ -92,7 +93,7 @@ export function useGameLogic() {
}, [stageRef.current]);

Check warning on line 93 in source/pages/game-page/game-page.hooks.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook useEffect has missing dependencies: 'dispatch', 'getOption', and 'stageActions'. Either include them or remove the dependency array. Mutable values like 'stageRef.current' aren't valid dependencies because mutating them doesn't re-render the component

const onQuit = () => {
HighScore.add(game.score);
addScore(game.score);
gameRef.current?.clear();

navigate(RoutePath.home);
Expand Down
8 changes: 5 additions & 3 deletions source/pages/high-score-page.tsx
Original file line number Diff line number Diff line change
@@ -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``;
Expand All @@ -16,7 +17,8 @@ const THeader = styled.th`
`;

export function HighScorePage() {
const { scores } = useHighScore();
const { getScores } = useContext(HighScoreContext);
const scores = getScores();

return (
<Container>
Expand Down
11 changes: 7 additions & 4 deletions source/pages/root-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -27,10 +28,12 @@ export function RootPage() {
return (
<DialogContextProvider>
<OptionsContextProvider>
<Container>
<Global styles={GlobalStyles} />
<Outlet />
</Container>
<HighScoreContextProvider>
<Container>
<Global styles={GlobalStyles} />
<Outlet />
</Container>
</HighScoreContextProvider>
</OptionsContextProvider>
</DialogContextProvider>
);
Expand Down

0 comments on commit acb7d1b

Please sign in to comment.