Skip to content

Commit

Permalink
feat: add undo, improved menus, clear also resets time
Browse files Browse the repository at this point in the history
  • Loading branch information
TN1ck committed Aug 24, 2024
1 parent 4b421ce commit e59eba0
Show file tree
Hide file tree
Showing 12 changed files with 220 additions and 62 deletions.
2 changes: 1 addition & 1 deletion src/components/modules/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const Button = ({
onClick={onClick}
className={overrideTailwindClasses(
clsx(
"rounded-sm border-none bg-white px-4 py-2 text-black shadow-sm transition-transform hover:brightness-90 focus:outline-none disabled:brightness-75",
"rounded-sm border-none bg-white md:px-4 md:py-2 px-2 py-1 text-black shadow-sm transition-transform hover:brightness-90 focus:outline-none disabled:brightness-75",
className,
{
"scale-110 brightness-90": active,
Expand Down
95 changes: 88 additions & 7 deletions src/components/modules/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,96 @@
import * as React from "react";
import {connect} from "react-redux";
import {connect, ConnectedProps} from "react-redux";
import Button from "./Button";
import {continueGame, GameStateMachine, pauseGame, resetGame} from "src/state/game";
import {chooseGame} from "src/state/application";
import {RootState} from "src/state/rootReducer";
import {setSudoku} from "src/state/sudoku";
import SUDOKUS from "src/sudoku-game/sudokus";

const Header = ({}) => {
const newGameConnector = connect(null, {
pauseGame,
chooseGame,
});

type NewGamePropsFromRedux = ConnectedProps<typeof newGameConnector>;

const clearGameConnector = connect(
(state: RootState) => ({
state: state.game.state,
difficulty: state.game.difficulty as keyof typeof SUDOKUS,
sudokuIndex: state.game.sudokuIndex,
}),
{
setSudoku,
resetGame,
pauseGame,
continueGame,
},
);

type ClearGamePropsFromRedux = ConnectedProps<typeof clearGameConnector>;

const ClearGameButton: React.FC<ClearGamePropsFromRedux> = ({
state,
difficulty,
sudokuIndex,
setSudoku,
resetGame,
pauseGame,
continueGame,
}) => {
const clearGame = async () => {
pauseGame();
// Wait 50ms to make sure the game is shown as paused when in the confirm dialog.
await new Promise((resolve) => setTimeout(resolve, 30));
const areYouSure = confirm("Are you sure? This will clear the sudoku and reset the timer.");
if (!areYouSure) {
continueGame();
return;
}
const sudoku = SUDOKUS[difficulty][sudokuIndex];
setSudoku(sudoku.sudoku, sudoku.solution);
resetGame();
// Wait 100ms as we have to wait until the updateTimer is called (should normally take 1/60 second)
// This is certainly not ideal and should be fixed there.
await new Promise((resolve) => setTimeout(resolve, 100));
continueGame();
};

return (
<Button disabled={state === GameStateMachine.wonGame || state === GameStateMachine.paused} onClick={clearGame}>
{"Clear"}
</Button>
);
};

const ConnectedClearGameButton = clearGameConnector(ClearGameButton);

const NewGameButton: React.FC<NewGamePropsFromRedux> = ({pauseGame, chooseGame}) => {
const pauseAndChoose = () => {
pauseGame();
chooseGame();
};

return (
<Button className="bg-teal-600 text-white" onClick={pauseAndChoose}>
{"New"}
</Button>
);
};

const ConnectedNewGameButton = newGameConnector(NewGameButton);

const Header = () => {
return (
<div className="flex justify-between bg-gray-900 p-4 text-white">
<div className="flex justify-between items-center bg-gray-900 p-4 text-white">
<div>{"Super Sudoku"}</div>
<a href="https://tn1ck.com" target="_blank" className="text-gray-500 hover:underline">
{"By tn1ck.com"}
</a>
<div className="flex space-x-2">
<ConnectedClearGameButton />
<ConnectedNewGameButton />
</div>
</div>
);
};

export default connect(null, {})(Header);
export default Header;
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const connector = connect(
notesMode: state.game.notesMode,
showOccurrences: state.game.showOccurrences,
activeCell: state.game.activeCellCoordinates,
sudoku: state.sudoku,
sudoku: state.sudoku.current,
showHints: state.game.showHints,
}),
{
Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/Game/GameSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ const GameSelect = React.memo(
setSudoku(sudoku.sudoku, sudoku.solution);
} else {
setGameState(local.game);
setSudokuState(local.sudoku);
setSudokuState({current: local.sudoku, history: [], historyIndex: 0});
}
continueGame();
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/Game/GameTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class GameTimer extends React.Component<PropsFromRedux> {
this._isMounted = false;
}
render() {
const {secondsPlayed, state} = this.props;
const {secondsPlayed} = this.props;

return <div className="text-center text-white">{formatDuration(secondsPlayed)}</div>;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/pages/Game/Sudoku/SudokuMenuCircle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const connector = connect(
(state: RootState) => {
return {
notesMode: state.game.notesMode,
sudoku: state.sudoku,
sudoku: state.sudoku.current,
showHints: state.game.showHints,
};
},
Expand Down
45 changes: 16 additions & 29 deletions src/components/pages/Game/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {

import {chooseGame} from "src/state/application";

import {setNumber, setNotes, setSudoku} from "src/state/sudoku";
import {setNumber, setNotes, setSudoku, undo} from "src/state/sudoku";

import {Sudoku} from "src/components/pages/Game/Sudoku/Sudoku";

Expand Down Expand Up @@ -50,10 +50,10 @@ const sudokuMenuNumbersConnector = connect(
);
const SudokuMenuNumbersConnected = sudokuMenuNumbersConnector(SudokuMenuNumbers);

function ClearButton({state, clearGame}: {state: GameStateMachine; clearGame: () => void}) {
function UndoButton({state, undoAction}: {state: GameStateMachine; undoAction: () => void}) {
return (
<Button disabled={state === GameStateMachine.wonGame || state === GameStateMachine.paused} onClick={clearGame}>
{"Clear"}
<Button disabled={state === GameStateMachine.wonGame || state === GameStateMachine.paused} onClick={undoAction}>
{"Undo"}
</Button>
);
}
Expand All @@ -77,10 +77,6 @@ function PauseButton({
);
}

function NewGameButton({newGame}: {newGame: () => void}) {
return <Button onClick={newGame}>{"New"}</Button>;
}

const ContinueIcon = styled.div.attrs({
className: "bg-teal-500",
})`
Expand Down Expand Up @@ -183,7 +179,7 @@ const connector = connect(
return {
game: state.game,
application: state.application,
sudoku: state.sudoku,
sudoku: state.sudoku.current,
};
},
{
Expand All @@ -201,6 +197,7 @@ const connector = connect(
toggleShowCircleMenu,
toggleShowWrongEntries,
toggleShowConflicts,
undo,
},
);

Expand Down Expand Up @@ -241,22 +238,8 @@ class Game extends React.Component<PropsFromRedux> {
};

render() {
const {game, application, pauseGame, continueGame, chooseGame, sudoku} = this.props;
const {game, application, pauseGame, continueGame, chooseGame, sudoku, undo} = this.props;
const pausedGame = game.state === GameStateMachine.paused;
const pauseAndChoose = () => {
pauseGame();
chooseGame();
};
const clear = () => {
// TODO: make nice.
const areYouSure = confirm("Are you sure? This will clear the sudoku.");
if (!areYouSure) {
return;
}
const sudoku = SUDOKUS[this.props.game.difficulty][this.props.game.sudokuIndex];
this.props.setSudoku(sudoku.sudoku, sudoku.solution);
this.props.continueGame();
};
const activeCell = game.activeCellCoordinates
? sudoku.find((s) => {
return s.x === game.activeCellCoordinates!.x && s.y === game.activeCellCoordinates!.y;
Expand All @@ -280,10 +263,9 @@ class Game extends React.Component<PropsFromRedux> {
<div className="mr-2">
<PauseButton state={game.state} continueGame={continueGame} pauseGame={pauseGame} />
</div>
<div className="mr-2">
<ClearButton state={game.state} clearGame={clear} />
<div>
<UndoButton state={game.state} undoAction={undo} />
</div>
<NewGameButton newGame={pauseAndChoose} />
</div>
</GameHeaderArea>
<GameMainArea>
Expand Down Expand Up @@ -328,7 +310,7 @@ class Game extends React.Component<PropsFromRedux> {
<SudokuMenuNumbersConnected />
<SudokuMenuControls />
<div className="mt-4 grid gap-4">
<div>
<div className="md:block hidden">
<h2 className="mb-2 text-3xl font-bold">Shortcuts</h2>
<div className="grid gap-2">
<ul className="list-disc pl-6">
Expand All @@ -338,6 +320,8 @@ class Game extends React.Component<PropsFromRedux> {
<li>Escape: Pause/unpause the game</li>
<li>H: Hint</li>
<li>N: Enter/exit note mode</li>
<li>CTRL + Z: Undo</li>
<li>CTRL + Y: Redo</li>
</ul>
</div>
</div>
Expand Down Expand Up @@ -381,7 +365,10 @@ class Game extends React.Component<PropsFromRedux> {
<a target="_blank" className="underline" href="https://github.com/TN1ck/super-sudoku">
Github
</a>
.
.{" "}
<a href="https://tn1ck.com" target="_blank" className="hover:underline">
{"Created by Tom Nick."}
</a>
</p>
</div>
</div>
Expand Down
20 changes: 17 additions & 3 deletions src/components/pages/Game/shortcuts/GridShortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import hotkeys from "hotkeys-js";
import {SUDOKU_COORDINATES, SUDOKU_NUMBERS} from "src/engine/utility";
import {Cell} from "src/engine/types";
import {showMenu, hideMenu, selectCell, pauseGame, activateNotesMode, deactivateNotesMode} from "src/state/game";
import {setNumber, clearNumber, getHint, setNotes} from "src/state/sudoku";
import {setNumber, clearNumber, getHint, setNotes, undo, redo} from "src/state/sudoku";
import {ShortcutScope} from "./ShortcutScope";
import {connect} from "react-redux";
import {RootState} from "src/state/rootReducer";
Expand All @@ -29,6 +29,8 @@ interface GameKeyboardShortcutsDispatchProps {
getHint: typeof getHint;
activateNotesMode: typeof activateNotesMode;
deactivateNotesMode: typeof deactivateNotesMode;
undo: typeof undo;
redo: typeof redo;
}

class GameKeyboardShortcuts extends React.Component<
Expand Down Expand Up @@ -141,6 +143,16 @@ class GameKeyboardShortcuts extends React.Component<
this.props.getHint(this.props.activeCell);
}
});

hotkeys("ctrl+z,cmd+z", ShortcutScope.Game, () => {
this.props.undo();
return false;
});

hotkeys("ctrl+y,cmd+y", ShortcutScope.Game, () => {
this.props.redo();
return false;
});
}

componentWillUnmount() {
Expand All @@ -154,12 +166,12 @@ class GameKeyboardShortcuts extends React.Component<
export default connect<GameKeyboardShortcutsStateProps, GameKeyboardShortcutsDispatchProps, {}, RootState>(
(state: RootState) => {
const activeCell = state.game.activeCellCoordinates
? state.sudoku.find((s) => {
? state.sudoku.current.find((s) => {
return s.x === state.game.activeCellCoordinates?.x && s.y === state.game.activeCellCoordinates.y;
})
: null;
return {
sudoku: state.sudoku,
sudoku: state.sudoku.current,
activeCell: activeCell!,
notesMode: state.game.notesMode,
showHints: state.game.showHints,
Expand All @@ -176,5 +188,7 @@ export default connect<GameKeyboardShortcutsStateProps, GameKeyboardShortcutsDis
getHint,
deactivateNotesMode,
activateNotesMode,
undo,
redo,
},
)(GameKeyboardShortcuts);
14 changes: 13 additions & 1 deletion src/state/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const TOGGLE_SHOW_WRONG_ENTRIES = "game/TOGGLE_SHOW_WRONG_ENTRIES";
const ACTIVATE_NOTES_MODE = "game/ACTIVATE_NOTES_MODE";
const DEACTIVATE_NOTES_MODE = "game/DEACTIVATE_NOTES_MODE";
export const UPDATE_TIMER = "game/UPDATE_TIME";
const RESET_GAME = "game/RESET_GAME";

export function activateNotesMode() {
return {
Expand Down Expand Up @@ -140,6 +141,12 @@ export function toggleShowCircleMenu() {
};
}

export function resetGame() {
return {
type: RESET_GAME,
};
}

export interface GameState {
activeCellCoordinates?: CellCoordinates;
difficulty: DIFFICULTY;
Expand Down Expand Up @@ -254,7 +261,12 @@ export default function gameReducer(state: GameState = INITIAL_GAME_STATE, actio
...state,
secondsPlayed: action.secondsPlayed,
};

case RESET_GAME:
return {
...state,
secondsPlayed: 0,
state: GameStateMachine.paused,
};
case SET_GAME_STATE_MACHINE:
switch (action.state) {
case GameStateMachine.paused: {
Expand Down
8 changes: 7 additions & 1 deletion src/state/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ const savedState = getState();
const currentSudoku = savedState.sudokus[savedState.active];
const initialState: RootState = {
game: currentSudoku ? currentSudoku.game : INITIAL_GAME_STATE,
sudoku: currentSudoku ? currentSudoku.sudoku : INITIAL_SUDOKU_STATE,
sudoku: currentSudoku
? {
history: [],
historyIndex: 0,
current: currentSudoku.sudoku,
}
: INITIAL_SUDOKU_STATE,
application: savedState.application ?? INITIAL_APPLICATION_STATE,
choose: INITIAL_CHOOSE_STATE,
};
Expand Down
Loading

0 comments on commit e59eba0

Please sign in to comment.