Merge pull request #3 from jhalitaksoy/feature/pit-animator
Feature/pit animator
This commit is contained in:
commit
5209c45bee
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mancala-frontend",
|
"name": "mancala-frontend",
|
||||||
"version": "0.1.3-beta.2",
|
"version": "0.1.3-beta.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -14,9 +14,11 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
"mancala.js": "^0.0.2-beta.1",
|
"mancala.js": "^0.0.2-beta.1",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2"
|
"react-dom": "^17.0.2",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^17.0.11",
|
"@types/react": "^17.0.11",
|
||||||
|
|||||||
81
src/Home.tsx
81
src/Home.tsx
@ -6,6 +6,8 @@ import { RTMTWS } from "./rtmt/rtmt_websocket";
|
|||||||
import {
|
import {
|
||||||
channel_game_move,
|
channel_game_move,
|
||||||
channel_leave_game,
|
channel_leave_game,
|
||||||
|
channel_on_game_crashed,
|
||||||
|
channel_on_game_start,
|
||||||
channel_on_game_update,
|
channel_on_game_update,
|
||||||
channel_on_game_user_leave,
|
channel_on_game_user_leave,
|
||||||
} from "./channel_names";
|
} from "./channel_names";
|
||||||
@ -13,6 +15,8 @@ import Button from "./components/Button";
|
|||||||
import InfoPanel from "./components/InfoPanel";
|
import InfoPanel from "./components/InfoPanel";
|
||||||
import { CommonMancalaGame, MancalaGame, Pit } from "mancala.js";
|
import { CommonMancalaGame, MancalaGame, Pit } from "mancala.js";
|
||||||
import { GameMove } from "./models/GameMove";
|
import { GameMove } from "./models/GameMove";
|
||||||
|
import PitAnimator from "./animation/PitAnimator";
|
||||||
|
import BoardViewModel from "./viewmodel/BoardViewModel";
|
||||||
|
|
||||||
type ConnectionState = "connecting" | "error" | "connected" | "reconnecting";
|
type ConnectionState = "connecting" | "error" | "connected" | "reconnecting";
|
||||||
|
|
||||||
@ -25,19 +29,16 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
|
|||||||
const [searchingOpponent, setSearchingOpponent] = useState<boolean>(false);
|
const [searchingOpponent, setSearchingOpponent] = useState<boolean>(false);
|
||||||
|
|
||||||
const [game, setGame] = useState<CommonMancalaGame>(undefined);
|
const [game, setGame] = useState<CommonMancalaGame>(undefined);
|
||||||
const gameRef = React.useRef<CommonMancalaGame>(game);
|
|
||||||
|
|
||||||
const [crashMessage, setCrashMessage] = useState<string>(undefined);
|
const [crashMessage, setCrashMessage] = useState<string>(undefined);
|
||||||
|
|
||||||
const [userKeyWhoLeave, setUserKeyWhoLeave] = useState<string>(undefined);
|
const [userKeyWhoLeave, setUserKeyWhoLeave] = useState<string>(undefined);
|
||||||
|
|
||||||
const [animationPitIndex, setAnimationPitIndex] = useState<number>(-1);
|
const [boardViewModel, setBoardViewModel] = useState<BoardViewModel>(null);
|
||||||
|
|
||||||
const [intervalId, setIntervalId] = useState<number>(-1);
|
const [boardId, setBoardId] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
const [pitAnimator, setPitAnimator] = useState<PitAnimator>();
|
||||||
gameRef.current = game;
|
|
||||||
});
|
|
||||||
|
|
||||||
const onConnectionDone = () => {
|
const onConnectionDone = () => {
|
||||||
setConnetionState("connected");
|
setConnetionState("connected");
|
||||||
@ -69,44 +70,23 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const listenMessages = () => {
|
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) => {
|
context.rtmt.listenMessage(channel_on_game_update, (message: Object) => {
|
||||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||||
const mancalaGame = MancalaGame.createFromMancalaGame(newGame);
|
const mancalaGame = MancalaGame.createFromMancalaGame(newGame);
|
||||||
if (gameRef.current && mancalaGame.history.length > 0) {
|
|
||||||
const lastHistoryItem =
|
|
||||||
mancalaGame.history[mancalaGame.history.length - 1];
|
|
||||||
if (lastHistoryItem.gameSteps.length > 0) {
|
|
||||||
let stepIndex = 0;
|
|
||||||
if (intervalId) {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
}
|
|
||||||
const id = setInterval(() => {
|
|
||||||
if (stepIndex === lastHistoryItem.gameSteps.length) {
|
|
||||||
clearInterval(id);
|
|
||||||
setAnimationPitIndex(-1);
|
|
||||||
setGame(mancalaGame);
|
setGame(mancalaGame);
|
||||||
} else {
|
pitAnimator.setUpdatedGame(mancalaGame);
|
||||||
const gameStep = lastHistoryItem.gameSteps[stepIndex];
|
|
||||||
const index = mancalaGame.board.getPitIndexCircularly(
|
|
||||||
gameStep.index
|
|
||||||
);
|
|
||||||
setAnimationPitIndex(index);
|
|
||||||
}
|
|
||||||
stepIndex++;
|
|
||||||
}, 250);
|
|
||||||
setIntervalId(intervalId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
context.rtmt.listenMessage("on_game_start", (message: Object) => {
|
context.rtmt.listenMessage(channel_on_game_crashed, (message: any) => {
|
||||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
|
||||||
setSearchingOpponent(false);
|
|
||||||
setGame(MancalaGame.createFromMancalaGame(newGame));
|
|
||||||
});
|
|
||||||
|
|
||||||
context.rtmt.listenMessage("on_game_crashed", (message: any) => {
|
|
||||||
const newCrashMessage = message as string;
|
const newCrashMessage = message as string;
|
||||||
console.error("on_game_crash");
|
console.error("on_game_crash");
|
||||||
console.error(newCrashMessage);
|
console.error(newCrashMessage);
|
||||||
@ -119,9 +99,19 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateBoardViewModel = (boardViewModel: BoardViewModel) => {
|
||||||
|
setBoardId(boardViewModel.id);
|
||||||
|
setBoardViewModel(boardViewModel);
|
||||||
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
listenMessages();
|
const pitAnimator = new PitAnimator(context, updateBoardViewModel);
|
||||||
|
setPitAnimator(pitAnimator);
|
||||||
|
listenMessages(pitAnimator);
|
||||||
connectToServer("connecting");
|
connectToServer("connecting");
|
||||||
|
return () => {
|
||||||
|
pitAnimator.dispose();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const resetGameState = () => {
|
const resetGameState = () => {
|
||||||
@ -140,7 +130,12 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
|
|||||||
context.rtmt.sendMessage(channel_leave_game, {});
|
context.rtmt.sendMessage(channel_leave_game, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onHoleSelect = (index: number, hole: Pit) => {
|
const onHoleSelect = (index: number, pit: Pit) => {
|
||||||
|
//TODO : stoneCount comes from view model!
|
||||||
|
if (pit.stoneCount === 0) {
|
||||||
|
//TODO : warn user
|
||||||
|
return;
|
||||||
|
}
|
||||||
const gameMove: GameMove = { index: index };
|
const gameMove: GameMove = { index: index };
|
||||||
context.rtmt.sendMessage(channel_game_move, gameMove);
|
context.rtmt.sendMessage(channel_game_move, gameMove);
|
||||||
};
|
};
|
||||||
@ -240,12 +235,14 @@ const Home: FunctionComponent<{ initial?: number }> = ({ initial = 0 }) => {
|
|||||||
userKeyWhoLeave={userKeyWhoLeave}
|
userKeyWhoLeave={userKeyWhoLeave}
|
||||||
searchingOpponent={searchingOpponent}
|
searchingOpponent={searchingOpponent}
|
||||||
/>
|
/>
|
||||||
{game && (
|
{game && boardViewModel && (
|
||||||
<BoardView
|
<BoardView
|
||||||
userKey={userKey}
|
userKey={userKey}
|
||||||
game={game}
|
game={game}
|
||||||
|
boardId={boardId}
|
||||||
|
boardViewModel={boardViewModel}
|
||||||
|
context={context}
|
||||||
onHoleSelect={onHoleSelect}
|
onHoleSelect={onHoleSelect}
|
||||||
animationPitIndex={animationPitIndex}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
194
src/animation/PitAnimator.ts
Normal file
194
src/animation/PitAnimator.ts
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import {
|
||||||
|
MancalaGame,
|
||||||
|
GameStep,
|
||||||
|
HistoryItem,
|
||||||
|
GAME_STEP_GAME_MOVE,
|
||||||
|
GAME_STEP_LAST_STONE_IN_EMPTY_PIT,
|
||||||
|
GAME_STEP_BOARD_CLEARED,
|
||||||
|
GAME_STEP_LAST_STONE_IN_BANK,
|
||||||
|
} from "mancala.js";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
import { Context } from "../context";
|
||||||
|
import BoardViewModelFactory from "../factory/BoardViewModelFactory";
|
||||||
|
import { PitViewModelFactory } from "../factory/PitViewModelFactory";
|
||||||
|
import BoardViewModel from "../viewmodel/BoardViewModel";
|
||||||
|
|
||||||
|
const animationUpdateInterval = 300;
|
||||||
|
|
||||||
|
export default class PitAnimator {
|
||||||
|
context: Context;
|
||||||
|
game: MancalaGame;
|
||||||
|
oldGame: MancalaGame;
|
||||||
|
currentIntervalID: number;
|
||||||
|
onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void;
|
||||||
|
boardViewModel: BoardViewModel;
|
||||||
|
oldBoardViewModel: BoardViewModel;
|
||||||
|
animationIndex: number = 0;
|
||||||
|
currentHistoryItem: HistoryItem;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void
|
||||||
|
) {
|
||||||
|
this.context = context;
|
||||||
|
this.onBoardViewModelUpdate = onBoardViewModelUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setNewGame(game: MancalaGame) {
|
||||||
|
this.reset();
|
||||||
|
this.game = game;
|
||||||
|
this.updateBoardViewModel(this.getBoardViewModelFromGame(this.game));
|
||||||
|
}
|
||||||
|
|
||||||
|
public setUpdatedGame(game: MancalaGame, forceClear = false) {
|
||||||
|
this.resetAnimationState();
|
||||||
|
if (!this.game) {
|
||||||
|
this.setNewGame(game);
|
||||||
|
} else {
|
||||||
|
this.oldGame = this.game;
|
||||||
|
this.game = game;
|
||||||
|
this.onGameMoveAnimationStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onGameMoveAnimationStart() {
|
||||||
|
this.stopCurrentAnimation();
|
||||||
|
if (this.game.history.length > 0) {
|
||||||
|
const lastHistoryItem = this.game.history[this.game.history.length - 1];
|
||||||
|
if (lastHistoryItem.gameSteps.length > 0) {
|
||||||
|
this.animationIndex = 0;
|
||||||
|
this.currentHistoryItem = lastHistoryItem;
|
||||||
|
this.boardViewModel = this.getBoardViewModelFromGame(this.game);
|
||||||
|
this.oldBoardViewModel = this.getBoardViewModelFromGame(this.oldGame);
|
||||||
|
this.startAnimationUpdateCyle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onAnimate() {
|
||||||
|
if (this.animationIndex === this.currentHistoryItem.gameSteps.length) {
|
||||||
|
this.clearCurrentInterval();
|
||||||
|
this.updateBoardViewModel(this.getBoardViewModelFromGame(this.game));
|
||||||
|
} else {
|
||||||
|
const gameStep = this.currentHistoryItem.gameSteps[this.animationIndex];
|
||||||
|
const index = this.game.board.getPitIndexCircularly(gameStep.index);
|
||||||
|
this.animatePit(index, this.oldBoardViewModel, gameStep);
|
||||||
|
this.updateBoardViewModel(this.oldBoardViewModel);
|
||||||
|
}
|
||||||
|
this.animationIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGameMoveStepCount(historyItem: HistoryItem) {
|
||||||
|
return historyItem.gameSteps.filter(
|
||||||
|
(gameStep) => gameStep.type === GAME_STEP_GAME_MOVE
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
animatePit(
|
||||||
|
index: number,
|
||||||
|
boardViewModel: BoardViewModel,
|
||||||
|
gameStep: GameStep
|
||||||
|
) {
|
||||||
|
const pitViewModel = boardViewModel.pits[index];
|
||||||
|
if (this.animationIndex === 0) {
|
||||||
|
//This one stone move case, TODO : beautify it later
|
||||||
|
if (this.getGameMoveStepCount(this.currentHistoryItem) === 1) {
|
||||||
|
const previousPitIndex = gameStep.index - 1;
|
||||||
|
if (previousPitIndex > 0) {
|
||||||
|
boardViewModel.pits[previousPitIndex].stoneCount = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pitViewModel.stoneCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const theme = this.context.themeManager.theme;
|
||||||
|
if (gameStep.type === GAME_STEP_GAME_MOVE) {
|
||||||
|
pitViewModel.stoneCount += 1;
|
||||||
|
pitViewModel.pitColor = theme.pitGameMoveAnimateColor;
|
||||||
|
} else if (gameStep.type === GAME_STEP_LAST_STONE_IN_EMPTY_PIT) {
|
||||||
|
pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||||
|
pitViewModel.stoneCount = 0;
|
||||||
|
const oppositeIndex = this.game.board.getPitIndexCircularly(
|
||||||
|
gameStep.data.oppositeIndex
|
||||||
|
);
|
||||||
|
const oppositePitViewModel = boardViewModel.pits[oppositeIndex];
|
||||||
|
oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||||
|
oppositePitViewModel.stoneCount = 0;
|
||||||
|
} else if (gameStep.type === GAME_STEP_LAST_STONE_IN_BANK) {
|
||||||
|
pitViewModel.pitColor = theme.pitLastStoneInBankPitAnimateColor;
|
||||||
|
} else if (gameStep.type === GAME_STEP_BOARD_CLEARED) {
|
||||||
|
for (const index of gameStep.data.pitIndexesThatHasStone) {
|
||||||
|
const oppositeIndex = this.game.board.getPitIndexCircularly(index);
|
||||||
|
const oppositePitViewModel = boardViewModel.pits[oppositeIndex];
|
||||||
|
oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||||
|
oppositePitViewModel.stoneCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startAnimationUpdateCyle() {
|
||||||
|
this.clearCurrentInterval();
|
||||||
|
this.currentIntervalID = setInterval(
|
||||||
|
() => this.onAnimate(),
|
||||||
|
animationUpdateInterval
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopCurrentAnimation() {
|
||||||
|
this.clearCurrentInterval();
|
||||||
|
if (this.oldGame) {
|
||||||
|
this.updateBoardViewModel(this.getBoardViewModelFromGame(this.oldGame));
|
||||||
|
}
|
||||||
|
this.resetAnimationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCurrentInterval() {
|
||||||
|
if (this.currentIntervalID) {
|
||||||
|
clearInterval(this.currentIntervalID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBoardViewModel(boardViewModel: BoardViewModel) {
|
||||||
|
boardViewModel.id = v4();
|
||||||
|
this.onBoardViewModelUpdate?.(boardViewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBoardViewModelFromGame(game: MancalaGame): BoardViewModel {
|
||||||
|
const pitViewModels = this.createPitViewModelsFromGame(game);
|
||||||
|
return BoardViewModelFactory.create(v4(), pitViewModels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPitViewModelsFromGame(game: MancalaGame) {
|
||||||
|
return game.board.pits.map((pit) => {
|
||||||
|
const theme = this.context.themeManager.theme;
|
||||||
|
const stoneCount = pit.stoneCount;
|
||||||
|
const stoneColor = theme.ballColor;
|
||||||
|
const pitColor = theme.holeColor;
|
||||||
|
const id = pit.index.toString();
|
||||||
|
return PitViewModelFactory.create({
|
||||||
|
id,
|
||||||
|
stoneCount,
|
||||||
|
stoneColor,
|
||||||
|
pitColor,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetAnimationState() {
|
||||||
|
this.animationIndex = -1;
|
||||||
|
this.currentHistoryItem = null;
|
||||||
|
this.boardViewModel = null;
|
||||||
|
this.oldBoardViewModel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
this.resetAnimationState();
|
||||||
|
this.game = null;
|
||||||
|
this.oldGame = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
this.resetAnimationState();
|
||||||
|
this.clearCurrentInterval();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,30 +1,9 @@
|
|||||||
import { Bank, MancalaGame, Pit } from "mancala.js";
|
import { Bank, MancalaGame, Pit } from "mancala.js";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { FunctionComponent, useState } from "react";
|
import { FunctionComponent, useState } from "react";
|
||||||
|
import { Context } from "../context";
|
||||||
type Theme = {
|
import BoardViewModel from "../viewmodel/BoardViewModel";
|
||||||
background: string;
|
import PitViewModel from "../viewmodel/PitViewModel";
|
||||||
boardColor: string;
|
|
||||||
boardColorWhenPlayerTurn: string;
|
|
||||||
storeColor: string;
|
|
||||||
storeColorWhenPlayerTurn: string;
|
|
||||||
holeColor: string;
|
|
||||||
ballColor: string;
|
|
||||||
ballLightColor: string;
|
|
||||||
holeAnimateColor: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme: Theme = {
|
|
||||||
background: "#EEEEEE",
|
|
||||||
boardColor: "#4D606E",
|
|
||||||
boardColorWhenPlayerTurn: "#84b8a6",
|
|
||||||
storeColor: "#3FBAC2",
|
|
||||||
storeColorWhenPlayerTurn: "#6cab94",
|
|
||||||
holeColor: "#D3D4D8",
|
|
||||||
ballColor: "#393E46",
|
|
||||||
ballLightColor: "#393E46",
|
|
||||||
holeAnimateColor: "#afb3a4",
|
|
||||||
};
|
|
||||||
|
|
||||||
const BallView: FunctionComponent<{ color: string }> = ({ color }) => {
|
const BallView: FunctionComponent<{ color: string }> = ({ color }) => {
|
||||||
return (
|
return (
|
||||||
@ -48,41 +27,19 @@ function range(size: number) {
|
|||||||
return ans;
|
return ans;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PitContainer: FunctionComponent<{
|
|
||||||
pit: Pit;
|
|
||||||
isAnimating: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
}> = ({ pit, isAnimating, onClick }) => {
|
|
||||||
if (isAnimating) {
|
|
||||||
pit.stoneCount += 1;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<HoleView
|
|
||||||
hole={pit}
|
|
||||||
color={isAnimating ? theme.holeAnimateColor : theme.holeColor}
|
|
||||||
stoneColor={isAnimating ? theme.ballLightColor : theme.ballColor}
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const HoleView: FunctionComponent<{
|
const HoleView: FunctionComponent<{
|
||||||
hole: Pit;
|
pitViewModel: PitViewModel;
|
||||||
color: string;
|
|
||||||
stoneColor: string;
|
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}> = ({ hole, color, stoneColor, onClick }) => {
|
}> = ({ pitViewModel, onClick }) => {
|
||||||
const balls = [...range(hole.stoneCount)].map((i) => (
|
const balls = [...range(pitViewModel.stoneCount)].map((i) => (
|
||||||
<BallView color={stoneColor} />
|
<BallView color={pitViewModel.stoneColor} />
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={{
|
style={{
|
||||||
background: color,
|
background: pitViewModel.pitColor,
|
||||||
margin: "5px",
|
margin: "5px",
|
||||||
padding: "5px",
|
padding: "5px",
|
||||||
borderRadius: "10vw",
|
borderRadius: "10vw",
|
||||||
@ -100,21 +57,19 @@ const HoleView: FunctionComponent<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const StoreView: FunctionComponent<{
|
const StoreView: FunctionComponent<{
|
||||||
store: Bank;
|
pitViewModel: PitViewModel;
|
||||||
color: string;
|
|
||||||
stoneColor: string;
|
|
||||||
gridColumn: string;
|
gridColumn: string;
|
||||||
gridRow: string;
|
gridRow: string;
|
||||||
}> = ({ store, color, stoneColor, gridColumn, gridRow }) => {
|
}> = ({ pitViewModel, gridColumn, gridRow }) => {
|
||||||
const balls = [...range(store.stoneCount)].map((i) => (
|
const balls = [...range(pitViewModel.stoneCount)].map((i) => (
|
||||||
<BallView color={stoneColor} />
|
<BallView color={pitViewModel.stoneColor} />
|
||||||
));
|
));
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
gridColumn: gridColumn,
|
gridColumn: gridColumn,
|
||||||
gridRow: gridRow,
|
gridRow: gridRow,
|
||||||
background: color,
|
background: pitViewModel.pitColor,
|
||||||
margin: "5px",
|
margin: "5px",
|
||||||
borderRadius: "10vw",
|
borderRadius: "10vw",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -131,64 +86,49 @@ const StoreView: FunctionComponent<{
|
|||||||
|
|
||||||
const BoardView: FunctionComponent<{
|
const BoardView: FunctionComponent<{
|
||||||
game?: MancalaGame;
|
game?: MancalaGame;
|
||||||
|
context: Context;
|
||||||
|
boardId: string;
|
||||||
|
boardViewModel: BoardViewModel;
|
||||||
userKey: string;
|
userKey: string;
|
||||||
onHoleSelect: (index: number, hole: Pit) => void;
|
onHoleSelect: (index: number, hole: Pit) => void;
|
||||||
animationPitIndex: number;
|
}> = ({ game, context, boardId, boardViewModel, userKey, onHoleSelect }) => {
|
||||||
}> = ({ game, userKey, onHoleSelect, animationPitIndex }) => {
|
const createPitView = (pitViewModel: PitViewModel, onClick: () => void) => {
|
||||||
|
return <HoleView pitViewModel={pitViewModel} onClick={onClick} />;
|
||||||
|
};
|
||||||
const player1Pits = game?.board.player1Pits.map((pit) => {
|
const player1Pits = game?.board.player1Pits.map((pit) => {
|
||||||
const isAnimating = pit.index === animationPitIndex;
|
const pitViewModel = boardViewModel.pits[pit.index];
|
||||||
return (
|
return createPitView(pitViewModel, () => {
|
||||||
<PitContainer
|
|
||||||
pit={pit}
|
|
||||||
isAnimating={isAnimating}
|
|
||||||
onClick={() => {
|
|
||||||
if (game.turnPlayerId === game.player1Id)
|
if (game.turnPlayerId === game.player1Id)
|
||||||
onHoleSelect(game.board.player1Pits.indexOf(pit), pit);
|
onHoleSelect(game.board.player1Pits.indexOf(pit), pit);
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
const player2Pits = game!!.board.player2Pits.map((pit) => {
|
const player2Pits = game!!.board.player2Pits.map((pit) => {
|
||||||
const isAnimating = pit.index === animationPitIndex;
|
const pitViewModel = boardViewModel.pits[pit.index];
|
||||||
return (
|
return createPitView(pitViewModel, () => {
|
||||||
<PitContainer
|
|
||||||
pit={pit}
|
|
||||||
isAnimating={isAnimating}
|
|
||||||
onClick={() => {
|
|
||||||
if (game.turnPlayerId === game.player2Id)
|
if (game.turnPlayerId === game.player2Id)
|
||||||
onHoleSelect(game.board.player2Pits.indexOf(pit), pit);
|
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
|
||||||
|
pitViewModel={player1BankViewModel}
|
||||||
|
gridColumn="1 / 2"
|
||||||
|
gridRow="1 / 3"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const player2Bank = (
|
||||||
|
<StoreView
|
||||||
|
pitViewModel={player2BankViewModel}
|
||||||
|
gridColumn="8 / 9"
|
||||||
|
gridRow="1 / 3"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const isUserTurn = game.checkIsPlayerTurn(userKey);
|
|
||||||
|
|
||||||
const animatingPlayer1Bank =
|
|
||||||
game.board.player1Bank.index === animationPitIndex;
|
|
||||||
const animatingPlayer2Bank =
|
|
||||||
game.board.player2Bank.index === animationPitIndex;
|
|
||||||
|
|
||||||
const storeColorPlayer1 = animatingPlayer1Bank
|
|
||||||
? theme.holeAnimateColor
|
|
||||||
: isUserTurn
|
|
||||||
? theme.storeColor
|
|
||||||
: theme.storeColorWhenPlayerTurn;
|
|
||||||
|
|
||||||
const storeStoneColorPlayer1 = animatingPlayer1Bank
|
|
||||||
? theme.ballLightColor
|
|
||||||
: theme.ballColor;
|
|
||||||
|
|
||||||
const storeColorPlayer2 = animatingPlayer2Bank
|
|
||||||
? theme.holeAnimateColor
|
|
||||||
: isUserTurn
|
|
||||||
? theme.storeColor
|
|
||||||
: theme.storeColorWhenPlayerTurn;
|
|
||||||
|
|
||||||
const storeStoneColorPlayer2 = animatingPlayer2Bank
|
|
||||||
? theme.ballLightColor
|
|
||||||
: theme.ballColor;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -206,16 +146,12 @@ const BoardView: FunctionComponent<{
|
|||||||
{userKey === game.player2Id ? (
|
{userKey === game.player2Id ? (
|
||||||
<>
|
<>
|
||||||
<StoreView
|
<StoreView
|
||||||
store={game!!.board.player1Bank}
|
pitViewModel={player1BankViewModel}
|
||||||
color={storeColorPlayer1}
|
|
||||||
stoneColor={storeStoneColorPlayer1}
|
|
||||||
gridColumn="1 / 2"
|
gridColumn="1 / 2"
|
||||||
gridRow="1 / 3"
|
gridRow="1 / 3"
|
||||||
/>
|
/>
|
||||||
<StoreView
|
<StoreView
|
||||||
store={game!!.board.player2Bank}
|
pitViewModel={player2BankViewModel}
|
||||||
color={storeColorPlayer2}
|
|
||||||
stoneColor={storeStoneColorPlayer2}
|
|
||||||
gridColumn="8 / 9"
|
gridColumn="8 / 9"
|
||||||
gridRow="1 / 3"
|
gridRow="1 / 3"
|
||||||
/>
|
/>
|
||||||
@ -225,17 +161,13 @@ const BoardView: FunctionComponent<{
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<StoreView
|
<StoreView
|
||||||
store={game!!.board.player2Bank}
|
pitViewModel={player1BankViewModel}
|
||||||
color={storeColorPlayer2}
|
gridColumn="8 / 9"
|
||||||
stoneColor={storeStoneColorPlayer2}
|
|
||||||
gridColumn="1 / 2"
|
|
||||||
gridRow="1 / 3"
|
gridRow="1 / 3"
|
||||||
/>
|
/>
|
||||||
<StoreView
|
<StoreView
|
||||||
store={game!!.board.player1Bank}
|
pitViewModel={player2BankViewModel}
|
||||||
color={storeColorPlayer1}
|
gridColumn="1 / 2"
|
||||||
stoneColor={storeStoneColorPlayer1}
|
|
||||||
gridColumn="8 / 9"
|
|
||||||
gridRow="1 / 3"
|
gridRow="1 / 3"
|
||||||
/>
|
/>
|
||||||
{player2Pits.reverse()}
|
{player2Pits.reverse()}
|
||||||
|
|||||||
@ -1,23 +1,28 @@
|
|||||||
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 defaultTheme from "./theme/DefaultTheme";
|
||||||
|
import ThemeManager from "./theme/ThemeManager";
|
||||||
|
|
||||||
type Context = {
|
export type Context = {
|
||||||
rtmt : RTMT
|
rtmt: RTMT;
|
||||||
userKeyStore : UserKeyStore
|
userKeyStore: UserKeyStore;
|
||||||
texts : Texts
|
texts: Texts;
|
||||||
}
|
themeManager: ThemeManager;
|
||||||
|
};
|
||||||
|
|
||||||
export const initContext = ()=> {
|
export const initContext = () => {
|
||||||
const rtmt = new RTMTWS()
|
const rtmt = new RTMTWS();
|
||||||
const userKeyStore = new UserKeyStoreImpl()
|
const userKeyStore = new UserKeyStoreImpl();
|
||||||
const texts = TrTr
|
const texts = TrTr;
|
||||||
|
const themeManager = new ThemeManager(defaultTheme);
|
||||||
return {
|
return {
|
||||||
rtmt : rtmt,
|
rtmt: rtmt,
|
||||||
userKeyStore : userKeyStore,
|
userKeyStore: userKeyStore,
|
||||||
texts : texts,
|
texts: texts,
|
||||||
}
|
themeManager,
|
||||||
}
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const context : Context = initContext()
|
export const context: Context = initContext();
|
||||||
|
|||||||
11
src/factory/BoardViewModelFactory.ts
Normal file
11
src/factory/BoardViewModelFactory.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import BoardViewModel from "../viewmodel/BoardViewModel";
|
||||||
|
import PitViewModel from "../viewmodel/PitViewModel";
|
||||||
|
|
||||||
|
export default class BoardViewModelFactory {
|
||||||
|
public static create(
|
||||||
|
id: string,
|
||||||
|
pitViewModels: PitViewModel[]
|
||||||
|
): BoardViewModel {
|
||||||
|
return new BoardViewModel(id, pitViewModels);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/factory/PitViewModelFactory.ts
Normal file
13
src/factory/PitViewModelFactory.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import PitViewModel from "../viewmodel/PitViewModel";
|
||||||
|
|
||||||
|
export class PitViewModelFactory {
|
||||||
|
public static create(params: {
|
||||||
|
id: string;
|
||||||
|
stoneCount: number;
|
||||||
|
stoneColor: string;
|
||||||
|
pitColor: string;
|
||||||
|
}): PitViewModel {
|
||||||
|
const { id, stoneCount, stoneColor, pitColor } = params;
|
||||||
|
return new PitViewModel(id, stoneCount, stoneColor, pitColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/theme/DefaultTheme.ts
Normal file
18
src/theme/DefaultTheme.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Theme } from "./Theme";
|
||||||
|
|
||||||
|
const defaultTheme: Theme = {
|
||||||
|
background: "#EEEEEE",
|
||||||
|
boardColor: "#4D606E",
|
||||||
|
boardColorWhenPlayerTurn: "#84b8a6",
|
||||||
|
storeColor: "#3FBAC2",
|
||||||
|
storeColorWhenPlayerTurn: "#6cab94",
|
||||||
|
holeColor: "#D3D4D8",
|
||||||
|
ballColor: "#393E46",
|
||||||
|
ballLightColor: "#393E46",
|
||||||
|
pitGameMoveAnimateColor: "#c9b43c",
|
||||||
|
pitEmptyPitAnimateColor: "#5d7322",
|
||||||
|
pitLastStoneInBankPitAnimateColor: "#9463f7",
|
||||||
|
pitGetRivalStonePitAnimateColor: "#ff3d44",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defaultTheme;
|
||||||
14
src/theme/Theme.ts
Normal file
14
src/theme/Theme.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export type Theme = {
|
||||||
|
background: string;
|
||||||
|
boardColor: string;
|
||||||
|
boardColorWhenPlayerTurn: string;
|
||||||
|
storeColor: string;
|
||||||
|
storeColorWhenPlayerTurn: string;
|
||||||
|
holeColor: string;
|
||||||
|
ballColor: string;
|
||||||
|
ballLightColor: string;
|
||||||
|
pitGameMoveAnimateColor: string;
|
||||||
|
pitEmptyPitAnimateColor: string;
|
||||||
|
pitLastStoneInBankPitAnimateColor: string;
|
||||||
|
pitGetRivalStonePitAnimateColor: string;
|
||||||
|
};
|
||||||
18
src/theme/ThemeManager.ts
Normal file
18
src/theme/ThemeManager.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Theme } from "./Theme";
|
||||||
|
|
||||||
|
export default class ThemeManager {
|
||||||
|
_theme: Theme;
|
||||||
|
onThemeChange: (theme: Theme) => void;
|
||||||
|
constructor(theme: Theme) {
|
||||||
|
this._theme = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get theme() {
|
||||||
|
return this._theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set theme(value) {
|
||||||
|
this._theme = value;
|
||||||
|
this.onThemeChange?.(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/viewmodel/BoardViewModel.ts
Normal file
10
src/viewmodel/BoardViewModel.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import PitViewModel from "./PitViewModel";
|
||||||
|
|
||||||
|
export default class BoardViewModel {
|
||||||
|
id: string;
|
||||||
|
pits: PitViewModel[];
|
||||||
|
constructor(id: string, pits: PitViewModel[]) {
|
||||||
|
this.id = id;
|
||||||
|
this.pits = pits;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/viewmodel/PitViewModel.ts
Normal file
18
src/viewmodel/PitViewModel.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export default class PitViewModel {
|
||||||
|
id: string;
|
||||||
|
stoneCount: number;
|
||||||
|
stoneColor: string;
|
||||||
|
pitColor: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
id: string,
|
||||||
|
stoneCount: number,
|
||||||
|
stoneColor: string,
|
||||||
|
pitColor: string
|
||||||
|
) {
|
||||||
|
this.id = id;
|
||||||
|
this.stoneCount = stoneCount;
|
||||||
|
this.stoneColor = stoneColor;
|
||||||
|
this.pitColor = pitColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
yarn.lock
10
yarn.lock
@ -1042,6 +1042,11 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz"
|
resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz"
|
||||||
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
||||||
|
|
||||||
|
"@types/uuid@^8.3.4":
|
||||||
|
version "8.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
|
||||||
|
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
|
||||||
|
|
||||||
abab@^2.0.0:
|
abab@^2.0.0:
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz"
|
resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz"
|
||||||
@ -5377,6 +5382,11 @@ uuid@^3.3.2:
|
|||||||
resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"
|
resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"
|
||||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||||
|
|
||||||
|
uuid@^8.3.2:
|
||||||
|
version "8.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|
||||||
v8-compile-cache@^2.0.0:
|
v8-compile-cache@^2.0.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz"
|
resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user