commit
0976ae668a
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mancala-frontend",
|
"name": "mancala-frontend",
|
||||||
"version": "0.1.3-beta.12",
|
"version": "0.1.3-beta.12",
|
||||||
"description": "",
|
"description": "Mancala Game Frontend",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "parcel src/index.html",
|
"dev": "parcel src/index.html",
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { initContext } from './context';
|
import { initContext } from './context/context';
|
||||||
|
|
||||||
import Home from './Home';
|
import Home from './routes/Home';
|
||||||
|
|
||||||
initContext();
|
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,
|
GAME_STEP_DOUBLE_STONE_IN_PIT,
|
||||||
} from "mancala.js";
|
} from "mancala.js";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { Context } from "../context";
|
import { Context } from "../context/context";
|
||||||
import BoardViewModelFactory from "../factory/BoardViewModelFactory";
|
import BoardViewModelFactory from "../factory/BoardViewModelFactory";
|
||||||
import { PitViewModelFactory } from "../factory/PitViewModelFactory";
|
import { PitViewModelFactory } from "../factory/PitViewModelFactory";
|
||||||
import { getColorByBrightness } from "../util/ColorUtil";
|
import { getColorByBrightness } from "../util/ColorUtil";
|
||||||
@ -19,14 +19,14 @@ const animationUpdateInterval = 300;
|
|||||||
|
|
||||||
export default class PitAnimator {
|
export default class PitAnimator {
|
||||||
context: Context;
|
context: Context;
|
||||||
game: MancalaGame;
|
game: MancalaGame | undefined;
|
||||||
oldGame: MancalaGame;
|
oldGame: MancalaGame | undefined;
|
||||||
currentIntervalID: number;
|
currentIntervalID: number;
|
||||||
onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void;
|
onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void;
|
||||||
boardViewModel: BoardViewModel;
|
boardViewModel: BoardViewModel | undefined;
|
||||||
oldBoardViewModel: BoardViewModel;
|
oldBoardViewModel: BoardViewModel | undefined;
|
||||||
animationIndex: number = 0;
|
animationIndex: number = 0;
|
||||||
currentHistoryItem: HistoryItem;
|
currentHistoryItem: HistoryItem | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
@ -55,7 +55,7 @@ export default class PitAnimator {
|
|||||||
|
|
||||||
onGameMoveAnimationStart() {
|
onGameMoveAnimationStart() {
|
||||||
this.stopCurrentAnimation();
|
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];
|
const lastHistoryItem = this.game.history[this.game.history.length - 1];
|
||||||
if (lastHistoryItem.gameSteps.length > 0) {
|
if (lastHistoryItem.gameSteps.length > 0) {
|
||||||
this.animationIndex = 0;
|
this.animationIndex = 0;
|
||||||
@ -68,6 +68,7 @@ export default class PitAnimator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onAnimate() {
|
onAnimate() {
|
||||||
|
if(!this.currentHistoryItem || !this.game || !this.oldBoardViewModel) return;
|
||||||
if (this.animationIndex === this.currentHistoryItem.gameSteps.length) {
|
if (this.animationIndex === this.currentHistoryItem.gameSteps.length) {
|
||||||
this.clearCurrentInterval();
|
this.clearCurrentInterval();
|
||||||
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
||||||
@ -91,9 +92,10 @@ export default class PitAnimator {
|
|||||||
boardViewModel: BoardViewModel,
|
boardViewModel: BoardViewModel,
|
||||||
gameStep: GameStep
|
gameStep: GameStep
|
||||||
) {
|
) {
|
||||||
|
if(!this.currentHistoryItem || !this.game) return;
|
||||||
const pitViewModel = boardViewModel.pits[index];
|
const pitViewModel = boardViewModel.pits[index];
|
||||||
if (this.animationIndex === 0) {
|
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) {
|
if (this.getGameMoveStepCount(this.currentHistoryItem) === 1) {
|
||||||
const previousPitIndex = gameStep.index - 1;
|
const previousPitIndex = gameStep.index - 1;
|
||||||
if (previousPitIndex > 0) {
|
if (previousPitIndex > 0) {
|
||||||
@ -172,7 +174,7 @@ export default class PitAnimator {
|
|||||||
const theme = this.context.themeManager.theme;
|
const theme = this.context.themeManager.theme;
|
||||||
const stoneCount = pit.stoneCount;
|
const stoneCount = pit.stoneCount;
|
||||||
const stoneColor = theme.stoneColor;
|
const stoneColor = theme.stoneColor;
|
||||||
const pitColor = theme.holeColor;
|
const pitColor = theme.pitColor;
|
||||||
const id = pit.index.toString();
|
const id = pit.index.toString();
|
||||||
return PitViewModelFactory.create({
|
return PitViewModelFactory.create({
|
||||||
id,
|
id,
|
||||||
@ -185,15 +187,15 @@ export default class PitAnimator {
|
|||||||
|
|
||||||
public resetAnimationState() {
|
public resetAnimationState() {
|
||||||
this.animationIndex = -1;
|
this.animationIndex = -1;
|
||||||
this.currentHistoryItem = null;
|
this.currentHistoryItem = undefined;
|
||||||
this.boardViewModel = null;
|
this.boardViewModel = undefined;
|
||||||
this.oldBoardViewModel = null;
|
this.oldBoardViewModel = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public reset() {
|
public reset() {
|
||||||
this.resetAnimationState();
|
this.resetAnimationState();
|
||||||
this.game = null;
|
this.game = undefined;
|
||||||
this.oldGame = null;
|
this.oldGame = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
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 * as React from "react";
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
import { Context } from "../context";
|
import { Context } from "../context/context";
|
||||||
import { getColorByBrightness } from "../util/ColorUtil";
|
import { getColorByBrightness } from "../util/ColorUtil";
|
||||||
|
|
||||||
const Button: FunctionComponent<{
|
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 { MancalaGame } from "mancala.js";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
import { Context } from "../context";
|
import { Context } from "../context/context";
|
||||||
import { getColorByBrightness } from "../util/ColorUtil";
|
import { getColorByBrightness } from "../util/ColorUtil";
|
||||||
|
|
||||||
function getInfoPanelTextByGameState(params: {
|
function getInfoPanelTextByGameState(params: {
|
||||||
context: Context;
|
context: Context;
|
||||||
game: MancalaGame;
|
game?: MancalaGame;
|
||||||
crashMessage: string;
|
crashMessage?: string;
|
||||||
userKey: string;
|
userKey?: string;
|
||||||
userKeyWhoLeave: string;
|
userKeyWhoLeave?: string;
|
||||||
searchingOpponent: boolean;
|
searchingOpponent: boolean;
|
||||||
}): string {
|
}): string | undefined {
|
||||||
const {
|
const {
|
||||||
context,
|
context,
|
||||||
game,
|
game,
|
||||||
@ -42,9 +42,9 @@ function getInfoPanelTextByGameState(params: {
|
|||||||
return context.texts.GameEnded + " " + whoWon;
|
return context.texts.GameEnded + " " + whoWon;
|
||||||
} else {
|
} else {
|
||||||
if (game) {
|
if (game) {
|
||||||
return game.checkIsPlayerTurn(userKey)
|
return userKey ? game.checkIsPlayerTurn(userKey)
|
||||||
? context.texts.YourTurn
|
? context.texts.YourTurn
|
||||||
: context.texts.OpponentTurn;
|
: context.texts.OpponentTurn : undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -70,10 +70,10 @@ const InfoPanelContainer: FunctionComponent<{
|
|||||||
|
|
||||||
const InfoPanel: FunctionComponent<{
|
const InfoPanel: FunctionComponent<{
|
||||||
context: Context;
|
context: Context;
|
||||||
game: MancalaGame;
|
game?: MancalaGame;
|
||||||
crashMessage: string;
|
crashMessage?: string;
|
||||||
userKey: string;
|
userKey?: string;
|
||||||
userKeyWhoLeave: string;
|
userKeyWhoLeave?: string;
|
||||||
searchingOpponent: boolean;
|
searchingOpponent: boolean;
|
||||||
}> = ({
|
}> = ({
|
||||||
context,
|
context,
|
||||||
@ -83,7 +83,7 @@ const InfoPanel: FunctionComponent<{
|
|||||||
userKeyWhoLeave,
|
userKeyWhoLeave,
|
||||||
searchingOpponent,
|
searchingOpponent,
|
||||||
}) => {
|
}) => {
|
||||||
const isUserTurn = game?.checkIsPlayerTurn(userKey);
|
const isUserTurn = userKey ? game?.checkIsPlayerTurn(userKey) : false;
|
||||||
const containerColor = isUserTurn
|
const containerColor = isUserTurn
|
||||||
? context.themeManager.theme.playerTurnColor
|
? context.themeManager.theme.playerTurnColor
|
||||||
: context.themeManager.theme.boardColor;
|
: 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 { Texts, TrTr } from "../const/texts";
|
||||||
import { RTMT } from "./rtmt/rtmt";
|
import { RTMT } from "../rtmt/rtmt";
|
||||||
import { RTMTWS } from "./rtmt/rtmt_websocket";
|
import { RTMTWS } from "../rtmt/rtmt_websocket";
|
||||||
import { UserKeyStore, UserKeyStoreImpl } from "./store/key_store";
|
import { UserKeyStore, UserKeyStoreImpl } from "../store/key_store";
|
||||||
import ThemeManager from "./theme/ThemeManager";
|
import ThemeManager from "../theme/ThemeManager";
|
||||||
|
|
||||||
export type Context = {
|
export type Context = {
|
||||||
rtmt: RTMT;
|
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",
|
textLightColor: "#AAAAAA",
|
||||||
playerTurnColor: colors.tertiary,
|
playerTurnColor: colors.tertiary,
|
||||||
boardColor: colors.secondary,
|
boardColor: colors.secondary,
|
||||||
holeColor: colors.tertiary,
|
pitColor: colors.tertiary,
|
||||||
pitSelectedColor: colors.secondary,
|
pitSelectedColor: colors.secondary,
|
||||||
stoneColor: "#252525",
|
stoneColor: "#252525",
|
||||||
stoneLightColor: "#252525",
|
stoneLightColor: "#252525",
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const greyTheme: Theme = {
|
|||||||
textLightColor: "#EEEEEE",
|
textLightColor: "#EEEEEE",
|
||||||
playerTurnColor: "#84b8a6",
|
playerTurnColor: "#84b8a6",
|
||||||
boardColor: "#4D606E",
|
boardColor: "#4D606E",
|
||||||
holeColor: "#D3D4D8",
|
pitColor: "#D3D4D8",
|
||||||
pitSelectedColor: "#8837fa",
|
pitSelectedColor: "#8837fa",
|
||||||
stoneColor: "#393E46",
|
stoneColor: "#393E46",
|
||||||
stoneLightColor: "#EEEEEE",
|
stoneLightColor: "#EEEEEE",
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const lightTheme: Theme = {
|
|||||||
textLightColor: "#EBEBEB",
|
textLightColor: "#EBEBEB",
|
||||||
playerTurnColor: "#6B6B6B",
|
playerTurnColor: "#6B6B6B",
|
||||||
boardColor: "#9B9B9B",
|
boardColor: "#9B9B9B",
|
||||||
holeColor: "#B8B8B8",
|
pitColor: "#B8B8B8",
|
||||||
pitSelectedColor: "#9B9B9B",
|
pitSelectedColor: "#9B9B9B",
|
||||||
stoneColor: "#5B5B5B",
|
stoneColor: "#5B5B5B",
|
||||||
stoneLightColor: "#3B3B3B",
|
stoneLightColor: "#3B3B3B",
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export type Theme = {
|
|||||||
appBarBgColor: string;
|
appBarBgColor: string;
|
||||||
playerTurnColor: string;
|
playerTurnColor: string;
|
||||||
boardColor: string;
|
boardColor: string;
|
||||||
holeColor: string;
|
pitColor: string;
|
||||||
pitSelectedColor: string;
|
pitSelectedColor: string;
|
||||||
stoneColor: string;
|
stoneColor: string;
|
||||||
stoneLightColor: 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