Skip to content

Commit

Permalink
Multiplayer
Browse files Browse the repository at this point in the history
  • Loading branch information
ProLoser committed Sep 20, 2024
1 parent 3f08297 commit 3a2d925
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 113 deletions.
2 changes: 1 addition & 1 deletion src/Game/Point.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Piece from './Piece'

type PointProps = {
pieces: number,
move: (from: number, to: number) => void,
move: (from: number | 'black' | 'white', to: number) => void,
position: number,
selected: boolean,
onSelect: (position: number) => void
Expand Down
121 changes: 63 additions & 58 deletions src/Game/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Point from './Point';
import Piece from './Piece';
import Toolbar from '../Toolbar'
import { useCallback, useContext, useEffect, useState, type DragEventHandler } from 'react';
import { GameContext, MultiplayerContext } from '../Online/Contexts';
import { GameType, MatchContext, MultiplayerContext } from '../Online/Contexts';
import firebase from 'firebase/compat/app';


Expand All @@ -22,104 +22,109 @@ 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)

export default function Game() {
const [blackHome, setBlackHome] = useState(0)
const [whiteHome, setWhiteHome] = useState(0)
const [blackBar, setBlackBar] = useState(0)
const [whiteBar, setWhiteBar] = useState(0)
const [board, setBoard] = useState(() => [...DEFAULT_BOARD])
const [dice, setDice] = useState(() => [rollDie(), rollDie()])
const database = firebase.database();
const [game, setGame] = useState<GameType>(newGame);
const [selected, setSelected] = useState<number|null>(null);
const game = useContext(GameContext);
const match = useContext(MatchContext);
const { move: sendMove } = useContext(MultiplayerContext);

// Subscribe to Game
useEffect(() => {
if (game?.exists()) {
setBoard(game.val().state)
if (match?.game) {
const subscriber = (snapshot: firebase.database.DataSnapshot) => {
const value = snapshot.val()
setBoard(value.state)
if (value.dice)
setDice(oldDice => {
const newDice = value.dice.split('-').map(Number)
if (oldDice[0] === newDice[0] && oldDice[1] === newDice[1]) return oldDice;
vibrate()
return newDice;
})
if (!value) return;
setGame(value)
// TODO: vibrate if enemy rolls?
}
game.ref.on('value', subscriber)
database.ref(`games/${match.game}`).on('value', subscriber)
return () => {
game.ref.off('value', subscriber)
database.ref(`games/${match.game}`).off('value', subscriber)
}
} else {
setBoard([...DEFAULT_BOARD])
setGame(newGame())
}
}, [game])
}, [match])

