Tic Tac Toe Ep 3: Redux Undo
This is a 3 parts serious we will cover from smooth UI/UX, undo step, different ways to build AI, from easy to unbeatable. Complete source code at GitHub, if it help you place a stars.
Introduction
After first 2 parts, we created a playable Tic tac toe game. In part 3, we want want to leverage Redux to make a undo move and history recording.
Final Product in Part 3
Rollback
Everytime the player move, placeMark
is dispatched, we also dispatch updateHistory
, we will only store the current gameboard
and the current player mark
, so that our history record wont growth to too large.
History Slice
We will first define a type History
and interface HistoryState
for the History Slice.
export type History = {
board: Cell[]
}
export interface HistoryState {
past: History[]
}
We draft out the reducer
initialHistory
: Restart GameupdateHistory
: Save game history for each move
export const historySlice = createSlice({
name: "history",
initialState,
reducers: {
initialHistory: (state) => {
state.past = initialState.past
state.rollBackStep = initialState.rollBackStep
},
updateHistory: (state, { payload }: PayloadAction<History>) => {
if (state.rollBackStep !== state.past.length - 1)
state.past = state.past.slice(0, state.rollBackStep + 1)
state.past.push(payload)
state.rollBackStep += 1
},
},
})
Recover From History with Thunk
We will export a function for recover from History, We will use Thunk, so that we can do dispatch and getState from the other slice.
// historySlice...
export const recoverFromHistory =
(step: number): AppThunk =>
(dispatch, getState) => {
const historyState = getState().history
if (step < 0 || step >= historyState.past.length) {
console.error("Invalid step number")
return
}
const rollbackRecord = getState().history.past[step]
dispatch(moveHistoryPointer(step))
dispatch(rollbackGameBoard(rollbackRecord))
}
rollbackGameBoard
is a reducer
that help use to replace the history record to current gameboard.
// ticTacToeSlice.ts
// other reducers...
rollbackGameBoard: (
state,
{ payload }: PayloadAction<{ board: Cell[]; currentRound: Marks }>
) => {
state.board = payload.board
state.currentRound = payload.currentRound
},
HistoryList
We iterate through the records in the list to RollBackButton
. Once we click on the button, it will dispatch
the thunk recoverFromHistory(index)
and update the gameboard instanly.
import styled from "styled-components"
import { useAppDispatch, useAppSelector } from "../../app/hooks"
import { selectResult } from "../tictactoe/ticTacToeSlice"
import {
recoverFromHistory,
selectHistories,
selectRollBackStep,
} from "./historySlice"
const SectionWrapper = styled.section`
margin-bottom: 0.25rem;
font-weight: 600;
color: var(--text-color);
`
const Heading = styled.p`
margin-bottom: 0.25rem;
font-weight: 600;
text-align: left;
color: var(--text-color);
`
const List = styled.div`
overflow-y: auto;
display: flex;
padding: 0.5em;
/* border: 1px var(--background-color-darker) solid; */
border-radius: 5px;
flex-direction: column;
margin-bottom: 0.25rem;
font-weight: 600;
row-gap: 8px;
height: 280px;
align-items: flex-end;
color: var(--text-color);
`
const RollBackButton = styled(({ ...props }) => <button {...props} />)`
border: 1px var(--background-color-darker) solid;
border-radius: 5px;
font-weight: 600;
height: 32px;
background-color: transparent;
width: 100%;
cursor: pointer;
width: ${(props) => (props.current ? "80%" : "100%")};
color: var(--text-color);
transition: all ease-in-out;
animation-duration: 500ms;
`
function HistoryList() {
const dispatch = useAppDispatch()
const histories = useAppSelector(selectHistories)
const currentStep = useAppSelector(selectRollBackStep)
const result = useAppSelector(selectResult)
const onClickHandler = (index: number) => {
if (result === null) {
dispatch(recoverFromHistory(index))
}
}
return (
<SectionWrapper>
<Heading>History</Heading>
<List>
{histories.map((_, index) => (
<RollBackButton
key={index}
current={currentStep === index}
onClick={() => onClickHandler(index)}
>
{index === 0 ? "Game Start" : `Back to Move #${index}`}
</RollBackButton>
))}
</List>
</SectionWrapper>
)
}
export default HistoryList
Final Product
Project Files
For more information about how to use the plugin and the features it includes, GitHub Repository for Part 3.