commit
0976ae668a
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mancala-frontend",
|
||||
"version": "0.1.3-beta.12",
|
||||
"description": "",
|
||||
"description": "Mancala Game Frontend",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "parcel src/index.html",
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { initContext } from './context';
|
||||
import { initContext } from './context/context';
|
||||
|
||||
import Home from './Home';
|
||||
import Home from './routes/Home';
|
||||
|
||||
initContext();
|
||||
|
||||
|
||||
346
src/Home.tsx
346
src/Home.tsx
@ -1,346 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent, useEffect, useState } from "react";
|
||||
import BoardView from "./components/BoardView";
|
||||
import { context } from "./context";
|
||||
import { RTMTWS } from "./rtmt/rtmt_websocket";
|
||||
import {
|
||||
channel_game_move,
|
||||
channel_leave_game,
|
||||
channel_on_game_crashed,
|
||||
channel_on_game_start,
|
||||
channel_on_game_update,
|
||||
channel_on_game_user_leave,
|
||||
} from "./channel_names";
|
||||
import Button from "./components/Button";
|
||||
import InfoPanel from "./components/InfoPanel";
|
||||
import { CommonMancalaGame, MancalaGame, Pit } from "mancala.js";
|
||||
import { GameMove } from "./models/GameMove";
|
||||
import PitAnimator from "./animation/PitAnimator";
|
||||
import BoardViewModel from "./viewmodel/BoardViewModel";
|
||||
import { v4 } from "uuid";
|
||||
import { Menu, MenuButton, MenuItem } from "@szhsin/react-menu";
|
||||
import "@szhsin/react-menu/dist/index.css";
|
||||
import "@szhsin/react-menu/dist/transitions/slide.css";
|
||||
import { getColorByBrightness } from "./util/ColorUtil";
|
||||
import { Theme } from "./theme/Theme";
|
||||
|
||||
type ConnectionState = "connecting" | "error" | "connected" | "reconnecting";
|
||||
|
||||
const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
|
||||
const [userKey, setUserKey] = useState(undefined);
|
||||
|
||||
const [connectionState, setConnetionState] =
|
||||
useState<ConnectionState>("connecting");
|
||||
|
||||
const [searchingOpponent, setSearchingOpponent] = useState<boolean>(false);
|
||||
|
||||
const [game, setGame] = useState<CommonMancalaGame>(undefined);
|
||||
|
||||
const [crashMessage, setCrashMessage] = useState<string>(undefined);
|
||||
|
||||
const [userKeyWhoLeave, setUserKeyWhoLeave] = useState<string>(undefined);
|
||||
|
||||
const [boardViewModel, setBoardViewModel] = useState<BoardViewModel>(null);
|
||||
|
||||
const [boardId, setBoardId] = useState<string>();
|
||||
|
||||
const [pitAnimator, setPitAnimator] = useState<PitAnimator>();
|
||||
|
||||
const [theme, setTheme] = useState<Theme | undefined>(undefined);
|
||||
|
||||
const onConnectionDone = () => {
|
||||
setConnetionState("connected");
|
||||
};
|
||||
|
||||
const onConnectionLost = () => {
|
||||
connectToServer("reconnecting");
|
||||
};
|
||||
|
||||
const onConnectionError = (event: Event) => {
|
||||
setConnetionState("error");
|
||||
};
|
||||
|
||||
const connectToServer = (connectionState: ConnectionState) => {
|
||||
setConnetionState(connectionState);
|
||||
context.userKeyStore.getUserKey((userKey: string) => {
|
||||
setUserKey(userKey);
|
||||
const rtmtws = context.rtmt as RTMTWS;
|
||||
if (rtmtws) {
|
||||
rtmtws.initWebSocket(
|
||||
userKey,
|
||||
onConnectionDone,
|
||||
onConnectionLost,
|
||||
onConnectionError
|
||||
);
|
||||
} else {
|
||||
console.error("context.rtmt is not RTMTWS");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const listenMessages = (pitAnimator: PitAnimator) => {
|
||||
context.rtmt.listenMessage(channel_on_game_start, (message: Object) => {
|
||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||
const mancalaGame = MancalaGame.createFromMancalaGame(newGame);
|
||||
setSearchingOpponent(false);
|
||||
setGame(mancalaGame);
|
||||
pitAnimator.setNewGame(mancalaGame);
|
||||
});
|
||||
|
||||
context.rtmt.listenMessage(channel_on_game_update, (message: Object) => {
|
||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||
const mancalaGame = MancalaGame.createFromMancalaGame(newGame);
|
||||
setGame(mancalaGame);
|
||||
pitAnimator.setUpdatedGame(mancalaGame);
|
||||
});
|
||||
|
||||
context.rtmt.listenMessage(channel_on_game_crashed, (message: any) => {
|
||||
const newCrashMessage = message as string;
|
||||
console.error("on_game_crash");
|
||||
console.error(newCrashMessage);
|
||||
setCrashMessage(newCrashMessage);
|
||||
});
|
||||
|
||||
context.rtmt.listenMessage(channel_on_game_user_leave, (message: any) => {
|
||||
const userKeyWhoLeave = message;
|
||||
setUserKeyWhoLeave(userKeyWhoLeave);
|
||||
});
|
||||
};
|
||||
|
||||
const updateBoardViewModel = (boardViewModel: BoardViewModel) => {
|
||||
boardViewModel.id = v4();
|
||||
setBoardId(boardViewModel.id);
|
||||
setBoardViewModel(boardViewModel);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setTheme(context.themeManager.theme);
|
||||
const pitAnimator = new PitAnimator(context, updateBoardViewModel);
|
||||
setPitAnimator(pitAnimator);
|
||||
listenMessages(pitAnimator);
|
||||
connectToServer("connecting");
|
||||
return () => {
|
||||
pitAnimator.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
context.themeManager.onThemeChange = (theme) => {
|
||||
setTheme(theme);
|
||||
pitAnimator && updateBoardViewModel(pitAnimator.getBoardViewModelFromGame(game));
|
||||
};
|
||||
}, [boardViewModel]);
|
||||
|
||||
const resetGameState = () => {
|
||||
setGame(undefined);
|
||||
setCrashMessage(undefined);
|
||||
setUserKeyWhoLeave(undefined);
|
||||
};
|
||||
|
||||
const newGameClick = () => {
|
||||
resetGameState();
|
||||
setSearchingOpponent(true);
|
||||
context.rtmt.sendMessage("new_game", {});
|
||||
};
|
||||
|
||||
const leaveGame = () => {
|
||||
context.rtmt.sendMessage(channel_leave_game, {});
|
||||
};
|
||||
|
||||
const getBoardIndex = (index: number) => {
|
||||
if (userKey === game.player2Id) return index + game.board.pits.length / 2;
|
||||
return index;
|
||||
};
|
||||
|
||||
const onHoleSelect = (index: number, pit: Pit) => {
|
||||
//TODO : stoneCount comes from view model!
|
||||
if (pit.stoneCount === 0) {
|
||||
//TODO : warn user
|
||||
return;
|
||||
}
|
||||
boardViewModel.pits[getBoardIndex(index)].pitColor =
|
||||
context.themeManager.theme.pitSelectedColor;
|
||||
updateBoardViewModel(boardViewModel);
|
||||
const gameMove: GameMove = { index: index };
|
||||
context.rtmt.sendMessage(channel_game_move, gameMove);
|
||||
};
|
||||
|
||||
const showConnectionState = connectionState != "connected";
|
||||
|
||||
const connectionStateText = () => {
|
||||
let map: { [key: string]: string } = {
|
||||
connecting: context.texts.Connecting,
|
||||
connected: context.texts.Connected,
|
||||
error: context.texts.CannotConnect,
|
||||
reconnecting: context.texts.ConnectingAgain,
|
||||
};
|
||||
|
||||
return map[connectionState];
|
||||
};
|
||||
|
||||
const renderNewGameButton = () => {
|
||||
const newGame = (
|
||||
<Button
|
||||
context={context}
|
||||
text={context.texts.NewGame}
|
||||
color={context.themeManager.theme.holeColor}
|
||||
onClick={newGameClick}
|
||||
/>
|
||||
);
|
||||
if (userKeyWhoLeave) {
|
||||
return newGame;
|
||||
}
|
||||
if (crashMessage) {
|
||||
return newGame;
|
||||
}
|
||||
if (!game) {
|
||||
return newGame;
|
||||
} else if (game.state == "ended") {
|
||||
return newGame;
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
const menuTextColor = getColorByBrightness(
|
||||
context.themeManager.theme.appBarBgColor,
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
background: theme?.background,
|
||||
flex: "1",
|
||||
minHeight: "400px"
|
||||
}}
|
||||
>
|
||||
{showConnectionState && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "0px",
|
||||
left: "0px",
|
||||
padding: "15px ",
|
||||
borderTopRightRadius: "1vw",
|
||||
minWidth: "10vw",
|
||||
minHeight: "1vw",
|
||||
background: context.themeManager.theme.textColor,
|
||||
color: context.themeManager.theme.textLightColor,
|
||||
}}
|
||||
>
|
||||
{connectionStateText()}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
padding: "0px 4vw",
|
||||
background: context.themeManager.theme.appBarBgColor,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
alignSelf: "stretch",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ color: menuTextColor, margin: "10px 0px" }}>
|
||||
{context.texts.Mancala}
|
||||
</h1>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginRight: "1vw",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
menuStyle={{
|
||||
background: context.themeManager.theme.appBarBgColor,
|
||||
}}
|
||||
menuButton={
|
||||
<span
|
||||
style={{ color: menuTextColor }}
|
||||
class="material-symbols-outlined"
|
||||
>
|
||||
light_mode
|
||||
</span>
|
||||
}
|
||||
transition
|
||||
align="end"
|
||||
>
|
||||
{context.themeManager.themes.map((theme) => {
|
||||
return (
|
||||
<MenuItem
|
||||
style={{
|
||||
color: menuTextColor,
|
||||
}}
|
||||
onMouseOver={(event) =>
|
||||
(event.target.style.background =
|
||||
context.themeManager.theme.background)
|
||||
}
|
||||
onMouseOut={(event) =>
|
||||
(event.target.style.background = "transparent")
|
||||
}
|
||||
onClick={() => (context.themeManager.theme = theme)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: "5vw",
|
||||
background: theme.boardColor,
|
||||
width: "1vw",
|
||||
height: "1vw",
|
||||
marginRight: "1vw",
|
||||
}}
|
||||
></div>
|
||||
{theme.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</div>
|
||||
{renderNewGameButton()}
|
||||
{game &&
|
||||
!userKeyWhoLeave &&
|
||||
!crashMessage &&
|
||||
(game?.state === "playing" || game?.state === "initial") && (
|
||||
<Button
|
||||
context={context}
|
||||
color={context.themeManager.theme.holeColor}
|
||||
text={context.texts.Leave}
|
||||
onClick={leaveGame}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<InfoPanel
|
||||
context={context}
|
||||
game={game}
|
||||
crashMessage={crashMessage}
|
||||
userKey={userKey}
|
||||
userKeyWhoLeave={userKeyWhoLeave}
|
||||
searchingOpponent={searchingOpponent}
|
||||
/>
|
||||
{game && boardViewModel && (
|
||||
<BoardView
|
||||
userKey={userKey}
|
||||
game={game}
|
||||
boardId={boardId}
|
||||
boardViewModel={boardViewModel}
|
||||
context={context}
|
||||
onHoleSelect={onHoleSelect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@ -9,7 +9,7 @@ import {
|
||||
GAME_STEP_DOUBLE_STONE_IN_PIT,
|
||||
} from "mancala.js";
|
||||
import { v4 } from "uuid";
|
||||
import { Context } from "../context";
|
||||
import { Context } from "../context/context";
|
||||
import BoardViewModelFactory from "../factory/BoardViewModelFactory";
|
||||
import { PitViewModelFactory } from "../factory/PitViewModelFactory";
|
||||
import { getColorByBrightness } from "../util/ColorUtil";
|
||||
@ -19,14 +19,14 @@ const animationUpdateInterval = 300;
|
||||
|
||||
export default class PitAnimator {
|
||||
context: Context;
|
||||
game: MancalaGame;
|
||||
oldGame: MancalaGame;
|
||||
game: MancalaGame | undefined;
|
||||
oldGame: MancalaGame | undefined;
|
||||
currentIntervalID: number;
|
||||
onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void;
|
||||
boardViewModel: BoardViewModel;
|
||||
oldBoardViewModel: BoardViewModel;
|
||||
boardViewModel: BoardViewModel | undefined;
|
||||
oldBoardViewModel: BoardViewModel | undefined;
|
||||
animationIndex: number = 0;
|
||||
currentHistoryItem: HistoryItem;
|
||||
currentHistoryItem: HistoryItem | undefined;
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
@ -55,7 +55,7 @@ export default class PitAnimator {
|
||||
|
||||
onGameMoveAnimationStart() {
|
||||
this.stopCurrentAnimation();
|
||||
if (this.game.history.length > 0) {
|
||||
if (this.game && this.oldGame && this.game.history.length > 0) {
|
||||
const lastHistoryItem = this.game.history[this.game.history.length - 1];
|
||||
if (lastHistoryItem.gameSteps.length > 0) {
|
||||
this.animationIndex = 0;
|
||||
@ -68,6 +68,7 @@ export default class PitAnimator {
|
||||
}
|
||||
|
||||
onAnimate() {
|
||||
if(!this.currentHistoryItem || !this.game || !this.oldBoardViewModel) return;
|
||||
if (this.animationIndex === this.currentHistoryItem.gameSteps.length) {
|
||||
this.clearCurrentInterval();
|
||||
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
||||
@ -91,9 +92,10 @@ export default class PitAnimator {
|
||||
boardViewModel: BoardViewModel,
|
||||
gameStep: GameStep
|
||||
) {
|
||||
if(!this.currentHistoryItem || !this.game) return;
|
||||
const pitViewModel = boardViewModel.pits[index];
|
||||
if (this.animationIndex === 0) {
|
||||
//This one stone move case, TODO : beautify it later
|
||||
//This is one stone move case, TODO: beautify it later
|
||||
if (this.getGameMoveStepCount(this.currentHistoryItem) === 1) {
|
||||
const previousPitIndex = gameStep.index - 1;
|
||||
if (previousPitIndex > 0) {
|
||||
@ -172,7 +174,7 @@ export default class PitAnimator {
|
||||
const theme = this.context.themeManager.theme;
|
||||
const stoneCount = pit.stoneCount;
|
||||
const stoneColor = theme.stoneColor;
|
||||
const pitColor = theme.holeColor;
|
||||
const pitColor = theme.pitColor;
|
||||
const id = pit.index.toString();
|
||||
return PitViewModelFactory.create({
|
||||
id,
|
||||
@ -185,15 +187,15 @@ export default class PitAnimator {
|
||||
|
||||
public resetAnimationState() {
|
||||
this.animationIndex = -1;
|
||||
this.currentHistoryItem = null;
|
||||
this.boardViewModel = null;
|
||||
this.oldBoardViewModel = null;
|
||||
this.currentHistoryItem = undefined;
|
||||
this.boardViewModel = undefined;
|
||||
this.oldBoardViewModel = undefined;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.resetAnimationState();
|
||||
this.game = null;
|
||||
this.oldGame = null;
|
||||
this.game = undefined;
|
||||
this.oldGame = undefined;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
|
||||
@ -1,208 +0,0 @@
|
||||
import { Bank, MancalaGame, Pit } from "mancala.js";
|
||||
import * as React from "react";
|
||||
import { FunctionComponent, useState } from "react";
|
||||
import { Context } from "../context";
|
||||
import { getColorByBrightness } from "../util/ColorUtil";
|
||||
import BoardViewModel from "../viewmodel/BoardViewModel";
|
||||
import PitViewModel from "../viewmodel/PitViewModel";
|
||||
|
||||
const BallView: FunctionComponent<{ color: string }> = ({ color }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: color,
|
||||
margin: "1px",
|
||||
width: "1vw",
|
||||
height: "1vw",
|
||||
borderRadius: "10vw",
|
||||
transition: "background-color 0.5s",
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
function range(size: number) {
|
||||
var ans = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
ans.push(i);
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
const HoleView: FunctionComponent<{
|
||||
pitViewModel: PitViewModel;
|
||||
onClick: () => void;
|
||||
}> = ({ pitViewModel, onClick }) => {
|
||||
const balls = [...range(pitViewModel.stoneCount)].map((i) => (
|
||||
<BallView color={pitViewModel.stoneColor} />
|
||||
));
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
style={{
|
||||
background: pitViewModel.pitColor,
|
||||
margin: "5px",
|
||||
padding: "5px",
|
||||
borderRadius: "10vw",
|
||||
transition: "background-color 0.5s",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignContent: "center",
|
||||
justifyContent: "center",
|
||||
justifyItems: "center",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{balls}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StoreView: FunctionComponent<{
|
||||
context: Context;
|
||||
pitViewModel: PitViewModel;
|
||||
gridColumn: string;
|
||||
gridRow: string;
|
||||
}> = ({ context, pitViewModel, gridColumn, gridRow }) => {
|
||||
const balls = [...range(pitViewModel.stoneCount)].map((i) => (
|
||||
<BallView color={pitViewModel.stoneColor} />
|
||||
));
|
||||
const textColor = getColorByBrightness(
|
||||
pitViewModel.pitColor,
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
gridColumn: gridColumn,
|
||||
gridRow: gridRow,
|
||||
background: pitViewModel.pitColor,
|
||||
margin: "5px",
|
||||
borderRadius: "10vw",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
flexWrap: "wrap",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{balls}
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "2vw",
|
||||
fontFamily: "monospace",
|
||||
fontWeight: "bold",
|
||||
fontSize: "2vw",
|
||||
color: textColor,
|
||||
}}
|
||||
>
|
||||
{balls.length}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BoardView: FunctionComponent<{
|
||||
game?: MancalaGame;
|
||||
context: Context;
|
||||
boardId: string;
|
||||
boardViewModel: BoardViewModel;
|
||||
userKey: string;
|
||||
onHoleSelect: (index: number, hole: Pit) => void;
|
||||
}> = ({ game, context, boardId, boardViewModel, userKey, onHoleSelect }) => {
|
||||
const createPitView = (pitViewModel: PitViewModel, onClick: () => void) => {
|
||||
return <HoleView pitViewModel={pitViewModel} onClick={onClick} />;
|
||||
};
|
||||
const player1Pits = game?.board.player1Pits.map((pit) => {
|
||||
const pitViewModel = boardViewModel.pits[pit.index];
|
||||
return createPitView(pitViewModel, () => {
|
||||
if (game.turnPlayerId === game.player1Id)
|
||||
onHoleSelect(game.board.player1Pits.indexOf(pit), pit);
|
||||
});
|
||||
});
|
||||
const player2Pits = game!!.board.player2Pits.map((pit) => {
|
||||
const pitViewModel = boardViewModel.pits[pit.index];
|
||||
return createPitView(pitViewModel, () => {
|
||||
if (game.turnPlayerId === game.player2Id)
|
||||
onHoleSelect(game.board.player2Pits.indexOf(pit), pit);
|
||||
});
|
||||
});
|
||||
const isUserTurn = game.checkIsPlayerTurn(userKey);
|
||||
const theme = context.themeManager.theme;
|
||||
const player1BankViewModel =
|
||||
boardViewModel.pits[game.board.player1BankIndex()];
|
||||
const player2BankViewModel =
|
||||
boardViewModel.pits[game.board.player2BankIndex()];
|
||||
const player1Bank = (
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={player1BankViewModel}
|
||||
gridColumn="1 / 2"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
);
|
||||
const player2Bank = (
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={player2BankViewModel}
|
||||
gridColumn="8 / 9"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
margin: "1vw",
|
||||
padding: "2vw",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(8, 11vw)",
|
||||
gridTemplateRows: "repeat(2, 11vw)",
|
||||
borderRadius: "3vw",
|
||||
transition: "background-color 0.5s",
|
||||
background: theme.boardColor,
|
||||
}}
|
||||
>
|
||||
{userKey === game.player2Id ? (
|
||||
<>
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={player1BankViewModel}
|
||||
gridColumn="1 / 2"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={player2BankViewModel}
|
||||
gridColumn="8 / 9"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
{player1Pits.reverse()}
|
||||
{player2Pits}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={player1BankViewModel}
|
||||
gridColumn="8 / 9"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={player2BankViewModel}
|
||||
gridColumn="1 / 2"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
{player2Pits.reverse()}
|
||||
{player1Pits}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardView;
|
||||
@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import { Context } from "../context";
|
||||
import { Context } from "../context/context";
|
||||
import { getColorByBrightness } from "../util/ColorUtil";
|
||||
|
||||
const Button: FunctionComponent<{
|
||||
|
||||
27
src/components/FloatingPanel.tsx
Normal file
27
src/components/FloatingPanel.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import { Context } from "../context/context";
|
||||
|
||||
const FloatingPanel: FunctionComponent<{
|
||||
context: Context;
|
||||
color: string;
|
||||
visible: boolean;
|
||||
}> = (props) => {
|
||||
if(!props.visible) return <></>
|
||||
return (
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
bottom: "0px",
|
||||
left: "0px",
|
||||
padding: "15px",
|
||||
borderTopRightRadius: "1vw",
|
||||
minWidth: "10vw",
|
||||
minHeight: "1vw",
|
||||
backgroundColor: props.color,
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default FloatingPanel;
|
||||
132
src/components/HeaderBar.tsx
Normal file
132
src/components/HeaderBar.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
import { MancalaGame } from "mancala.js";
|
||||
import { Context } from "../context/context";
|
||||
import { getColorByBrightness } from "../util/ColorUtil";
|
||||
import Button from "./Button";
|
||||
import "@szhsin/react-menu/dist/index.css";
|
||||
import "@szhsin/react-menu/dist/transitions/slide.css";
|
||||
|
||||
function renderNewGameButton(
|
||||
context: Context, game:
|
||||
MancalaGame | undefined,
|
||||
onNewGameClick: () => void,
|
||||
userKeyWhoLeave: string | undefined,
|
||||
crashMessage: string | undefined): JSX.Element {
|
||||
const newGame = (
|
||||
<Button
|
||||
context={context}
|
||||
text={context.texts.NewGame}
|
||||
color={context.themeManager.theme.pitColor}
|
||||
onClick={onNewGameClick}
|
||||
/>
|
||||
);
|
||||
if (userKeyWhoLeave) {
|
||||
return newGame;
|
||||
}
|
||||
if (crashMessage) {
|
||||
return newGame;
|
||||
}
|
||||
if (!game) {
|
||||
return newGame;
|
||||
} else if (game.state == "ended") {
|
||||
return newGame;
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const HeaderBar: FunctionComponent<{
|
||||
context: Context,
|
||||
game?: MancalaGame,
|
||||
userKeyWhoLeave?: string,
|
||||
crashMessage?: string,
|
||||
onNewGameClick: () => void,
|
||||
onLeaveGameClick: () => void
|
||||
}> = (props) => {
|
||||
const { context, game, userKeyWhoLeave, crashMessage, onNewGameClick, onLeaveGameClick } = props;
|
||||
const textColor = getColorByBrightness(
|
||||
context.themeManager.theme.appBarBgColor,
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
return (
|
||||
<div style={{
|
||||
padding: "0px 4vw",
|
||||
background: context.themeManager.theme.appBarBgColor,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
alignSelf: "stretch",
|
||||
}}>
|
||||
<h1 style={{ color: textColor, margin: "10px 0px" }}>
|
||||
{context.texts.Mancala}
|
||||
</h1>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}} >
|
||||
<div style={{
|
||||
marginRight: "1vw",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<ThemeSwitchMenu context={context} textColor={textColor} />
|
||||
</div>
|
||||
{renderNewGameButton(context, game, onNewGameClick, userKeyWhoLeave, crashMessage)}
|
||||
{game &&
|
||||
!userKeyWhoLeave &&
|
||||
!crashMessage &&
|
||||
(game?.state === "playing" || game?.state === "initial") && (
|
||||
<Button
|
||||
context={context}
|
||||
color={context.themeManager.theme.pitColor}
|
||||
text={context.texts.Leave}
|
||||
onClick={onLeaveGameClick} />
|
||||
)}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
export default HeaderBar;
|
||||
|
||||
const ThemeSwitchMenu: FunctionComponent<{ context: Context, textColor: string }> = (props) => {
|
||||
const { context, textColor } = props;
|
||||
const menuButton = <span
|
||||
style={{ color: textColor }}
|
||||
className="material-symbols-outlined">
|
||||
light_mode
|
||||
</span>;
|
||||
const menuItems = context.themeManager.themes.map((theme, index) => {
|
||||
return (<MenuItem
|
||||
key={index}
|
||||
style={{ color: textColor }}
|
||||
//@ts-ignore
|
||||
onMouseOver={(event) => (event.target.style.background =
|
||||
context.themeManager.theme.background)}
|
||||
//@ts-ignore
|
||||
onMouseOut={(event) => (event.target.style.background = "transparent")}
|
||||
onClick={() => (context.themeManager.theme = theme)}>
|
||||
<div style={{
|
||||
borderRadius: "5vw",
|
||||
background: theme.boardColor,
|
||||
width: "1vw",
|
||||
height: "1vw",
|
||||
marginRight: "1vw",
|
||||
}} />
|
||||
{theme.name}
|
||||
</MenuItem>);
|
||||
})
|
||||
return (<Menu
|
||||
menuStyle={{
|
||||
background: context.themeManager.theme.appBarBgColor,
|
||||
}}
|
||||
menuButton={menuButton}
|
||||
transition
|
||||
align="end">
|
||||
{menuItems}
|
||||
</Menu>);
|
||||
}
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import { MancalaGame } from "mancala.js";
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import { Context } from "../context";
|
||||
import { Context } from "../context/context";
|
||||
import { getColorByBrightness } from "../util/ColorUtil";
|
||||
|
||||
function getInfoPanelTextByGameState(params: {
|
||||
context: Context;
|
||||
game: MancalaGame;
|
||||
crashMessage: string;
|
||||
userKey: string;
|
||||
userKeyWhoLeave: string;
|
||||
game?: MancalaGame;
|
||||
crashMessage?: string;
|
||||
userKey?: string;
|
||||
userKeyWhoLeave?: string;
|
||||
searchingOpponent: boolean;
|
||||
}): string {
|
||||
}): string | undefined {
|
||||
const {
|
||||
context,
|
||||
game,
|
||||
@ -42,9 +42,9 @@ function getInfoPanelTextByGameState(params: {
|
||||
return context.texts.GameEnded + " " + whoWon;
|
||||
} else {
|
||||
if (game) {
|
||||
return game.checkIsPlayerTurn(userKey)
|
||||
return userKey ? game.checkIsPlayerTurn(userKey)
|
||||
? context.texts.YourTurn
|
||||
: context.texts.OpponentTurn;
|
||||
: context.texts.OpponentTurn : undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@ -70,10 +70,10 @@ const InfoPanelContainer: FunctionComponent<{
|
||||
|
||||
const InfoPanel: FunctionComponent<{
|
||||
context: Context;
|
||||
game: MancalaGame;
|
||||
crashMessage: string;
|
||||
userKey: string;
|
||||
userKeyWhoLeave: string;
|
||||
game?: MancalaGame;
|
||||
crashMessage?: string;
|
||||
userKey?: string;
|
||||
userKeyWhoLeave?: string;
|
||||
searchingOpponent: boolean;
|
||||
}> = ({
|
||||
context,
|
||||
@ -83,7 +83,7 @@ const InfoPanel: FunctionComponent<{
|
||||
userKeyWhoLeave,
|
||||
searchingOpponent,
|
||||
}) => {
|
||||
const isUserTurn = game?.checkIsPlayerTurn(userKey);
|
||||
const isUserTurn = userKey ? game?.checkIsPlayerTurn(userKey) : false;
|
||||
const containerColor = isUserTurn
|
||||
? context.themeManager.theme.playerTurnColor
|
||||
: context.themeManager.theme.boardColor;
|
||||
|
||||
20
src/components/PageContainer.tsx
Normal file
20
src/components/PageContainer.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import { Theme } from "../theme/Theme";
|
||||
|
||||
const PageContainer: FunctionComponent<{theme: Theme}> = (props) => {
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
background: props.theme?.background,
|
||||
flex: "1",
|
||||
minHeight: "400px"
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageContainer;
|
||||
72
src/components/board/BoardView.tsx
Normal file
72
src/components/board/BoardView.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import * as React from "react";
|
||||
import { MancalaGame, Pit } from "mancala.js";
|
||||
import { FunctionComponent } from "react";
|
||||
import { Context } from "../../context/context";
|
||||
import BoardViewModel from "../../viewmodel/BoardViewModel";
|
||||
import PitViewModel from "../../viewmodel/PitViewModel";
|
||||
import PitView from "./PitView";
|
||||
import StoreView from "./StoreView";
|
||||
|
||||
const BoardView: FunctionComponent<{
|
||||
game: MancalaGame;
|
||||
context: Context;
|
||||
boardId: string;
|
||||
boardViewModel: BoardViewModel;
|
||||
userKey: string;
|
||||
onPitSelect: (index: number, pit: Pit) => void;
|
||||
}> = ({ game, context, boardId, boardViewModel, userKey, onPitSelect: onPitSelect }) => {
|
||||
const createPitView = (key: any, pitViewModel: PitViewModel, onClick: () => void) => {
|
||||
return <PitView key={key} pitViewModel={pitViewModel} onClick={onClick} />;
|
||||
};
|
||||
const player1Pits = game?.board.player1Pits.map((pit, index) => {
|
||||
const pitViewModel = boardViewModel.pits[pit.index];
|
||||
return createPitView(index, pitViewModel, () => {
|
||||
if (game.turnPlayerId === game.player1Id && userKey === game.player1Id)
|
||||
onPitSelect(game.board.player1Pits.indexOf(pit), pit);
|
||||
});
|
||||
});
|
||||
const player2Pits = game?.board.player2Pits.map((pit, index) => {
|
||||
const pitViewModel = boardViewModel.pits[pit.index];
|
||||
return createPitView(index, pitViewModel, () => {
|
||||
if (game.turnPlayerId === game.player2Id && userKey === game.player2Id)
|
||||
onPitSelect(game.board.player2Pits.indexOf(pit), pit);
|
||||
});
|
||||
});
|
||||
const theme = context.themeManager.theme;
|
||||
const player1BankViewModel =
|
||||
boardViewModel.pits[game.board.player1BankIndex()];
|
||||
const player2BankViewModel =
|
||||
boardViewModel.pits[game.board.player2BankIndex()];
|
||||
const isPlayer2 = userKey === game?.player2Id;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
margin: "1vw",
|
||||
padding: "2vw",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(8, 11vw)",
|
||||
gridTemplateRows: "repeat(2, 11vw)",
|
||||
borderRadius: "3vw",
|
||||
transition: "background-color 0.5s",
|
||||
background: theme.boardColor,
|
||||
}}
|
||||
>
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={isPlayer2 ? player2BankViewModel : player1BankViewModel}
|
||||
gridColumn="8 / 9"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={isPlayer2 ? player1BankViewModel : player2BankViewModel}
|
||||
gridColumn="1 / 2"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
{isPlayer2 ? player1Pits?.reverse() : player2Pits?.reverse()}
|
||||
{isPlayer2 ? player2Pits : player1Pits}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardView;
|
||||
38
src/components/board/PitView.tsx
Normal file
38
src/components/board/PitView.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import Util from "../../util/Util";
|
||||
import PitViewModel from "../../viewmodel/PitViewModel";
|
||||
import StoneView from "./StoneView";
|
||||
|
||||
|
||||
const PitView: FunctionComponent<{
|
||||
pitViewModel: PitViewModel;
|
||||
onClick: () => void;
|
||||
}> = ({ pitViewModel, onClick }) => {
|
||||
const stones = [...Util.range(pitViewModel.stoneCount)].map((i, index) => (
|
||||
<StoneView key={index} color={pitViewModel.stoneColor} />
|
||||
));
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
style={{
|
||||
background: pitViewModel.pitColor,
|
||||
margin: "5px",
|
||||
padding: "5px",
|
||||
borderRadius: "10vw",
|
||||
transition: "background-color 0.5s",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignContent: "center",
|
||||
justifyContent: "center",
|
||||
justifyItems: "center",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{stones}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PitView;
|
||||
19
src/components/board/StoneView.tsx
Normal file
19
src/components/board/StoneView.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
const StoneView: FunctionComponent<{ color: string }> = ({ color }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: color,
|
||||
margin: "1px",
|
||||
width: "1vw",
|
||||
height: "1vw",
|
||||
borderRadius: "10vw",
|
||||
transition: "background-color 0.5s",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default StoneView;
|
||||
56
src/components/board/StoreView.tsx
Normal file
56
src/components/board/StoreView.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import { Context } from "../../context/context";
|
||||
import { getColorByBrightness } from "../../util/ColorUtil";
|
||||
import Util from "../../util/Util";
|
||||
import PitViewModel from "../../viewmodel/PitViewModel";
|
||||
import StoneView from "./StoneView";
|
||||
|
||||
const StoreView: FunctionComponent<{
|
||||
context: Context;
|
||||
pitViewModel: PitViewModel;
|
||||
gridColumn: string;
|
||||
gridRow: string;
|
||||
}> = ({ context, pitViewModel, gridColumn, gridRow }) => {
|
||||
const stones = [...Util.range(pitViewModel.stoneCount)].map((i, index) => (
|
||||
<StoneView key={index} color={pitViewModel.stoneColor} />
|
||||
));
|
||||
const textColor = getColorByBrightness(
|
||||
pitViewModel.pitColor,
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
gridColumn: gridColumn,
|
||||
gridRow: gridRow,
|
||||
background: pitViewModel.pitColor,
|
||||
margin: "5px",
|
||||
borderRadius: "10vw",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
flexWrap: "wrap",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{stones}
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "2vw",
|
||||
fontFamily: "monospace",
|
||||
fontWeight: "bold",
|
||||
fontSize: "2vw",
|
||||
color: textColor,
|
||||
}}
|
||||
>
|
||||
{stones.length}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StoreView;
|
||||
@ -1,8 +1,8 @@
|
||||
import { Texts, TrTr } from "./const/texts";
|
||||
import { RTMT } from "./rtmt/rtmt";
|
||||
import { RTMTWS } from "./rtmt/rtmt_websocket";
|
||||
import { UserKeyStore, UserKeyStoreImpl } from "./store/key_store";
|
||||
import ThemeManager from "./theme/ThemeManager";
|
||||
import { Texts, TrTr } from "../const/texts";
|
||||
import { RTMT } from "../rtmt/rtmt";
|
||||
import { RTMTWS } from "../rtmt/rtmt_websocket";
|
||||
import { UserKeyStore, UserKeyStoreImpl } from "../store/key_store";
|
||||
import ThemeManager from "../theme/ThemeManager";
|
||||
|
||||
export type Context = {
|
||||
rtmt: RTMT;
|
||||
220
src/routes/Home.tsx
Normal file
220
src/routes/Home.tsx
Normal file
@ -0,0 +1,220 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent, useEffect, useState } from "react";
|
||||
import BoardView from "../components/board/BoardView";
|
||||
import { context } from "../context/context";
|
||||
import { RTMTWS } from "../rtmt/rtmt_websocket";
|
||||
import {
|
||||
channel_game_move,
|
||||
channel_leave_game,
|
||||
channel_on_game_crashed,
|
||||
channel_on_game_start,
|
||||
channel_on_game_update,
|
||||
channel_on_game_user_leave,
|
||||
} from "../const/channel_names";
|
||||
import InfoPanel from "../components/InfoPanel";
|
||||
import { CommonMancalaGame, MancalaGame, Pit } from "mancala.js";
|
||||
import { GameMove } from "../models/GameMove";
|
||||
import PitAnimator from "../animation/PitAnimator";
|
||||
import BoardViewModel from "../viewmodel/BoardViewModel";
|
||||
import { v4 } from "uuid";
|
||||
import { getColorByBrightness } from "../util/ColorUtil";
|
||||
import { Theme } from "../theme/Theme";
|
||||
import HeaderBar from "../components/HeaderBar";
|
||||
import FloatingPanel from "../components/FloatingPanel";
|
||||
import PageContainer from "../components/PageContainer";
|
||||
|
||||
type ConnectionState = "connecting" | "error" | "connected" | "reconnecting";
|
||||
|
||||
const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
|
||||
const [userKey, setUserKey] = useState<string | undefined>(undefined);
|
||||
|
||||
const [connectionState, setConnetionState] =
|
||||
useState<ConnectionState>("connecting");
|
||||
|
||||
const [searchingOpponent, setSearchingOpponent] = useState<boolean>(false);
|
||||
|
||||
const [game, setGame] = useState<CommonMancalaGame | undefined>(undefined);
|
||||
|
||||
const [crashMessage, setCrashMessage] = useState<string | undefined>(undefined);
|
||||
|
||||
const [userKeyWhoLeave, setUserKeyWhoLeave] = useState<string | undefined>(undefined);
|
||||
|
||||
const [boardViewModel, setBoardViewModel] = useState<BoardViewModel | undefined>(undefined);
|
||||
|
||||
const [boardId, setBoardId] = useState<string>("-1");
|
||||
|
||||
const [pitAnimator, setPitAnimator] = useState<PitAnimator | undefined>(undefined);
|
||||
|
||||
const [theme, setTheme] = useState<Theme | undefined>(undefined);
|
||||
|
||||
const onConnectionDone = () => {
|
||||
setConnetionState("connected");
|
||||
};
|
||||
|
||||
const onConnectionLost = () => {
|
||||
connectToServer("reconnecting");
|
||||
};
|
||||
|
||||
const onConnectionError = (event: Event) => {
|
||||
setConnetionState("error");
|
||||
};
|
||||
|
||||
const connectToServer = (connectionState: ConnectionState) => {
|
||||
setConnetionState(connectionState);
|
||||
context.userKeyStore.getUserKey((userKey: string) => {
|
||||
setUserKey(userKey);
|
||||
const rtmtws = context.rtmt as RTMTWS;
|
||||
if (rtmtws) {
|
||||
rtmtws.initWebSocket(
|
||||
userKey,
|
||||
onConnectionDone,
|
||||
onConnectionLost,
|
||||
onConnectionError
|
||||
);
|
||||
} else {
|
||||
console.error("context.rtmt is not RTMTWS");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const listenMessages = (pitAnimator: PitAnimator) => {
|
||||
context.rtmt.listenMessage(channel_on_game_start, (message: Object) => {
|
||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||
const mancalaGame = MancalaGame.createFromMancalaGame(newGame);
|
||||
setSearchingOpponent(false);
|
||||
setGame(mancalaGame);
|
||||
pitAnimator.setNewGame(mancalaGame);
|
||||
});
|
||||
|
||||
context.rtmt.listenMessage(channel_on_game_update, (message: Object) => {
|
||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||
const mancalaGame = MancalaGame.createFromMancalaGame(newGame);
|
||||
setGame(mancalaGame);
|
||||
pitAnimator.setUpdatedGame(mancalaGame);
|
||||
});
|
||||
|
||||
context.rtmt.listenMessage(channel_on_game_crashed, (message: any) => {
|
||||
const newCrashMessage = message as string;
|
||||
console.error("on_game_crash");
|
||||
console.error(newCrashMessage);
|
||||
setCrashMessage(newCrashMessage);
|
||||
});
|
||||
|
||||
context.rtmt.listenMessage(channel_on_game_user_leave, (message: any) => {
|
||||
const userKeyWhoLeave = message;
|
||||
setUserKeyWhoLeave(userKeyWhoLeave);
|
||||
});
|
||||
};
|
||||
|
||||
const updateBoardViewModel = (boardViewModel: BoardViewModel) => {
|
||||
boardViewModel.id = v4();
|
||||
setBoardId(boardViewModel.id);
|
||||
setBoardViewModel(boardViewModel);
|
||||
};
|
||||
|
||||
const resetGameState = () => {
|
||||
setGame(undefined);
|
||||
setCrashMessage(undefined);
|
||||
setUserKeyWhoLeave(undefined);
|
||||
};
|
||||
|
||||
const getBoardIndex = (index: number) => {
|
||||
if(!game) return -1;
|
||||
if (userKey === game.player2Id) return index + game.board.pits.length / 2;
|
||||
return index;
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setTheme(context.themeManager.theme);
|
||||
const pitAnimator = new PitAnimator(context, updateBoardViewModel);
|
||||
setPitAnimator(pitAnimator);
|
||||
listenMessages(pitAnimator);
|
||||
connectToServer("connecting");
|
||||
return () => {
|
||||
pitAnimator.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
context.themeManager.onThemeChange = (theme) => {
|
||||
setTheme(theme);
|
||||
pitAnimator && game && updateBoardViewModel(pitAnimator.getBoardViewModelFromGame(game));
|
||||
};
|
||||
}, [boardViewModel]);
|
||||
|
||||
const onNewGameClick = () => {
|
||||
resetGameState();
|
||||
setSearchingOpponent(true);
|
||||
context.rtmt.sendMessage("new_game", {});
|
||||
};
|
||||
|
||||
const onLeaveGameClick = () => {
|
||||
context.rtmt.sendMessage(channel_leave_game, {});
|
||||
};
|
||||
|
||||
const onPitSelect = (index: number, pit: Pit) => {
|
||||
if(!boardViewModel) return;
|
||||
//TODO : stoneCount comes from view model!
|
||||
if (pit.stoneCount === 0) {
|
||||
//TODO : warn user
|
||||
return;
|
||||
}
|
||||
boardViewModel.pits[getBoardIndex(index)].pitColor =
|
||||
context.themeManager.theme.pitSelectedColor;
|
||||
updateBoardViewModel(boardViewModel);
|
||||
const gameMove: GameMove = { index: index };
|
||||
context.rtmt.sendMessage(channel_game_move, gameMove);
|
||||
};
|
||||
|
||||
const showConnectionState = connectionState != "connected";
|
||||
|
||||
const connectionStateText = () => {
|
||||
let map: { [key: string]: string } = {
|
||||
connecting: context.texts.Connecting,
|
||||
connected: context.texts.Connected,
|
||||
error: context.texts.CannotConnect,
|
||||
reconnecting: context.texts.ConnectingAgain,
|
||||
};
|
||||
|
||||
return map[connectionState];
|
||||
};
|
||||
|
||||
const textColorOnBoard = getColorByBrightness(
|
||||
context.themeManager.theme.boardColor,
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContainer theme={theme!}>
|
||||
<FloatingPanel context={context} color={context.themeManager.theme.boardColor} visible={showConnectionState}>
|
||||
<span style={{ color: textColorOnBoard }}>{connectionStateText()}</span>
|
||||
</FloatingPanel>
|
||||
<HeaderBar
|
||||
context={context}
|
||||
game={game}
|
||||
userKeyWhoLeave={userKeyWhoLeave}
|
||||
crashMessage={crashMessage}
|
||||
onNewGameClick={onNewGameClick}
|
||||
onLeaveGameClick={onLeaveGameClick} />
|
||||
<InfoPanel
|
||||
context={context}
|
||||
game={game}
|
||||
crashMessage={crashMessage}
|
||||
userKey={userKey}
|
||||
userKeyWhoLeave={userKeyWhoLeave}
|
||||
searchingOpponent={searchingOpponent} />
|
||||
{game && boardViewModel && userKey && (
|
||||
<BoardView
|
||||
userKey={userKey}
|
||||
game={game}
|
||||
boardId={boardId}
|
||||
boardViewModel={boardViewModel}
|
||||
context={context}
|
||||
onPitSelect={onPitSelect} />
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@ -19,7 +19,7 @@ const darkTheme: Theme = {
|
||||
textLightColor: "#AAAAAA",
|
||||
playerTurnColor: colors.tertiary,
|
||||
boardColor: colors.secondary,
|
||||
holeColor: colors.tertiary,
|
||||
pitColor: colors.tertiary,
|
||||
pitSelectedColor: colors.secondary,
|
||||
stoneColor: "#252525",
|
||||
stoneLightColor: "#252525",
|
||||
|
||||
@ -9,7 +9,7 @@ const greyTheme: Theme = {
|
||||
textLightColor: "#EEEEEE",
|
||||
playerTurnColor: "#84b8a6",
|
||||
boardColor: "#4D606E",
|
||||
holeColor: "#D3D4D8",
|
||||
pitColor: "#D3D4D8",
|
||||
pitSelectedColor: "#8837fa",
|
||||
stoneColor: "#393E46",
|
||||
stoneLightColor: "#EEEEEE",
|
||||
|
||||
@ -11,7 +11,7 @@ const lightTheme: Theme = {
|
||||
textLightColor: "#EBEBEB",
|
||||
playerTurnColor: "#6B6B6B",
|
||||
boardColor: "#9B9B9B",
|
||||
holeColor: "#B8B8B8",
|
||||
pitColor: "#B8B8B8",
|
||||
pitSelectedColor: "#9B9B9B",
|
||||
stoneColor: "#5B5B5B",
|
||||
stoneLightColor: "#3B3B3B",
|
||||
|
||||
@ -7,7 +7,7 @@ export type Theme = {
|
||||
appBarBgColor: string;
|
||||
playerTurnColor: string;
|
||||
boardColor: string;
|
||||
holeColor: string;
|
||||
pitColor: string;
|
||||
pitSelectedColor: string;
|
||||
stoneColor: string;
|
||||
stoneLightColor: string;
|
||||
|
||||
10
src/util/Util.ts
Normal file
10
src/util/Util.ts
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
export default class Util {
|
||||
public static range(size: number) {
|
||||
var ans : number[] = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
ans.push(i);
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user