Merge pull request #30 from jhalitaksoy/feature/game-model

Feature/game model
This commit is contained in:
Halit Aksoy 2022-08-01 22:35:24 +03:00 committed by GitHub
commit 2ea22f5303
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 100 additions and 62 deletions

View File

@ -1,5 +1,4 @@
import {
MancalaGame,
GameStep,
HistoryItem,
GAME_STEP_GAME_MOVE,
@ -7,11 +6,13 @@ import {
GAME_STEP_BOARD_CLEARED,
GAME_STEP_LAST_STONE_IN_BANK,
GAME_STEP_DOUBLE_STONE_IN_PIT,
MancalaGame,
} from "mancala.js";
import { v4 } from "uuid";
import { Context } from "../context/context";
import BoardViewModelFactory from "../factory/BoardViewModelFactory";
import { PitViewModelFactory } from "../factory/PitViewModelFactory";
import { Game } from "../models/Game";
import { getColorByBrightness } from "../util/ColorUtil";
import BoardViewModel from "../viewmodel/BoardViewModel";
@ -19,8 +20,8 @@ const animationUpdateInterval = 300;
export default class PitAnimator {
context: Context;
game: MancalaGame | undefined;
oldGame: MancalaGame | undefined;
game: Game | undefined;
oldGame: Game | undefined;
currentIntervalID: number;
onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void;
boardViewModel: BoardViewModel | undefined;
@ -36,13 +37,17 @@ export default class PitAnimator {
this.onBoardViewModelUpdate = onBoardViewModelUpdate;
}
public setNewGame(game: MancalaGame) {
get mancalaGame(): MancalaGame | undefined {
return this.game?.mancalaGame;
}
public setNewGame(game: Game) {
this.reset();
this.game = game;
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
}
public setUpdatedGame(game: MancalaGame) {
public setUpdatedGame(game: Game) {
this.resetAnimationState();
if (!this.game) {
this.setNewGame(game);
@ -55,8 +60,8 @@ export default class PitAnimator {
onGameMoveAnimationStart() {
this.stopCurrentAnimation();
if (this.game && this.oldGame && this.game.history.length > 0) {
const lastHistoryItem = this.game.history[this.game.history.length - 1];
if (this.game && this.oldGame && this.mancalaGame && this.mancalaGame?.history.length > 0) {
const lastHistoryItem = this.mancalaGame.history[this.mancalaGame.history.length - 1];
if (lastHistoryItem.gameSteps.length > 0) {
this.animationIndex = 0;
this.currentHistoryItem = lastHistoryItem;
@ -68,13 +73,13 @@ export default class PitAnimator {
}
onAnimate() {
if (!this.currentHistoryItem || !this.game || !this.oldBoardViewModel) return;
if (!this.currentHistoryItem || !this.game || !this.oldBoardViewModel || !this.mancalaGame) return;
if (this.animationIndex === this.currentHistoryItem.gameSteps.length) {
this.clearCurrentInterval();
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
} else {
const gameStep = this.currentHistoryItem.gameSteps[this.animationIndex];
const index = this.game.board.getPitIndexCircularly(gameStep.index);
const index = this.mancalaGame.board.getPitIndexCircularly(gameStep.index);
this.animatePit(index, this.oldBoardViewModel, gameStep);
this.onBoardViewModelUpdate?.(this.oldBoardViewModel);
}
@ -92,7 +97,7 @@ export default class PitAnimator {
boardViewModel: BoardViewModel,
gameStep: GameStep
) {
if (!this.currentHistoryItem || !this.game) return;
if (!this.currentHistoryItem || !this.game || !this.mancalaGame) return;
const pitViewModel = boardViewModel.pits[index];
if (this.animationIndex === 0) {
//This is one stone move case, TODO: beautify it later
@ -112,7 +117,7 @@ export default class PitAnimator {
} else if (gameStep.type === GAME_STEP_LAST_STONE_IN_EMPTY_PIT) {
pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
pitViewModel.stoneCount = 0;
const oppositeIndex = this.game.board.getPitIndexCircularly(
const oppositeIndex = this.mancalaGame.board.getPitIndexCircularly(
gameStep.data.oppositeIndex
);
const oppositePitViewModel = boardViewModel.pits[oppositeIndex];
@ -122,13 +127,13 @@ export default class PitAnimator {
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 oppositeIndex = this.mancalaGame.board.getPitIndexCircularly(index);
const oppositePitViewModel = boardViewModel.pits[oppositeIndex];
oppositePitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
oppositePitViewModel.stoneCount = 0;
}
} else if (gameStep.type === GAME_STEP_DOUBLE_STONE_IN_PIT) {
const _index = this.game.board.getPitIndexCircularly(index);
const _index = this.mancalaGame.board.getPitIndexCircularly(index);
const pitViewModel = boardViewModel.pits[_index];
pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
pitViewModel.stoneCount = 0;
@ -164,13 +169,13 @@ export default class PitAnimator {
}
}
public getBoardViewModelFromGame(game: MancalaGame): BoardViewModel {
public getBoardViewModelFromGame(game: Game): BoardViewModel {
const pitViewModels = this.createPitViewModelsFromGame(game);
return BoardViewModelFactory.create(v4(), pitViewModels);
}
private createPitViewModelsFromGame(game: MancalaGame) {
return game.board.pits.map((pit) => {
private createPitViewModelsFromGame(game: Game) {
return game.mancalaGame.board.pits.map((pit) => {
const theme = this.context.themeManager.theme;
const stoneCount = pit.stoneCount;
const stoneColor = theme.stoneColor;

View File

@ -1,13 +1,13 @@
import { MancalaGame } from "mancala.js";
import * as React from "react";
import { FunctionComponent } from "react";
import { Context } from "../context/context";
import { Game } from "../models/Game";
import { getColorByBrightness } from "../util/ColorUtil";
import CircularPanel from "./CircularPanel";
function getInfoPanelTextByGameState(params: {
context: Context;
game?: MancalaGame;
game?: Game;
crashMessage?: string;
userKey?: string;
userKeyWhoLeave?: string;
@ -27,10 +27,10 @@ function getInfoPanelTextByGameState(params: {
message = context.texts.YouLeftTheGame;
}
return message;
} else if (game?.state == "ended") {
const wonPlayer = game.getWonPlayerId();
} else if (game?.mancalaGame.state == "ended") {
const wonPlayer = game.mancalaGame.getWonPlayerId();
let whoWon =
game.getWonPlayerId() === userKey
game.mancalaGame.getWonPlayerId() === userKey
? context.texts.YouWon
: context.texts.YouLost;
if (!wonPlayer) {
@ -39,7 +39,7 @@ function getInfoPanelTextByGameState(params: {
return context.texts.GameEnded + " " + whoWon;
} else {
if (game) {
return userKey ? game.checkIsPlayerTurn(userKey)
return userKey ? game.mancalaGame.checkIsPlayerTurn(userKey)
? context.texts.YourTurn
: context.texts.OpponentTurn : undefined;
}
@ -49,7 +49,7 @@ function getInfoPanelTextByGameState(params: {
const InfoPanel: FunctionComponent<{
context: Context;
game?: MancalaGame;
game?: Game;
crashMessage?: string;
userKey?: string;
userKeyWhoLeave?: string;
@ -65,7 +65,7 @@ const InfoPanel: FunctionComponent<{
visible
}) => {
if (visible === false) return <></>;
const isUserTurn = userKey ? game?.checkIsPlayerTurn(userKey) : false;
const isUserTurn = userKey ? game?.mancalaGame.checkIsPlayerTurn(userKey) : false;
const containerColor = isUserTurn
? context.themeManager.theme.playerTurnColor
: context.themeManager.theme.boardColor;

View File

@ -1,43 +1,45 @@
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";
import { Game } from "../../models/Game";
import { Pit } from "mancala.js";
const BoardView: FunctionComponent<{
game: MancalaGame;
game: Game;
context: Context;
boardId: string;
boardViewModel: BoardViewModel;
userKey: string;
onPitSelect: (index: number, pit: Pit) => void;
}> = ({ game, context, boardId, boardViewModel, userKey, onPitSelect: onPitSelect }) => {
const mancalaGame = game?.mancalaGame;
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 player1Pits = mancalaGame?.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);
if (mancalaGame?.turnPlayerId === mancalaGame?.player1Id && userKey === mancalaGame?.player1Id)
onPitSelect(mancalaGame?.board.player1Pits.indexOf(pit), pit);
});
});
const player2Pits = game?.board.player2Pits.map((pit, index) => {
const player2Pits = mancalaGame?.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);
if (mancalaGame?.turnPlayerId === mancalaGame?.player2Id && userKey === mancalaGame?.player2Id)
onPitSelect(mancalaGame?.board.player2Pits.indexOf(pit), pit);
});
});
const theme = context.themeManager.theme;
const player1BankViewModel =
boardViewModel.pits[game.board.player1BankIndex()];
boardViewModel.pits[mancalaGame?.board.player1BankIndex()];
const player2BankViewModel =
boardViewModel.pits[game.board.player2BankIndex()];
const isPlayer2 = userKey === game?.player2Id;
boardViewModel.pits[mancalaGame?.board.player2BankIndex()];
const isPlayer2 = userKey === mancalaGame?.player2Id;
return (
<div className="board" style={{ background: theme.boardColor }}>
<StoreView

13
src/models/Game.ts Normal file
View File

@ -0,0 +1,13 @@
import { MancalaGame } from "mancala.js";
import { UserConnectionInfo } from "./UserConnectionInfo";
export interface Game {
id: string;
mancalaGame: MancalaGame;
gameUsersConnectionInfo: GameUsersConnectionInfo;
}
export interface GameUsersConnectionInfo {
user1ConnectionInfo: UserConnectionInfo;
user2ConnectionInfo: UserConnectionInfo;
}

View File

@ -1,4 +1,4 @@
import { CommonMancalaGame, MancalaGame, Pit } from 'mancala.js';
import { MancalaGame, Pit } from 'mancala.js';
import * as React from 'react';
import { FunctionComponent, useState } from 'react';
import { useNavigate, useParams } from 'react-router';
@ -23,11 +23,11 @@ import useWindowDimensions from '../hooks/useWindowDimensions';
import { ConnectionState } from '../models/ConnectionState';
import { GameMove } from '../models/GameMove';
import { LoadingState } from '../models/LoadingState';
import { UserConnectionInfo } from '../models/UserConnectionInfo';
import { Theme } from '../theme/Theme';
import { getColorByBrightness } from '../util/ColorUtil';
import BoardViewModel from '../viewmodel/BoardViewModel';
import Center from '../components/Center';
import { Game, GameUsersConnectionInfo } from '../models/Game';
const GamePage: FunctionComponent<{
context: Context,
@ -37,7 +37,7 @@ const GamePage: FunctionComponent<{
}> = ({ context, userKey, theme, connectionState }) => {
let params = useParams<{ gameId: string }>();
const [game, setGame] = useState<MancalaGame | undefined>(undefined);
const [game, setGame] = useState<Game | undefined>(undefined);
const [crashMessage, setCrashMessage] = useState<string | undefined>(undefined);
@ -53,20 +53,36 @@ const GamePage: FunctionComponent<{
// We have to block future actions if there is an ongoing action.
const [hasOngoingAction, setHasOngoingAction] = useState<boolean>(false);
const [isOpponentOnline, setIsOpponentOnline] = useState<boolean>(false);
const [gameUsersConnectionInfo, setGameUsersConnectionInfo] = useState<GameUsersConnectionInfo | undefined>();
const { height, width } = useWindowDimensions();
const navigate = useNavigate();
const [gameLoadingState, setLoadingStateGame] = useState<LoadingState<MancalaGame>>(LoadingState.Unset());
const [gameLoadingState, setLoadingStateGame] = useState<LoadingState<Game>>(LoadingState.Unset());
const onGameUpdate = (pitAnimator: PitAnimator, message: Object) => {
const newGame: CommonMancalaGame = message as CommonMancalaGame;
const mancalaGame = MancalaGame.createFromMancalaGame(newGame);
setGame(mancalaGame);
pitAnimator.setUpdatedGame(mancalaGame);
const mancalaGame: MancalaGame | undefined = game?.mancalaGame;
const onGameUpdate = (pitAnimator: PitAnimator, newGame: Game) => {
setGame(newGame);
pitAnimator.setUpdatedGame(newGame);
setHasOngoingAction(false);
setGameUsersConnectionInfo(newGame.gameUsersConnectionInfo);
}
const isUserOnline = (userId: string) => {
if (!gameUsersConnectionInfo) return false;
const user1ConnectionInfo = gameUsersConnectionInfo.user1ConnectionInfo;
const user2ConnectionInfo = gameUsersConnectionInfo.user2ConnectionInfo;
if (user1ConnectionInfo.userId === userId) return user1ConnectionInfo.isOnline;
if (user2ConnectionInfo.userId === userId) return user2ConnectionInfo.isOnline;
return false;
}
const onGameUpdateEvent = (pitAnimator: PitAnimator, message: Object) => {
const newGame: Game = message as Game;
newGame.mancalaGame = MancalaGame.createFromMancalaGame(newGame.mancalaGame);
onGameUpdate(pitAnimator, newGame);
}
const onGameCrashed = (message: any) => {
const newCrashMessage = message as string;
@ -80,13 +96,12 @@ const GamePage: FunctionComponent<{
setHasOngoingAction(false);
};
const onUserConnectionChange = (message: any) => {
const userConnectionInfo = message as UserConnectionInfo;
//todo: change this when implementing watch the game feature
setIsOpponentOnline(userConnectionInfo.isOnline);
const gameUsersConnectionInfo = message as GameUsersConnectionInfo;
setGameUsersConnectionInfo(gameUsersConnectionInfo);
};
const listenMessages = (pitAnimator: PitAnimator): () => void => {
const _onGameUpdate = (message: object) => onGameUpdate(pitAnimator, message);
const _onGameUpdate = (message: object) => onGameUpdateEvent(pitAnimator, message);
context.rtmt.listenMessage(channel_on_game_update, _onGameUpdate);
context.rtmt.listenMessage(channel_on_game_crashed, onGameCrashed);
context.rtmt.listenMessage(channel_on_game_user_leave, onGameUserLeave);
@ -106,12 +121,13 @@ const GamePage: FunctionComponent<{
};
const getBoardIndex = (index: number) => {
if (!game) return -1;
if (userKey === game.player2Id) return index + game.board.pits.length / 2;
if (!game || !mancalaGame) return -1;
const pitsLenght = mancalaGame.board.pits.length;
if (userKey === mancalaGame.player2Id) return index + pitsLenght / 2;
return index;
};
const getOpponentId = () => game?.player1Id === userKey ? game?.player2Id : game?.player1Id;
const getOpponentId = () => mancalaGame?.player1Id === userKey ? mancalaGame?.player2Id : mancalaGame?.player1Id;
const checkHasAnOngoingAction = () => hasOngoingAction;
@ -147,11 +163,9 @@ const GamePage: FunctionComponent<{
setLoadingStateGame(LoadingState.Loading())
context.gameStore.get(params.gameId!!).then((game) => {
if (game) {
setGame(game);
setHasOngoingAction(false);
pitAnimator = new PitAnimator(context, updateBoardViewModel);
pitAnimator.setNewGame(game);
setPitAnimator(pitAnimator);
onGameUpdate(pitAnimator, game);
unlistenMessages = listenMessages(pitAnimator);
setLoadingStateGame(LoadingState.Loaded({ value: game }))
} else {
@ -169,9 +183,10 @@ const GamePage: FunctionComponent<{
context.themeManager.theme.textColor,
context.themeManager.theme.textLightColor
);
const renderNewGameBtn = userKeyWhoLeave || !game || (game && game.state == "ended");
const renderNewGameBtn = userKeyWhoLeave || !game || (game && game.mancalaGame.state == "ended");
const showBoardView = game && boardViewModel && userKey && true;
const opponentUser = { id: getOpponentId() || "0", name: "Anonymous", isOnline: isOpponentOnline, isAnonymous: true };
const opponentId = getOpponentId();
const opponentUser = { id: getOpponentId() || "0", name: "Anonymous", isOnline: opponentId ? isUserOnline(opponentId) : false, isAnonymous: true };
const user = { id: userKey || "1", name: "Anonymous", isOnline: connectionState === "connected", isAnonymous: true };
const isMobile = width < 600;

View File

@ -1,8 +1,9 @@
import { CommonMancalaGame, MancalaGame } from "mancala.js";
import { Game } from "../models/Game";
import { HttpService } from "../service/HttpService";
export interface GameStore {
get(id: string): Promise<MancalaGame | undefined>;
get(id: string): Promise<Game | undefined>;
}
export class GameStoreImpl implements GameStore {
@ -11,11 +12,13 @@ export class GameStoreImpl implements GameStore {
this.httpService = props.httpService;
}
async get(id: string): Promise<MancalaGame | undefined> {
async get(id: string): Promise<Game | undefined> {
try {
const response = await this.httpService.get(`/game/${id}`);
const json = await response.json();
return MancalaGame.createFromMancalaGame(json as CommonMancalaGame);
const game: Game = json as Game;
game.mancalaGame = MancalaGame.createFromMancalaGame(game.mancalaGame);
return game;
} catch (error) {
// todo check error
Promise.resolve(undefined);