const roll = useCallback(() => {
vibrate()
const newDice = [rollDie(), rollDie()]
game?.ref.update({ dice: newDice.join('-') })
setDice(newDice)
}, [game])
if (match?.game)
database.ref(`games/${match.game}`).update({ dice: 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 nextBoard = [...board];
const nextGame: GameType = newGame(game);
let moveLabel; // @see https://en.wikipedia.org/wiki/Backgammon_notation
if (from == "white") { // white re-enter
if (board[to] == -1) { // hit
if (nextGame.board[to] == -1) { // hit
moveLabel = `bar/${to}*`
setBlackBar(bar => bar + 1)
setWhiteBar(bar => bar - 1)
nextBoard[to] = 1
} else if (board[to] >= -1) { // move
nextGame.prison!.black++
nextGame.prison!.white--
nextGame.board[to] = 1
} else if (nextGame.board[to] >= -1) { // move
moveLabel = `bar/${to}`
setWhiteBar(bar => bar - 1)
nextBoard[to]++
nextGame.prison!.white--
nextGame.board[to]++
} else { return; } // blocked
} else if (from == 'black') { // black re-enter
if (board[to] == 1) { // hit
if (nextGame.board[to] == 1) { // hit
moveLabel = `bar/${to}*`
setWhiteBar(bar => bar + 1)
setBlackBar(bar => bar - 1)
nextBoard[to] = -1
} else if (board[to] <= 1) { // move
nextGame.prison!.white++
nextGame.prison!.black--
nextGame.board[to] = -1
} else if (nextGame.board[to] <= 1) { // move
moveLabel = `bar/${to}`
setBlackBar(bar => bar - 1)
nextBoard[to]--
nextGame.prison!.black--
nextGame.board[to]--
} else { return; } // blocked
} else {
const offense = board[from];
const defense = board[to];
const offense = nextGame.board[from];
const defense = nextGame.board[to];

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

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

setBoard(nextBoard);
sendMove(nextBoard, moveLabel);
}, [board, game, sendMove])
setGame(nextGame);
sendMove(nextGame, `${nextGame.dice.join('-')}: ${moveLabel}`);
}, [game, sendMove])

const onDragOver: DragEventHandler = useCallback((event) => { event.preventDefault(); }, [])
const onDrop: DragEventHandler = useCallback((event) => {
Expand All @@ -140,20 +145,20 @@ export default function Game() {

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

<div className="bar">
{Array.from({ length: whiteBar }, (_, 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: blackBar }, (_, 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: blackHome }, (_, 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: whiteHome }, (_, index) => <Piece key={index} color="white" />)}
{Array.from({ length: game.home.white }, (_, index) => <Piece key={index} color="white" />)}
</div>
{board.map((pieces, index) => <Point key={index} pieces={pieces} move={move} position={index} selected={selected==index} onSelect={onSelect} />)}
{game.board.map((pieces, index) => <Point key={index} pieces={pieces} move={move} position={index} selected={selected===index} onSelect={onSelect} />)}
</div >;
}
43 changes: 23 additions & 20 deletions src/Online/Contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,25 @@ export type Match = {
lastMove?: number;
}

export type Game = {
moves: {
// timestamp
[key: number]: {
// 13/10, 10/8
moves: string,
// cj123
player: string
}
},
state: {
// 1: -2
[key: number]: number
}
export type Move = {
player: string;
game: string;
move: string;
time: string;
}

export type GameType = {
status?: string;
board: number[];
dice: number[];
prison: {
black: number;
white: number;
};
home: {
black: number;
white: number;
};
};

export type Chat = {
Expand All @@ -63,13 +68,11 @@ export type ChatContextType = {
export const AuthContext = createContext<SnapshotOrNullType>(null);
export const MultiplayerContext = createContext({
load: (userId: UserData["uid"]) => { },
move: (nextBoard: number[], move: string) => { },
});
export const ChatContext = createContext<ChatContextType>({
send: (message: string) => { },
state: null
move: (game: GameType, move: string) => { },
chat: (message: string) => { },
});
export const GameContext = createContext<SnapshotOrNullType>(null);
export const ChatContext = createContext<SnapshotOrNullType>(null);
export const MatchContext = createContext<Match|null>(null);
export const FriendContext = createContext<SnapshotOrNullType>(null);
export const ModalContext = createContext<ModalContextType>({
toggle: (newState: ModalState) => { },
Expand Down
61 changes: 27 additions & 34 deletions src/Online/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import Friends from "./Friends";
import Chat from "./Chat";
import Profile from "./Profile";
import Login from "./Login";
import { useContext, useEffect, useState, PropsWithChildren, useCallback, useMemo } from "react";
import { ModalContext, AuthContext, ChatContext, FriendContext, GameContext, Match, MultiplayerContext, SnapshotOrNullType, UserData, ModalState } from "./Contexts";
import { useContext, useEffect, useState, PropsWithChildren, useCallback } from "react";
import { ModalContext, AuthContext, ChatContext, FriendContext, MatchContext, Match, Game, MultiplayerContext, SnapshotOrNullType, UserData, ModalState, Move } from "./Contexts";
import firebase from 'firebase/compat/app';
import 'firebase/compat/database';

Expand Down Expand Up @@ -33,11 +33,9 @@ export function Provider({ children }: PropsWithChildren) {
const [state, setState] = useState<ModalState>(false);
const [lastState, setLastState] = useState<ModalState>('friends');
const [match, setMatch] = useState<Match | null>(null);
const [game, setGame] = useState<SnapshotOrNullType>(null);
const [chat, setChat] = useState<SnapshotOrNullType>(null);
const [chats, setChats] = useState<SnapshotOrNullType>(null);
const [friend, setFriend] = useState<SnapshotOrNullType>(null);


const toggle = (newState: ModalState) => {
if (newState === true) {
setState(lastState);
Expand All @@ -52,36 +50,42 @@ export function Provider({ children }: PropsWithChildren) {
}
};

const send = useCallback((message: string) => {
const chat = useCallback((message: string) => {
if (match && user) {
database.ref(`chats/${match.chat}`).push({
message,
author: user.key
author: user.key,
time: new Date().toISOString()
})
}
}, [match, user]);

const move = useCallback((nextBoard: number[], move: string) => {
if (game?.key) {
game.ref.child('moves').push({
const move = useCallback((game: Game, move: Move["move"]) => {
if (match?.game) {
const time = new Date().toISOString();
const nextMove: Move = {
player: user?.val().uid,
move
})
game.ref.update({ state: nextBoard })
game: match.game,
move,
time,
}
const update = {
sort: new Date().toISOString(),
sort: time,
};
database.ref('moves').push(nextMove)
database.ref(`games/${match.game}`).update(game)
database.ref(`matches/${user?.key}/${friend?.key}`).update(update);
database.ref(`matches/${friend?.key}/${user?.key}`).update(update);

}
}, [game, user, friend]);
}, [match, user, friend]);

const load = useCallback(async (userId?: string) => {
console.log('Loading', userId);

if (!user || !userId) {
setGame(null);
setChat(null);
setMatch(null);
setChats(null);
setFriend(null);
return;
}
Expand Down Expand Up @@ -114,27 +118,16 @@ export function Provider({ children }: PropsWithChildren) {
toggle(false);
}, [user]);

// Autoload Match upon Login
useEffect(() => {
if (!user) return;

const friendLocation = location.pathname.split('/').pop()
if (friendLocation && friendLocation !== 'PeaceInTheMiddleEast') load(friendLocation);
}, [load, user]);

// Synchronize Selected Match
useEffect(() => {
if (!user || !match) return;
database.ref(`games/${match.game}`).get().then(setGame);
database.ref(`chats/${match.chat}`).orderByKey().limitToLast(1000).on('value', setChat);

return () => {
database.ref(`chats/${match.chat}`).off('value', setChat);
}
}, [user, match]);


// onLogin/Logout
useEffect(() => {
// onAuthStateChanged
const unregisterAuthObserver = firebase.auth().onAuthStateChanged(async authUser => {
if (authUser) {
const userRef = firebase.database().ref(`users/${authUser.uid}`)
Expand All @@ -161,12 +154,12 @@ export function Provider({ children }: PropsWithChildren) {
return (
<AuthContext.Provider value={user}>
<ModalContext.Provider value={{ toggle, state }}>
<MultiplayerContext.Provider value={{ load, move }}>
<MultiplayerContext.Provider value={{ load, move, chat }}>
<FriendContext.Provider value={friend}>
<ChatContext.Provider value={{ send, state:chat }}>
<GameContext.Provider value={game}>
<ChatContext.Provider value={chats}>
<MatchContext.Provider value={match}>
{children}
</GameContext.Provider>
</MatchContext.Provider>
</ChatContext.Provider>
</FriendContext.Provider>
</MultiplayerContext.Provider>
Expand Down

0 comments on commit 3a2d925

Please sign in to comment.