See all posts

Tic Tac Toe Ep 3: Redux Undo

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

Tic Tac Toe Gameplay

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 Game
  • updateHistory : 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

Tic Tac Toe Gameplay

Project Files

For more information about how to use the plugin and the features it includes, GitHub Repository for Part 3.


Previous Lesson