Skip to content

Commit

Permalink
Changed game logic into a hook with reducer
Browse files Browse the repository at this point in the history
  • Loading branch information
ProLoser committed Sep 30, 2024
1 parent 2f49979 commit 8bef27a
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 127 deletions.
147 changes: 22 additions & 125 deletions src/Game/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,135 +3,22 @@ import Dice from './Dice';
import Point from './Point';
import Piece from './Piece';
import Toolbar from '../Toolbar'
import { useCallback, useContext, useEffect, useState, type DragEventHandler } from 'react';
import { GameType, MatchContext, MultiplayerContext } from '../Online/Contexts';
import firebase from 'firebase/compat/app';


// White = Positive, Black = Negative
const DEFAULT_BOARD = [
5, 0, 0, 0, -3, 0, -5, 0, 0, 0, 0, 2,
-5, 0, 0, 0, 3, 0, 5, 0, 0, 0, 0, -2,
]

function rollDie() {
return Math.floor(Math.random() * 6) + 1
}

function vibrate() {
navigator.vibrate?.([50, 50, 60, 30, 90, 20, 110, 10, 150])
}

const newGame = (oldGame?: GameType) => ({
status: '',
board: [...(oldGame?.board || DEFAULT_BOARD)],
dice: oldGame?.dice || [6,6],
prison: oldGame?.prison || {
black: 0,
white: 0,
},
home: oldGame?.home || {
black: 0,
white: 0,
},
} as GameType)
import { useCallback, useContext, useState, type DragEventHandler } from 'react';
import { MatchContext } from '../Online/Contexts';
import useGameState from './useGameState';

