[mobile] implement basic working Game UI
This commit is contained in:
parent
341fe0e083
commit
2cca40034d
@ -12,16 +12,19 @@
|
||||
"dependencies": {
|
||||
"@react-navigation/native": "^6.1.17",
|
||||
"@react-navigation/native-stack": "^6.9.26",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"i18next": "^23.10.1",
|
||||
"mancala.js": "^0.0.2-beta.3",
|
||||
"react": "18.2.0",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-native": "0.73.6",
|
||||
"react-native-get-random-values": "^1.11.0",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-safe-area-context": "^4.9.0",
|
||||
"react-native-screens": "^3.29.0",
|
||||
"react-native-snackbar": "^2.6.2",
|
||||
"tiny-emitter": "^2.1.0"
|
||||
"tiny-emitter": "^2.1.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
|
||||
@ -12,6 +12,9 @@ import { ConnectionState } from './rtmt/rtmt';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Snackbar from 'react-native-snackbar';
|
||||
|
||||
// https://github.com/uuidjs/uuid/issues/514#issuecomment-691475020
|
||||
import 'react-native-get-random-values';
|
||||
|
||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||
|
||||
const context = initContext();
|
||||
|
||||
220
mobile/src/animation/PitAnimator.ts
Normal file
220
mobile/src/animation/PitAnimator.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import {
|
||||
GameStep,
|
||||
HistoryItem,
|
||||
GAME_STEP_GAME_MOVE,
|
||||
GAME_STEP_LAST_STONE_IN_EMPTY_PIT,
|
||||
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 { Theme } from "../theme/Theme";
|
||||
import { getColorByBrightness } from "../util/ColorUtil";
|
||||
import BoardViewModel from "../viewmodel/BoardViewModel";
|
||||
|
||||
const animationUpdateInterval = 300;
|
||||
|
||||
export default class PitAnimator {
|
||||
context: Context;
|
||||
game: Game | undefined;
|
||||
oldGame: Game | undefined;
|
||||
currentIntervalID: number | undefined;
|
||||
onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void;
|
||||
boardViewModel: BoardViewModel | undefined;
|
||||
oldBoardViewModel: BoardViewModel | undefined;
|
||||
animationIndex: number = 0;
|
||||
currentHistoryItem: HistoryItem | undefined;
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
onBoardViewModelUpdate: (boardViewModel: BoardViewModel) => void
|
||||
) {
|
||||
this.context = context;
|
||||
this.onBoardViewModelUpdate = onBoardViewModelUpdate;
|
||||
this.context.themeManager.on("themechange", this.onThemeChange.bind(this));
|
||||
}
|
||||
|
||||
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: Game) {
|
||||
this.resetAnimationState();
|
||||
if (!this.game) {
|
||||
this.setNewGame(game);
|
||||
} else {
|
||||
this.oldGame = this.game;
|
||||
this.game = game;
|
||||
this.onGameMoveAnimationStart();
|
||||
}
|
||||
}
|
||||
|
||||
onGameMoveAnimationStart() {
|
||||
this.stopCurrentAnimation();
|
||||
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;
|
||||
this.boardViewModel = this.getBoardViewModelFromGame(this.game);
|
||||
this.oldBoardViewModel = this.getBoardViewModelFromGame(this.oldGame);
|
||||
this.startAnimationUpdateCyle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onAnimate() {
|
||||
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.mancalaGame.board.getPitIndexCircularly(gameStep.index);
|
||||
this.animatePit(index, this.oldBoardViewModel, gameStep);
|
||||
this.onBoardViewModelUpdate?.(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
|
||||
) {
|
||||
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
|
||||
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.mancalaGame.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.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.mancalaGame.board.getPitIndexCircularly(index);
|
||||
const pitViewModel = boardViewModel.pits[_index];
|
||||
pitViewModel.pitColor = theme.pitGetRivalStonePitAnimateColor;
|
||||
pitViewModel.stoneCount = 0;
|
||||
}
|
||||
pitViewModel.stoneColor = getColorByBrightness(
|
||||
pitViewModel.pitColor,
|
||||
theme.stoneColor,
|
||||
theme.stoneLightColor
|
||||
);
|
||||
}
|
||||
|
||||
startAnimationUpdateCyle() {
|
||||
this.clearCurrentInterval();
|
||||
|
||||
//@ts-ignore
|
||||
this.currentIntervalID = setInterval(
|
||||
() => this.onAnimate(),
|
||||
animationUpdateInterval
|
||||
);
|
||||
}
|
||||
|
||||
stopCurrentAnimation() {
|
||||
this.clearCurrentInterval();
|
||||
if (this.oldGame) {
|
||||
this.onBoardViewModelUpdate?.(
|
||||
this.getBoardViewModelFromGame(this.oldGame)
|
||||
);
|
||||
}
|
||||
this.resetAnimationState();
|
||||
}
|
||||
|
||||
clearCurrentInterval() {
|
||||
if (this.currentIntervalID) {
|
||||
clearInterval(this.currentIntervalID);
|
||||
}
|
||||
}
|
||||
|
||||
public getBoardViewModelFromGame(game: Game): BoardViewModel {
|
||||
const pitViewModels = this.createPitViewModelsFromGame(game);
|
||||
return BoardViewModelFactory.create(v4(), pitViewModels);
|
||||
}
|
||||
|
||||
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;
|
||||
const pitColor = theme.pitColor;
|
||||
const id = pit.index.toString();
|
||||
return PitViewModelFactory.create({
|
||||
id,
|
||||
stoneCount,
|
||||
stoneColor,
|
||||
pitColor,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private onThemeChange(theme: Theme){
|
||||
if(!this.game) return;
|
||||
this.onBoardViewModelUpdate?.(this.getBoardViewModelFromGame(this.game));
|
||||
}
|
||||
|
||||
public resetAnimationState() {
|
||||
this.animationIndex = -1;
|
||||
this.currentHistoryItem = undefined;
|
||||
this.boardViewModel = undefined;
|
||||
this.oldBoardViewModel = undefined;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.resetAnimationState();
|
||||
this.game = undefined;
|
||||
this.oldGame = undefined;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.context.themeManager.off("themechange", this.onThemeChange.bind(this));
|
||||
this.resetAnimationState();
|
||||
this.clearCurrentInterval();
|
||||
}
|
||||
}
|
||||
35
mobile/src/components/Button.tsx
Normal file
35
mobile/src/components/Button.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import { Context } from "../context/context";
|
||||
import { getColorByBrightness } from "../util/ColorUtil";
|
||||
import { Pressable, View } from "react-native";
|
||||
|
||||
const Button: FunctionComponent<{
|
||||
context: Context;
|
||||
text: String;
|
||||
onClick: () => void;
|
||||
color: string;
|
||||
}> = ({ context, text, color, onClick }) => {
|
||||
const textColor = getColorByBrightness(
|
||||
color,
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
return (
|
||||
<Pressable onPress={onClick}>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
//color: textColor,
|
||||
margin: 5,
|
||||
padding: 10,
|
||||
borderRadius: 30,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
19
mobile/src/components/Center.tsx
Normal file
19
mobile/src/components/Center.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
const Center: FunctionComponent<{children: React.ReactNode}> = ({children}) => {
|
||||
return (
|
||||
<View style={{
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default Center;
|
||||
21
mobile/src/components/CircularPanel.tsx
Normal file
21
mobile/src/components/CircularPanel.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
|
||||
const CircularPanel: FunctionComponent<{
|
||||
color: string;
|
||||
style?: ViewStyle,
|
||||
children: React.ReactNode
|
||||
}> = (props) => {
|
||||
return (
|
||||
<View style={[{backgroundColor: props.color}, props.style, {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 30,
|
||||
}]}>
|
||||
{props.children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default CircularPanel;
|
||||
116
mobile/src/components/InfoPanel.tsx
Normal file
116
mobile/src/components/InfoPanel.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import { Context } from "../context/context";
|
||||
import { Game } from "../models/Game";
|
||||
import { User } from "../models/User";
|
||||
import { getColorByBrightness } from "../util/ColorUtil";
|
||||
import CircularPanel from "./CircularPanel";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text, View, ViewStyle } from "react-native";
|
||||
|
||||
function getInfoPanelTextByGameState(params: {
|
||||
context: Context;
|
||||
game?: Game;
|
||||
currentUser: User;
|
||||
whitePlayer: User;
|
||||
blackPlayer: User;
|
||||
leftPlayer?: User;
|
||||
isSpectator?: boolean;
|
||||
}): string | undefined {
|
||||
const {
|
||||
context,
|
||||
game,
|
||||
currentUser,
|
||||
whitePlayer,
|
||||
blackPlayer,
|
||||
leftPlayer,
|
||||
isSpectator
|
||||
} = params;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (leftPlayer) {
|
||||
return isSpectator ? `${leftPlayer.name} ${t('UserLeftTheGame')}` :
|
||||
leftPlayer.id == currentUser.id ? t('YouLeftTheGame') : t('OpponentLeftTheGame');
|
||||
}
|
||||
|
||||
const isGameEnded = game?.mancalaGame.state == "ended";
|
||||
if (isGameEnded) {
|
||||
const wonPlayerID = game.mancalaGame.getWonPlayerId();
|
||||
let whoWon;
|
||||
if (wonPlayerID) {
|
||||
const wonPlayer = wonPlayerID == whitePlayer.id ? whitePlayer : blackPlayer;
|
||||
whoWon = isSpectator ? `${wonPlayer.name} ${t('Won')}` :
|
||||
game.mancalaGame.getWonPlayerId() === currentUser.id
|
||||
? t('YouWon')
|
||||
: t('YouLost');
|
||||
} else {
|
||||
whoWon = t('GameDraw');
|
||||
}
|
||||
return t('GameEnded') + " " + whoWon;
|
||||
}
|
||||
|
||||
if (game) {
|
||||
const playingPlayer = game.mancalaGame.checkIsPlayerTurn(whitePlayer.id) ? whitePlayer : blackPlayer;
|
||||
return isSpectator ? `${playingPlayer.name} ${t('Playing')}` : game.mancalaGame.checkIsPlayerTurn(currentUser.id)
|
||||
? t('YourTurn')
|
||||
: t('OpponentTurn');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const InfoPanel: FunctionComponent<{
|
||||
context: Context;
|
||||
game?: Game;
|
||||
currentUser: User;
|
||||
whitePlayer: User;
|
||||
blackPlayer: User;
|
||||
leftPlayer?: User;
|
||||
style?: ViewStyle;
|
||||
visible?: boolean;
|
||||
isSpectator?: boolean;
|
||||
}> = ({
|
||||
context,
|
||||
game,
|
||||
currentUser,
|
||||
whitePlayer,
|
||||
blackPlayer,
|
||||
leftPlayer,
|
||||
style,
|
||||
visible,
|
||||
isSpectator
|
||||
}) => {
|
||||
if (visible === false) return <></>;
|
||||
const isUserTurn = currentUser.id ? game?.mancalaGame.checkIsPlayerTurn(currentUser.id) : false;
|
||||
const containerColor = isUserTurn
|
||||
? context.themeManager.theme.playerTurnColor
|
||||
: context.themeManager.theme.boardColor;
|
||||
const textColor = getColorByBrightness(
|
||||
containerColor,
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
const text = getInfoPanelTextByGameState({
|
||||
context,
|
||||
game,
|
||||
currentUser,
|
||||
whitePlayer,
|
||||
blackPlayer,
|
||||
leftPlayer,
|
||||
isSpectator
|
||||
});
|
||||
if (text) {
|
||||
return (
|
||||
<CircularPanel style={style} color={containerColor}>
|
||||
<Text style={{ margin: 0, color: textColor}}>
|
||||
{text}
|
||||
</Text>
|
||||
</CircularPanel>
|
||||
);
|
||||
} else {
|
||||
return (<View></View>)
|
||||
}
|
||||
};
|
||||
|
||||
export default InfoPanel;
|
||||
31
mobile/src/components/LoadingComponent.tsx
Normal file
31
mobile/src/components/LoadingComponent.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { Context } from '../context/context';
|
||||
import { LoadingState } from '../models/LoadingState';
|
||||
import { getColorByBrightness } from '../util/ColorUtil';
|
||||
import CircularPanel from './CircularPanel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
const LoadingComponent: FunctionComponent<{ context: Context, loadingState: LoadingState<any> }> = ({ context, loadingState }) => {
|
||||
const { t } = useTranslation();
|
||||
if (loadingState.isUnset() || loadingState.isLoaded()) {
|
||||
return <></>
|
||||
}
|
||||
if (loadingState.isLoading() || loadingState.isError()) {
|
||||
const textColorOnBoard = getColorByBrightness(
|
||||
context.themeManager.theme.boardColor,
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
const text = loadingState.isLoading() ? t('Loading') +"..." : loadingState.errorMessage;
|
||||
return (
|
||||
<CircularPanel color={context.themeManager.theme.boardColor}>
|
||||
<Text style={{ margin: 0, color: textColorOnBoard }}>{`${text}`}</Text>
|
||||
</CircularPanel>
|
||||
);
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
|
||||
export default LoadingComponent;
|
||||
22
mobile/src/components/PageContainer.tsx
Normal file
22
mobile/src/components/PageContainer.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import { Theme } from "../theme/Theme";
|
||||
import { Context } from "../context/context";
|
||||
import { View } from "react-native";
|
||||
|
||||
const PageContainer: FunctionComponent<{ context: Context, children: React.ReactNode }> = (props) => {
|
||||
return (
|
||||
<View style={{
|
||||
backgroundColor: props.context.themeManager.theme?.background,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
minHeight: 400
|
||||
}}>
|
||||
{props.children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageContainer;
|
||||
18
mobile/src/components/Row.tsx
Normal file
18
mobile/src/components/Row.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
const Row: FunctionComponent<{children: React.ReactNode}> = ({children}) => {
|
||||
return (
|
||||
<View style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent:'space-between'
|
||||
}}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default Row;
|
||||
11
mobile/src/components/Space.tsx
Normal file
11
mobile/src/components/Space.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
const Space: FunctionComponent<{ width?: number, height?: number }> = ({ width, height }) => {
|
||||
return (
|
||||
<View style={{ width, height}}/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Space;
|
||||
80
mobile/src/components/UserStatus.tsx
Normal file
80
mobile/src/components/UserStatus.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { Context } from '../context/context';
|
||||
import { User } from '../models/User';
|
||||
import { getColorByBrightness } from '../util/ColorUtil';
|
||||
import Space from './Space';
|
||||
import { Theme } from '../theme/Theme';
|
||||
import { StyleSheet, Text, View, ViewStyle } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type LayoutMode = "right" | "left";
|
||||
|
||||
const UserStatus: FunctionComponent<{
|
||||
context: Context,
|
||||
user: User,
|
||||
layoutMode: LayoutMode,
|
||||
visible?: boolean,
|
||||
style?: ViewStyle
|
||||
}> = ({ context, user, layoutMode, visible, style }) => {
|
||||
if (visible === false) return <></>;
|
||||
const textColorOnBoard = getColorByBrightness(
|
||||
context.themeManager.theme.background,
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const styles = getDynamicStyles(context.themeManager.theme);
|
||||
return (
|
||||
<View style={[style, layoutMode === "right" ? styles.flexRtl : styles.flexLtr]}>
|
||||
<Text style={[{color: textColorOnBoard}, styles.text]}>{
|
||||
user.isAnonymous ? t("Anonymous") : user.name}
|
||||
</Text>
|
||||
<Space width={5} />
|
||||
<View style={[styles.circle, (user.isOnline? styles.online: styles.offline)]} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function getDynamicStyles(theme: Theme) {
|
||||
return StyleSheet.create({
|
||||
online: {
|
||||
backgroundColor: theme.boardColor
|
||||
},
|
||||
offline: {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
circle: {
|
||||
width: 15,
|
||||
height: 15,
|
||||
minWidth: 15,
|
||||
minHeight: 15,
|
||||
borderRadius: 15,
|
||||
borderWidth: 2,
|
||||
borderColor: theme.boardColor,
|
||||
borderStyle: "solid",
|
||||
},
|
||||
flexRtl: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
alignItems: 'center',
|
||||
},
|
||||
flexLtr: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
fontWeight: 'bold',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
icon: {
|
||||
color : "grey",
|
||||
width: 32,
|
||||
height: 32,
|
||||
fontSize: 32,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default UserStatus;
|
||||
20
mobile/src/components/board/BoardToolbar.tsx
Normal file
20
mobile/src/components/board/BoardToolbar.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import { FunctionComponent } from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
|
||||
const BoardToolbar: FunctionComponent<{ visible?: boolean, style?: ViewStyle, children: React.ReactNode }> = ({ children, visible, style }) => {
|
||||
if (visible === false) return <></>;
|
||||
return (
|
||||
<View style={[style, {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
alignSelf: 'stretch',
|
||||
}]} >
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default BoardToolbar;
|
||||
61
mobile/src/components/board/BoardView.tsx
Normal file
61
mobile/src/components/board/BoardView.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import * as React from "react";
|
||||
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";
|
||||
import { View } from "react-native";
|
||||
|
||||
const BoardView: FunctionComponent<{
|
||||
game: Game;
|
||||
context: Context;
|
||||
boardId: string;
|
||||
boardViewModel: BoardViewModel;
|
||||
revert: boolean;
|
||||
onPitSelect: (index: number, pit: Pit) => void;
|
||||
}> = ({ game, context, boardId, boardViewModel, revert, onPitSelect: onPitSelect }) => {
|
||||
const mancalaGame = game?.mancalaGame;
|
||||
const theme = context.themeManager.theme;
|
||||
|
||||
const createPitView = (key: any, pit: Pit, pitViewModel: PitViewModel) => {
|
||||
return <PitView key={key} pitViewModel={pitViewModel} onClick={() => onPitSelect(pit.index, pit)} />;
|
||||
};
|
||||
const createPitViewList = (pits: Pit[]) => pits.map((pit, index) => createPitView(index, pit, boardViewModel.pits[pit.index]));
|
||||
|
||||
const player1Pits = createPitViewList(mancalaGame?.board.player1Pits);
|
||||
const player2Pits = createPitViewList(mancalaGame?.board.player2Pits);
|
||||
const player1BankIndex = mancalaGame?.board.player1BankIndex();
|
||||
const player2BankIndex = mancalaGame?.board.player2BankIndex();
|
||||
const player1BankViewModel = boardViewModel.pits[player1BankIndex];
|
||||
const player2BankViewModel = boardViewModel.pits[player2BankIndex];
|
||||
return (
|
||||
<View style={{
|
||||
backgroundColor: theme.boardColor,
|
||||
padding: 20,
|
||||
//display: 'grid',
|
||||
//gridTemplateColumns: 'repeat(8, 11vw)',
|
||||
//gridTemplateRows: 'repeat(2, 11vw)',
|
||||
borderRadius: 30
|
||||
}}>
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={revert ? player2BankViewModel : player1BankViewModel}
|
||||
gridColumn="8 / 9"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
<StoreView
|
||||
context={context}
|
||||
pitViewModel={revert ? player1BankViewModel : player2BankViewModel}
|
||||
gridColumn="1 / 2"
|
||||
gridRow="1 / 3"
|
||||
/>
|
||||
{revert ? player1Pits?.reverse() : player2Pits?.reverse()}
|
||||
{revert ? player2Pits : player1Pits}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default BoardView;
|
||||
36
mobile/src/components/board/PitView.tsx
Normal file
36
mobile/src/components/board/PitView.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import Util from "../../util/Util";
|
||||
import PitViewModel from "../../viewmodel/PitViewModel";
|
||||
import StoneView from "./StoneView";
|
||||
import { Pressable, View } from "react-native";
|
||||
|
||||
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 (
|
||||
<Pressable onPress={onClick}>
|
||||
<View style={{
|
||||
backgroundColor: pitViewModel.pitColor,
|
||||
margin: 5,
|
||||
padding: 5,
|
||||
borderRadius: 50,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
//justifyItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
{stones}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default PitView;
|
||||
18
mobile/src/components/board/StoneView.tsx
Normal file
18
mobile/src/components/board/StoneView.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import * as React from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
const StoneView: FunctionComponent<{ color: string }> = ({ color }) => {
|
||||
return (
|
||||
<View style={{
|
||||
backgroundColor: color,
|
||||
margin: 1,
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 10,
|
||||
}}>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default StoneView;
|
||||
54
mobile/src/components/board/StoreView.tsx
Normal file
54
mobile/src/components/board/StoreView.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
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";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
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 (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: pitViewModel.pitColor,
|
||||
//gridColumn: gridColumn,
|
||||
//gridRow: gridRow,
|
||||
margin: 5,
|
||||
borderRadius: 50,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{stones}
|
||||
<Text style={{
|
||||
color: textColor,
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 20,
|
||||
}}>
|
||||
{stones.length}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default StoreView;
|
||||
11
mobile/src/factory/BoardViewModelFactory.ts
Normal file
11
mobile/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
mobile/src/factory/PitViewModelFactory.ts
Normal file
13
mobile/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);
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,314 @@
|
||||
import * as React from 'react';
|
||||
import { View, Button, Text } from 'react-native';
|
||||
import { View, Button, Text, useWindowDimensions } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GameScreenProps } from '../types';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Game, GameUsersConnectionInfo } from '../models/Game';
|
||||
import BoardViewModel from '../viewmodel/BoardViewModel';
|
||||
import { Link } from '@react-navigation/native';
|
||||
import { MancalaGame, Pit } from 'mancala.js';
|
||||
import { v4 } from 'uuid';
|
||||
import PitAnimator from '../animation/PitAnimator';
|
||||
import { channel_on_game_update, channel_on_game_crashed, channel_on_game_user_leave, channel_on_user_connection_change, channel_listen_game_events, channel_unlisten_game_events, channel_leave_game, channel_game_move } from '../const/channel_names';
|
||||
import { GameMove } from '../models/GameMove';
|
||||
import { LoadingState } from '../models/LoadingState';
|
||||
import { getColorByBrightness } from '../util/ColorUtil';
|
||||
import Util from '../util/Util';
|
||||
import Snackbar from 'react-native-snackbar';
|
||||
import PageContainer from '../components/PageContainer';
|
||||
import Center from '../components/Center';
|
||||
import InfoPanel from '../components/InfoPanel';
|
||||
import LoadingComponent from '../components/LoadingComponent';
|
||||
import UserStatus from '../components/UserStatus';
|
||||
import BoardToolbar from '../components/board/BoardToolbar';
|
||||
import BoardView from '../components/board/BoardView';
|
||||
export function GameScreen({ navigation, route }: GameScreenProps) {
|
||||
|
||||
const { context, gameId } = route.params;
|
||||
const { context, gameId, userKey } = route.params;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Text>{gameId}</Text>
|
||||
</View>
|
||||
const [game, setGame] = useState<Game | 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);
|
||||
|
||||
// It is a flag for ongoing action such as send game move.
|
||||
// We have to block future actions if there is an ongoing action.
|
||||
const [hasOngoingAction, setHasOngoingAction] = useState<boolean>(false);
|
||||
|
||||
const [gameUsersConnectionInfo, setGameUsersConnectionInfo] = useState<GameUsersConnectionInfo | undefined>();
|
||||
|
||||
const { height, width } = useWindowDimensions();
|
||||
|
||||
const [gameLoadingState, setLoadingStateGame] = useState<LoadingState<Game>>(LoadingState.Unset());
|
||||
|
||||
const checkIsSpectator = (game: Game) => userKey !== game.mancalaGame.player1Id && userKey !== game.mancalaGame.player2Id;
|
||||
|
||||
const mancalaGame: MancalaGame | undefined = game?.mancalaGame;
|
||||
const isSpectator = game ? checkIsSpectator(game) : undefined;
|
||||
const isPlayer2 = !isSpectator && userKey === mancalaGame?.player2Id;
|
||||
|
||||
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;
|
||||
Snackbar.show({text: t("InternalErrorOccurred")});
|
||||
console.error("on_game_crash");
|
||||
console.error(newCrashMessage);
|
||||
}
|
||||
const onGameUserLeave = (message: any) => {
|
||||
const userKeyWhoLeave = message;
|
||||
setUserKeyWhoLeave(userKeyWhoLeave);
|
||||
setHasOngoingAction(false);
|
||||
};
|
||||
const onUserConnectionChange = (message: any) => {
|
||||
const gameUsersConnectionInfo = message as GameUsersConnectionInfo;
|
||||
setGameUsersConnectionInfo(gameUsersConnectionInfo);
|
||||
};
|
||||
|
||||
const listenMessages = (game: Game, pitAnimator: PitAnimator): () => void => {
|
||||
const _onGameUpdate = (message: object) => onGameUpdateEvent(pitAnimator, message);
|
||||
context.rtmt.addMessageListener(channel_on_game_update, _onGameUpdate);
|
||||
context.rtmt.addMessageListener(channel_on_game_crashed, onGameCrashed);
|
||||
context.rtmt.addMessageListener(channel_on_game_user_leave, onGameUserLeave);
|
||||
context.rtmt.addMessageListener(channel_on_user_connection_change, onUserConnectionChange);
|
||||
checkIsSpectator(game) && userKey && context.rtmt.sendMessage(channel_listen_game_events, game.id);
|
||||
return () => {
|
||||
checkIsSpectator(game) && userKey && context.rtmt.sendMessage(channel_unlisten_game_events, game.id);
|
||||
context.rtmt.removeMessageListener(channel_on_game_update, _onGameUpdate);
|
||||
context.rtmt.removeMessageListener(channel_on_game_crashed, onGameCrashed);
|
||||
context.rtmt.removeMessageListener(channel_on_game_user_leave, onGameUserLeave);
|
||||
context.rtmt.removeMessageListener(channel_on_user_connection_change, onUserConnectionChange);
|
||||
}
|
||||
};
|
||||
|
||||
const updateBoardViewModel = (boardViewModel: BoardViewModel) => {
|
||||
boardViewModel.id = v4();
|
||||
setBoardId(boardViewModel.id);
|
||||
setBoardViewModel(boardViewModel);
|
||||
};
|
||||
|
||||
const getBoardIndex = (index: number) => {
|
||||
if (!game || !mancalaGame) return -1;
|
||||
const pitsLenght = mancalaGame.board.pits.length;
|
||||
if (userKey === mancalaGame.player2Id) return index + pitsLenght / 2;
|
||||
return index;
|
||||
};
|
||||
|
||||
const getOpponentId = () => mancalaGame?.player1Id === userKey ? mancalaGame?.player2Id : mancalaGame?.player1Id;
|
||||
|
||||
const checkHasAnOngoingAction = () => hasOngoingAction;
|
||||
|
||||
const onLeaveGameClick = () => {
|
||||
if (Util.checkConnectionAndMaybeAlert(context, t("ConnectionLost"))) return;
|
||||
|
||||
// TODO
|
||||
//swal({
|
||||
// title: context.texts.AreYouSureToLeaveGame,
|
||||
// icon: "warning",
|
||||
// buttons: [context.texts.Yes, context.texts.Cancel],
|
||||
// dangerMode: true,
|
||||
//})
|
||||
// .then((cancel) => {
|
||||
// if (!cancel) {
|
||||
// context.rtmt.sendMessage(channel_leave_game, {});
|
||||
// }
|
||||
// });
|
||||
};
|
||||
|
||||
const onNewGameClick = () => {
|
||||
if (Util.checkConnectionAndMaybeAlert(context, t("ConnectionLost"))) return;
|
||||
navigation.navigate("Loby", { context })
|
||||
};
|
||||
|
||||
const onPitSelect = (index: number, pit: Pit) => {
|
||||
if (!game || isSpectator || !userKey) {
|
||||
return;
|
||||
}
|
||||
if (userKeyWhoLeave) {
|
||||
Snackbar.show({text: t("GameEnded")});
|
||||
return;
|
||||
}
|
||||
if (game.mancalaGame.state === "ended") {
|
||||
Snackbar.show({text: t("GameEnded")});
|
||||
return;
|
||||
}
|
||||
if (Util.checkConnectionAndMaybeAlert(context, t("ConnectionLost"))) return;
|
||||
if (game.mancalaGame.getPlayerIdByIndex(index) !== userKey) {
|
||||
Snackbar.show({text: t("UCanOnlyPlayYourOwnPits")});
|
||||
return;
|
||||
}
|
||||
const pitIndexForUser = index % (game.mancalaGame.board.totalPitCount() / 2);
|
||||
if (!game.mancalaGame.canPlayerMove(userKey, pitIndexForUser)) {
|
||||
Snackbar.show({text: t("OpponentTurn")});
|
||||
return;
|
||||
}
|
||||
if (checkHasAnOngoingAction()) {
|
||||
Snackbar.show({text: t("UMustWaitUntilCurrentMoveComplete")});
|
||||
return;
|
||||
}
|
||||
if (!boardViewModel) return;
|
||||
//TODO: this check should be in mancala.js
|
||||
if (pit.stoneCount === 0) {
|
||||
Snackbar.show({text: t("UCanNotPlayEmptyPit")});
|
||||
return;
|
||||
}
|
||||
setHasOngoingAction(true);
|
||||
boardViewModel.pits[getBoardIndex(pitIndexForUser)].pitColor =
|
||||
context.themeManager.theme.pitSelectedColor;
|
||||
updateBoardViewModel(boardViewModel);
|
||||
const gameMove: GameMove = { index: pitIndexForUser };
|
||||
context.rtmt.sendMessage(channel_game_move, gameMove);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
let pitAnimator: PitAnimator | undefined;
|
||||
let unlistenMessages: () => void;
|
||||
setLoadingStateGame(LoadingState.Loading())
|
||||
context.gameStore.get(gameId!!).then((game) => {
|
||||
if (game) {
|
||||
pitAnimator = new PitAnimator(context, updateBoardViewModel);
|
||||
setPitAnimator(pitAnimator);
|
||||
onGameUpdate(pitAnimator, game);
|
||||
unlistenMessages = listenMessages(game, pitAnimator);
|
||||
setLoadingStateGame(LoadingState.Loaded({ value: game }))
|
||||
} else {
|
||||
setLoadingStateGame(LoadingState.Error({ errorMessage: t('GameNotFound') }))
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
unlistenMessages?.();
|
||||
pitAnimator?.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const textColorOnAppBar = getColorByBrightness(
|
||||
context.themeManager.theme.appBarBgColor,
|
||||
context.themeManager.theme.textColor,
|
||||
context.themeManager.theme.textLightColor
|
||||
);
|
||||
const isMobile = width < 600;
|
||||
|
||||
const renderNewGameBtn = isSpectator || (userKeyWhoLeave || !game || (game && game.mancalaGame.state == "ended"));
|
||||
const showBoardView = game && boardViewModel && userKey && true;
|
||||
const topLocatedUserId = (isSpectator ? mancalaGame?.player2Id : getOpponentId()) || "0";
|
||||
const bottomLocatedUserId = (isSpectator ? mancalaGame?.player1Id : userKey) || "1";
|
||||
const topLocatedUser = {
|
||||
id: topLocatedUserId,
|
||||
name: "Anonymous",
|
||||
isOnline: isUserOnline(topLocatedUserId),
|
||||
isAnonymous: true
|
||||
};
|
||||
const bottomLocatedUser = {
|
||||
id: bottomLocatedUserId,
|
||||
name: "Anonymous",
|
||||
isOnline: isSpectator ? isUserOnline(bottomLocatedUserId) : context.rtmt.connectionState === "connected",
|
||||
isAnonymous: true
|
||||
};
|
||||
const currentUser = isSpectator ? {
|
||||
id: "2",
|
||||
name: "Anonymous",
|
||||
isOnline: context.rtmt.connectionState === "connected",
|
||||
isAnonymous: true
|
||||
} : bottomLocatedUser;
|
||||
const leftPlayer = userKeyWhoLeave ? (userKeyWhoLeave === topLocatedUser.id ? topLocatedUser : bottomLocatedUser) : undefined;
|
||||
return (
|
||||
<PageContainer context={context}>
|
||||
{/* {renderHeaderBar()} */}
|
||||
{renderMobileBoardToolbar()}
|
||||
{buildBoardTopToolbar()}
|
||||
{showBoardView && (
|
||||
<BoardView
|
||||
game={game}
|
||||
boardId={boardId}
|
||||
boardViewModel={boardViewModel}
|
||||
context={context}
|
||||
onPitSelect={onPitSelect}
|
||||
revert={isPlayer2} />
|
||||
)}
|
||||
<Center>
|
||||
<LoadingComponent context={context} loadingState={gameLoadingState}></LoadingComponent>
|
||||
</Center>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
//function renderHeaderBar() {
|
||||
// return <HeaderBar color={theme?.appBarBgColor}>
|
||||
// <Row>
|
||||
// <Link style={{ textDecoration: 'none' }} to={"/"}>
|
||||
// <HeaderbarIcon />
|
||||
// </Link>
|
||||
// <Link style={{ textDecoration: 'none' }} to={"/"}>
|
||||
// <HeaderbarTitle title={context.texts.Mancala} color={textColorOnAppBar} />
|
||||
// </Link>
|
||||
// </Row>
|
||||
// <Row>
|
||||
// <ThemeSwitchMenu context={context} textColor={textColorOnAppBar} />
|
||||
// <Button
|
||||
// context={context}
|
||||
// color={context.themeManager.theme.pitColor}
|
||||
// text={renderNewGameBtn ? context.texts.NewGame : context.texts.Leave}
|
||||
// onClick={renderNewGameBtn ? onNewGameClick : onLeaveGameClick} />
|
||||
// </Row>
|
||||
// </HeaderBar>;
|
||||
//}
|
||||
|
||||
function renderMobileBoardToolbar() {
|
||||
return <BoardToolbar style={{ justifyContent: "center" }} visible={showBoardView && isMobile || false}>
|
||||
{buildInfoPanel({ visible: isMobile })}
|
||||
</BoardToolbar>;
|
||||
}
|
||||
|
||||
function buildBoardTopToolbar() {
|
||||
return <BoardToolbar style={{ alignItems: "flex-end" }} visible={showBoardView || false}>
|
||||
<UserStatus style={{
|
||||
marginBottom: 15, marginLeft: "6%", maxWidth: isMobile ? 40 : 30,
|
||||
width: isMobile ? 40 : 30
|
||||
}} context={context} layoutMode="left" user={topLocatedUser} visible={showBoardView || false} />
|
||||
{buildInfoPanel({ visible: !isMobile })}
|
||||
<UserStatus style={{
|
||||
marginBottom: 15, marginRight: "6%", maxWidth: isMobile ? 40: 30,
|
||||
width: isMobile ? 40: 30
|
||||
}} context={context} layoutMode="right" user={bottomLocatedUser} visible={showBoardView || false} />
|
||||
</BoardToolbar>;
|
||||
}
|
||||
|
||||
function buildInfoPanel(params: { visible: boolean }) {
|
||||
return (
|
||||
<InfoPanel
|
||||
style={{ marginTop: 10, marginBottom: 10 }}
|
||||
context={context}
|
||||
game={game}
|
||||
currentUser={currentUser}
|
||||
whitePlayer={topLocatedUser}
|
||||
blackPlayer={bottomLocatedUser}
|
||||
leftPlayer={leftPlayer}
|
||||
visible={params.visible}
|
||||
isSpectator={isSpectator} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,9 +11,10 @@ export default function LobyScreen({ navigation, route }: LobyScreenProps) {
|
||||
const { context } = route.params;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onGameStart = (message: Object) => {
|
||||
const onGameStart = async (message: Object) => {
|
||||
const newGame: CommonMancalaGame = message as CommonMancalaGame;
|
||||
navigation.navigate("Game", { context, gameId: newGame.id })
|
||||
const userKey = await context.userKeyStore.getUserKey();
|
||||
navigation.navigate("Game", { context, gameId: newGame.id, userKey })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -4,7 +4,7 @@ import { Context } from '../context/context';
|
||||
export type RootStackParamList = {
|
||||
Home: { context: Context },
|
||||
Loby: { context: Context },
|
||||
Game: { context: Context, gameId: string }
|
||||
Game: { context: Context, gameId: string, userKey: string }
|
||||
};
|
||||
|
||||
export type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;
|
||||
|
||||
32
mobile/src/util/ColorUtil.ts
Normal file
32
mobile/src/util/ColorUtil.ts
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
export function getBrightness(r: number, g: number, b: number) {
|
||||
return (r * 299 + g * 587 + b * 114) / 1000;
|
||||
}
|
||||
|
||||
export function getBrightnessFromHexColor(hexColor: string): number {
|
||||
const [r, g, b] = hexToRgb(hexColor);
|
||||
return getBrightness(r, g, b);
|
||||
}
|
||||
|
||||
// from https://www.codegrepper.com/code-examples/javascript/javascript+convert+color+string+to+rgb
|
||||
export function rgbToHex(r: number, g: number, b: number): string {
|
||||
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||||
}
|
||||
|
||||
// from https://www.codegrepper.com/code-examples/javascript/javascript+convert+color+string+to+rgb
|
||||
export function hexToRgb(
|
||||
hex: string,
|
||||
result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
): number[] {
|
||||
//@ts-ignore
|
||||
return result ? result.map((i) => parseInt(i, 16)).slice(1) : null;
|
||||
//returns [23, 14, 45] -> reformat if needed
|
||||
}
|
||||
|
||||
export function getColorByBrightness(
|
||||
color: string,
|
||||
lightColor: string,
|
||||
darkColor: string
|
||||
): string {
|
||||
return getBrightnessFromHexColor(color) < 125 ? darkColor : lightColor;
|
||||
}
|
||||
34
mobile/src/util/Util.ts
Normal file
34
mobile/src/util/Util.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Context } from "../context/context";
|
||||
import { ConnectionState } from "../rtmt/rtmt";
|
||||
import Snackbar from "react-native-snackbar";
|
||||
|
||||
export default class Util {
|
||||
public static range(size: number) {
|
||||
var ans: number[] = [];
|
||||
for (let i = 0; i < size; i++) {
|
||||
ans.push(i);
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
|
||||
public static checkConnectionAndMaybeAlert(context: Context, message: string): boolean {
|
||||
if (context.rtmt.connectionState !== "connected") {
|
||||
Snackbar.show({text: message});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static getTextByConnectionState(context: Context, connectionState: ConnectionState): string {
|
||||
const { t } = useTranslation();
|
||||
const map: { [key: string]: string } = {
|
||||
connecting: t("Connecting"),
|
||||
connected: t("Connected"),
|
||||
error: t("CannotConnect"),
|
||||
closed: t("ConnectingAgain"),
|
||||
reconnecting: t("ConnectingAgain"),
|
||||
};
|
||||
return map[connectionState];
|
||||
}
|
||||
}
|
||||
10
mobile/src/viewmodel/BoardViewModel.ts
Normal file
10
mobile/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
mobile/src/viewmodel/PitViewModel.ts
Normal file
18
mobile/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;
|
||||
}
|
||||
}
|
||||
@ -2041,6 +2041,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
|
||||
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
|
||||
|
||||
"@types/uuid@^9.0.8":
|
||||
version "9.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
|
||||
integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==
|
||||
|
||||
"@types/yargs-parser@*":
|
||||
version "21.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
|
||||
@ -3420,6 +3425,11 @@ expect@^29.7.0:
|
||||
jest-message-util "^29.7.0"
|
||||
jest-util "^29.7.0"
|
||||
|
||||
fast-base64-decode@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418"
|
||||
integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==
|
||||
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
@ -5644,6 +5654,13 @@ react-is@^17.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-native-get-random-values@^1.11.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.11.0.tgz#1ca70d1271f4b08af92958803b89dccbda78728d"
|
||||
integrity sha512-4BTbDbRmS7iPdhYLRcz3PGFIpFJBwNZg9g42iwa2P6FOv9vZj/xJc678RZXnLNZzd0qd7Q3CCF6Yd+CU2eoXKQ==
|
||||
dependencies:
|
||||
fast-base64-decode "^1.0.0"
|
||||
|
||||
react-native-mmkv@^2.12.2:
|
||||
version "2.12.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz#4bba0f5f04e2cf222494cce3a9794ba6a4894dee"
|
||||
@ -6595,6 +6612,11 @@ utils-merge@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
uuid@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
|
||||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||
|
||||
v8-to-istanbul@^9.0.1:
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user