export default function Game() {
const database = firebase.database();
const [game, setGame] = useState<GameType>(newGame);
const [selected, setSelected] = useState<number|null>(null);
const [selected, setSelected] = useState<number | null>(null);
const match = useContext(MatchContext);
const { move: sendMove } = useContext(MultiplayerContext);

// Subscribe to Game
useEffect(() => {
if (match?.game) {
const subscriber = (snapshot: firebase.database.DataSnapshot) => {
const value = snapshot.val()
if (!value) return;
setGame(value)
// TODO: vibrate if enemy rolls?
}
database.ref(`games/${match.game}`).on('value', subscriber)
return () => {
database.ref(`games/${match.game}`).off('value', subscriber)
}
} else {
setGame(newGame())
}
}, [match])

const roll = useCallback(() => {
vibrate()
const newDice = [rollDie(), rollDie()]
if (match?.game)
database.ref(`games/${match.game}/dice`).set(newDice)
setGame(game => ({...game, dice: newDice}))
}, [match])

// TODO: Validate moves against dice
const move = useCallback((from: number | "white" | "black", to: number) => {
if (from == to) return; // no move
const nextGame: GameType = newGame(game);
let moveLabel; // @see https://en.wikipedia.org/wiki/Backgammon_notation
if (from == "white") { // white re-enter
if (nextGame.board[to] == -1) { // hit
moveLabel = `bar/${to}*`
nextGame.prison!.black++
nextGame.prison!.white--
nextGame.board[to] = 1
} else if (nextGame.board[to] >= -1) { // move
moveLabel = `bar/${to}`
nextGame.prison!.white--
nextGame.board[to]++
} else { return; } // blocked
} else if (from == 'black') { // black re-enter
if (nextGame.board[to] == 1) { // hit
moveLabel = `bar/${to}*`
nextGame.prison!.white++
nextGame.prison!.black--
nextGame.board[to] = -1
} else if (nextGame.board[to] <= 1) { // move
moveLabel = `bar/${to}`
nextGame.prison!.black--
nextGame.board[to]--
} else { return; } // blocked
} else {
const offense = nextGame.board[from];
const defense = nextGame.board[to];

if (defense === undefined) { // bear off
moveLabel = `${from}/off`
if (offense > 0) {
nextGame.home!.white++
} else {
nextGame.home!.black++
}
} else if (!defense || Math.sign(defense) === Math.sign(offense)) { // move
moveLabel = `${from}/${to}`
nextGame.board[to] += Math.sign(offense)
} else if (Math.abs(defense) === 1) { // hit
moveLabel = `${from}/${to}*`
nextGame.board[to] = -Math.sign(defense);
if (offense > 0)
nextGame.prison!.black++
else
nextGame.prison!.white++
} else { return; } // blocked

nextGame.board[from] -= Math.sign(offense)
}

setGame(nextGame);
sendMove(nextGame, `${nextGame.dice.join('-')}: ${moveLabel}`);
}, [game, sendMove])
const { state: game, rollDice, move } = useGameState(match?.game);

const onDragOver: DragEventHandler = useCallback((event) => { event.preventDefault(); }, [])
const onDrop: DragEventHandler = useCallback((event) => {
event.preventDefault();
let from = parseInt(event.dataTransfer?.getData("text")!)
return move(from, -1,)
}, [move])

const onSelect = useCallback((position: number | null) => {
if (position === null || selected === position) {
setSelected(null);
Expand All @@ -145,20 +32,30 @@ export default function Game() {

return <div id="board">
<Toolbar />
<Dice onClick={roll} values={game.dice} />
<Dice onClick={rollDice} values={game.dice} />

<div className="bar">
{Array.from({ length: game.prison.white }, (_, index) => <Piece key={index} position={-1} color="white" />)}
{Array.from({ length: game.prison.white }, (_, index) =>
<Piece key={index} position={-1} color="white" />
)}
</div>
<div className="bar">
{Array.from({ length: game.prison.black }, (_, index) => <Piece key={index} position={-1} color="black" />)}
{Array.from({ length: game.prison.black }, (_, index) =>
<Piece key={index} position={-1} color="black" />
)}
</div>
<div className="home" onDragOver={onDragOver} onDrop={onDrop}>
{Array.from({ length: game.home.black }, (_, index) => <Piece key={index} color="black" />)}
{Array.from({ length: game.home.black }, (_, index) =>
<Piece key={index} color="black" />
)}
</div>
<div className="home" onDragOver={onDragOver} onDrop={onDrop}>
{Array.from({ length: game.home.white }, (_, index) => <Piece key={index} color="white" />)}
{Array.from({ length: game.home.white }, (_, index) =>
<Piece key={index} color="white" />
)}
</div>
{game.board.map((pieces, index) => <Point key={index} pieces={pieces} move={move} position={index} selected={selected===index} onSelect={onSelect} />)}
{game.board.map((pieces: number, index: number) =>
<Point key={index} pieces={pieces} move={move} position={index} selected={selected === index} onSelect={onSelect} />
)}
</div >;
}
199 changes: 199 additions & 0 deletions src/Game/useGameState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { useReducer, useContext, useEffect } from "react";
import { MultiplayerContext, type GameType } from "../Online/Contexts";
import firebase from "firebase/compat/app";

// White = Positive, Black = Negative
const DEFAULT_BOARD = [
5, 0, 0, 0, -3, 0, -5, 0, 0, 0, 0, 2, -5, 0, 0, 0, 3, 0, 5, 0, 0, 0, 0, -2,
];

function rollDie() {
return Math.floor(Math.random() * 6) + 1;
}

function vibrate() {
navigator.vibrate?.([50, 100, 60, 60, 90, 40, 110, 20, 150]);
}

export const newGame = (oldGame?: GameType) => ({
status: "",
board: [...(oldGame?.board || DEFAULT_BOARD)],
dice: oldGame?.dice || [6, 6],
prison: oldGame?.prison || {
black: 0,
white: 0,
},
home: oldGame?.home || {
black: 0,
white: 0,
},
} as GameType);

interface MoveAction {
type: typeof Actions.MOVE;
data: {
from: number | "white" | "black";
to: number;
};
}

interface SetGameAction {
type: typeof Actions.SET_GAME;
data: GameType;
}

interface RollAction {
type: typeof Actions.ROLL;
data: {
dice: number[];
};
}

type Action = MoveAction | SetGameAction | RollAction;

export enum Actions {
LOAD = "LOAD",
ROLL = "ROLL",
MOVE = "MOVE",
SET_GAME = "SET_GAME",
}

const firstGame = newGame();

function calculate(state: GameType, from: number | "white" | "black", to: number) {
if (from === to) return { state }; // no move
const nextGame: GameType = newGame(state);
let moveLabel: string; // @see https://en.wikipedia.org/wiki/Backgammon_notation
if (from === "white") {
// white re-enter
if (nextGame.board[to] === -1) {
// hit
moveLabel = `bar/${to}*`;
nextGame.prison.black++;
nextGame.prison.white--;
nextGame.board[to] = 1;
} else if (nextGame.board[to] >= -1) {
// move
moveLabel = `bar/${to}`;
nextGame.prison.white--;
nextGame.board[to]++;
} else {
// blocked
return { state };
}
} else if (from === "black") {
// black re-enter
if (nextGame.board[to] === 1) {
// hit
moveLabel = `bar/${to}*`;
nextGame.prison.white++;
nextGame.prison.black--;
nextGame.board[to] = -1;
} else if (nextGame.board[to] <= 1) {
// move
moveLabel = `bar/${to}`;
nextGame.prison.black--;
nextGame.board[to]--;
} else {
// blocked
return { state };
}
} else {
const offense = nextGame.board[from];
const defense = nextGame.board[to];

if (defense === undefined) {
// bear off
moveLabel = `${from}/off`;
if (offense > 0) {
nextGame.home.white++;
} else {
nextGame.home.black++;
}
} else if (!defense || Math.sign(defense) === Math.sign(offense)) {
// move
moveLabel = `${from}/${to}`;
nextGame.board[to] += Math.sign(offense);
} else if (Math.abs(defense) === 1) {
// hit
moveLabel = `${from}/${to}*`;
nextGame.board[to] = -Math.sign(defense);
if (offense > 0) nextGame.prison.black++;
else nextGame.prison.white++;
} else {
// blocked
return { state };
}

// remove from previous position
nextGame.board[from] -= Math.sign(nextGame.board[from]);
}
return { state: nextGame, moveLabel };
}

function reducer(state: GameType, action: Action): GameType {
switch (action.type) {
case Actions.SET_GAME:
return { ...state, ...action.data };
case Actions.ROLL:
return { ...state, dice: action.data.dice };
default:
return state;
}
}

export default function useGameState(gameId?: string) {
const [state, dispatch] = useReducer(reducer, firstGame);
const { move: sendMove } = useContext(MultiplayerContext);

useEffect(() => {
if (gameId) {
const gameRef = firebase.database().ref(`games/${gameId}`)
const onValue = (snapshot: firebase.database.DataSnapshot) => {
const value = snapshot.val();
if (value) {
dispatch({ type: Actions.SET_GAME, data: value });
} else {
dispatch({ type: Actions.SET_GAME, data: firstGame });
gameRef.set(firstGame);
}
};
gameRef.on("value", onValue);
return () => {
gameRef.off("value", onValue);
};
}
}, [gameId]);

const rollDice = () => {
const newDice = [rollDie(), rollDie()];
dispatch({ type: Actions.ROLL, data: { dice: newDice } });
vibrate();
if (gameId)
firebase.database().ref(`games/${gameId}/dice`).set(newDice);
};

const move = (from: number | "white" | "black", to: number) => {
const { state: nextState, moveLabel } = calculate(state, from, to);
if (!moveLabel) return;
dispatch({ type: Actions.SET_GAME, data: nextState });
// dispatch({ type: Actions.MOVE, data: { from, to } });
sendMove(nextState, `${nextState.dice.join("-")}: ${moveLabel}`);
};

const reset = () => {
if (confirm('Are you sure you want to reset the match?')) {
console.log('Resetting', gameId);
let data = newGame()
firebase.database().ref(`games/${gameId}`).set(data);
dispatch({ type: Actions.SET_GAME, data });
}
}

return {
state,
rollDice,
move,
reset,
};
}
5 changes: 3 additions & 2 deletions src/Online/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useContext, useEffect, useState, PropsWithChildren, useCallback, useMem
import { ModalContext, AuthContext, ChatContext, FriendContext, MatchContext, Match, GameType, MultiplayerContext, SnapshotOrNullType, UserData, ModalState, Move } from "./Contexts";
import firebase from 'firebase/compat/app';
import 'firebase/compat/database';
import { newGame } from "../Game/useGameState";

/**
* The rendered component tree
Expand Down Expand Up @@ -127,8 +128,8 @@ export function Provider({ children }: PropsWithChildren) {
if (match?.game) {
if (confirm('Are you sure you want to reset the match?')) {
console.log('Resetting', match.game);
database.ref(`games/${match.game}`).remove();
// TODO: update state
database.ref(`games/${match.game}`).set(newGame());
// TODO: update state?
}
}
}, [match]);
Expand Down

0 comments on commit 8bef27a

Please sign in to comment.