Compare commits

...

11 commits

38 changed files with 1392 additions and 655 deletions

View file

@ -363,6 +363,14 @@
}, },
"game": { "game": {
"title": "Game", "title": "Game",
"welcomeTitle": "Welcome !",
"welcomeMessage": "Stuck on the toilet? The teacher is late?\nThis game is for you!\n\nTry to get the best score and beat your friends.",
"play": "Play!",
"score": "Score: %{score}",
"highScore": "High score: %{score}",
"newHighScore": "New High Score!",
"time": "Time:",
"level": "Level:",
"pause": "Game Paused", "pause": "Game Paused",
"pauseMessage": "The game is paused", "pauseMessage": "The game is paused",
"resume": "Resume", "resume": "Resume",
@ -379,6 +387,11 @@
"level": "Level: ", "level": "Level: ",
"time": "Time: ", "time": "Time: ",
"exit": "leave Game" "exit": "leave Game"
},
"mascotDialog": {
"title": "Play !",
"message": "Play.",
"button": "Yes !"
} }
}, },
"debug": { "debug": {

View file

@ -361,16 +361,24 @@
"homeButtonSubtitle": "Contacte le développeur de l'appli" "homeButtonSubtitle": "Contacte le développeur de l'appli"
}, },
"game": { "game": {
"title": "Le jeu trop ouf", "title": "Jeu trop ouf",
"welcomeTitle": "Bienvenue !",
"welcomeMessage": "Coincé sur les WC ? Le prof est pas là ?\nCe jeu est fait pour toi !\n\nEssaie d'avoir le meilleur score et de battre tes amis.",
"play": "Jouer !",
"score": "Score : %{score}",
"highScore": "Meilleur score : %{score}",
"newHighScore": "Meilleur score !",
"time": "Temps :",
"level": "Niveau :",
"pause": "Pause", "pause": "Pause",
"pauseMessage": "Le jeu est en pause", "pauseMessage": "T'as fait pause, t'es nul",
"resume": "Continuer", "resume": "Continuer",
"restart": { "restart": {
"text": "Redémarrer", "text": "Redémarrer",
"confirm": "Est-tu sûr de vouloir redémarrer ?", "confirm": "T'es sûr de vouloir redémarrer ?",
"confirmMessage": "Tout ton progrès sera perdu, continuer ?", "confirmMessage": "Tout ton progrès sera perdu, continuer ?",
"confirmYes": "Oui", "confirmYes": "Oui",
"confirmNo": "Non" "confirmNo": "Oula non"
}, },
"gameOver": { "gameOver": {
"text": "Game Over", "text": "Game Over",
@ -378,6 +386,11 @@
"level": "Niveau: ", "level": "Niveau: ",
"time": "Temps: ", "time": "Temps: ",
"exit": "Quitter" "exit": "Quitter"
},
"mascotDialog": {
"title": "Jeu !",
"message": "Jouer.",
"button": "Oui !"
} }
}, },
"debug": { "debug": {

View file

@ -2,6 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper'; import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import i18n from "i18n-js";
type Props = { type Props = {
visible: boolean, visible: boolean,
@ -23,7 +24,7 @@ class AlertDialog extends React.PureComponent<Props> {
<Paragraph>{this.props.message}</Paragraph> <Paragraph>{this.props.message}</Paragraph>
</Dialog.Content> </Dialog.Content>
<Dialog.Actions> <Dialog.Actions>
<Button onPress={this.props.onDismiss}>OK</Button> <Button onPress={this.props.onDismiss}>{i18n.t("dialog.ok")}</Button>
</Dialog.Actions> </Dialog.Actions>
</Dialog> </Dialog>
</Portal> </Portal>

View file

@ -0,0 +1,56 @@
// @flow
import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import {FlatList} from "react-native";
export type OptionsDialogButton = {
title: string,
onPress: () => void,
}
type Props = {
visible: boolean,
title: string,
message: string,
buttons: Array<OptionsDialogButton>,
onDismiss: () => void,
}
class OptionsDialog extends React.PureComponent<Props> {
getButtonRender = ({item}: { item: OptionsDialogButton }) => {
return <Button
onPress={item.onPress}>
{item.title}
</Button>;
}
keyExtractor = (item: OptionsDialogButton) => item.title;
render() {
return (
<Portal>
<Dialog
visible={this.props.visible}
onDismiss={this.props.onDismiss}>
<Dialog.Title>{this.props.title}</Dialog.Title>
<Dialog.Content>
<Paragraph>{this.props.message}</Paragraph>
</Dialog.Content>
<Dialog.Actions>
<FlatList
data={this.props.buttons}
renderItem={this.getButtonRender}
keyExtractor={this.keyExtractor}
horizontal={true}
inverted={true}
/>
</Dialog.Actions>
</Dialog>
</Portal>
);
}
}
export default OptionsDialog;

View file

@ -3,9 +3,10 @@
import * as React from 'react'; import * as React from 'react';
import * as Animatable from "react-native-animatable"; import * as Animatable from "react-native-animatable";
import {Image, TouchableWithoutFeedback, View} from "react-native"; import {Image, TouchableWithoutFeedback, View} from "react-native";
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
type Props = { type Props = {
size: number, style?: ViewStyle,
emotion: number, emotion: number,
animated: boolean, animated: boolean,
entryAnimation: Animatable.AnimatableProperties | null, entryAnimation: Animatable.AnimatableProperties | null,
@ -116,9 +117,10 @@ class Mascot extends React.Component<Props, State> {
if (this.props.onPress == null) { if (this.props.onPress == null) {
this.onPress = (viewRef: AnimatableViewRef) => { this.onPress = (viewRef: AnimatableViewRef) => {
if (viewRef.current != null) { let ref = viewRef.current;
if (ref != null) {
this.setState({currentEmotion: MASCOT_STYLE.LOVE}); this.setState({currentEmotion: MASCOT_STYLE.LOVE});
viewRef.current.rubberBand(1500).then(() => { ref.rubberBand(1500).then(() => {
this.setState({currentEmotion: this.initialEmotion}); this.setState({currentEmotion: this.initialEmotion});
}); });
@ -130,9 +132,10 @@ class Mascot extends React.Component<Props, State> {
if (this.props.onLongPress == null) { if (this.props.onLongPress == null) {
this.onLongPress = (viewRef: AnimatableViewRef) => { this.onLongPress = (viewRef: AnimatableViewRef) => {
if (viewRef.current != null) { let ref = viewRef.current;
if (ref != null) {
this.setState({currentEmotion: MASCOT_STYLE.ANGRY}); this.setState({currentEmotion: MASCOT_STYLE.ANGRY});
viewRef.current.tada(1000).then(() => { ref.tada(1000).then(() => {
this.setState({currentEmotion: this.initialEmotion}); this.setState({currentEmotion: this.initialEmotion});
}); });
@ -153,8 +156,8 @@ class Mascot extends React.Component<Props, State> {
position: "absolute", position: "absolute",
top: "15%", top: "15%",
left: 0, left: 0,
width: this.props.size, width: "100%",
height: this.props.size, height: "100%",
}} }}
/> />
} }
@ -168,8 +171,8 @@ class Mascot extends React.Component<Props, State> {
position: "absolute", position: "absolute",
top: "15%", top: "15%",
left: isRight ? "-11%" : "11%", left: isRight ? "-11%" : "11%",
width: this.props.size, width: "100%",
height: this.props.size, height: "100%",
transform: [{rotateY: rotation}] transform: [{rotateY: rotation}]
}} }}
/> />
@ -181,8 +184,8 @@ class Mascot extends React.Component<Props, State> {
key={"container"} key={"container"}
style={{ style={{
position: "absolute", position: "absolute",
width: this.props.size, width: "100%",
height: this.props.size, height: "100%",
}}/>); }}/>);
if (emotion === MASCOT_STYLE.CUTE) { if (emotion === MASCOT_STYLE.CUTE) {
final.push(this.getEye(EYE_STYLE.CUTE, true)); final.push(this.getEye(EYE_STYLE.CUTE, true));
@ -217,14 +220,13 @@ class Mascot extends React.Component<Props, State> {
} }
render() { render() {
const size = this.props.size;
const entryAnimation = this.props.animated ? this.props.entryAnimation : null; const entryAnimation = this.props.animated ? this.props.entryAnimation : null;
const loopAnimation = this.props.animated ? this.props.loopAnimation : null; const loopAnimation = this.props.animated ? this.props.loopAnimation : null;
return ( return (
<Animatable.View <Animatable.View
style={{ style={{
width: size, aspectRatio: 1,
height: size, ...this.props.style
}} }}
{...entryAnimation} {...entryAnimation}
> >
@ -241,8 +243,8 @@ class Mascot extends React.Component<Props, State> {
<Image <Image
source={MASCOT_IMAGE} source={MASCOT_IMAGE}
style={{ style={{
width: size, width: "100%",
height: size, height:"100%",
}} }}
/> />
{this.getEyes(this.state.currentEmotion)} {this.getEyes(this.state.currentEmotion)}

View file

@ -6,6 +6,7 @@ import Mascot from "./Mascot";
import * as Animatable from "react-native-animatable"; import * as Animatable from "react-native-animatable";
import {BackHandler, Dimensions, ScrollView, TouchableWithoutFeedback, View} from "react-native"; import {BackHandler, Dimensions, ScrollView, TouchableWithoutFeedback, View} from "react-native";
import type {CustomTheme} from "../../managers/ThemeManager"; import type {CustomTheme} from "../../managers/ThemeManager";
import SpeechArrow from "./SpeechArrow";
type Props = { type Props = {
visible: boolean, visible: boolean,
@ -102,19 +103,11 @@ class MascotPopup extends React.Component<Props, State> {
animation={this.props.visible ? "bounceInLeft" : "bounceOutLeft"} animation={this.props.visible ? "bounceInLeft" : "bounceOutLeft"}
duration={this.props.visible ? 1000 : 300} duration={this.props.visible ? 1000 : 300}
> >
<View style={{ <SpeechArrow
marginLeft: this.mascotSize / 3, style={{marginLeft: this.mascotSize / 3}}
width: 0, size={20}
height: 0, color={this.props.theme.colors.mascotMessageArrow}
borderLeftWidth: 0, />
borderRightWidth: 20,
borderBottomWidth: 20,
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: this.props.theme.colors.mascotMessageArrow,
}}/>
<Card style={{ <Card style={{
borderColor: this.props.theme.colors.mascotMessageArrow, borderColor: this.props.theme.colors.mascotMessageArrow,
borderWidth: 4, borderWidth: 4,
@ -160,7 +153,7 @@ class MascotPopup extends React.Component<Props, State> {
duration={this.props.visible ? 1500 : 200} duration={this.props.visible ? 1500 : 200}
> >
<Mascot <Mascot
size={this.mascotSize} style={{width: this.mascotSize}}
animated={true} animated={true}
emotion={this.props.emotion} emotion={this.props.emotion}
/> />
@ -241,15 +234,16 @@ class MascotPopup extends React.Component<Props, State> {
}}> }}>
<View style={{ <View style={{
marginTop: -80, marginTop: -80,
width: "100%"
}}> }}>
{this.getMascot()} {this.getMascot()}
{this.getSpeechBubble()} {this.getSpeechBubble()}
</View> </View>
</View> </View>
</Portal> </Portal>
) )
; ;
} else } else
return null; return null;

View file

@ -0,0 +1,33 @@
// @flow
import * as React from 'react';
import {View} from "react-native";
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
type Props = {
style?: ViewStyle,
size: number,
color: string,
}
export default class SpeechArrow extends React.Component<Props> {
render() {
return (
<View style={this.props.style}>
<View style={{
width: 0,
height: 0,
borderLeftWidth: 0,
borderRightWidth: this.props.size,
borderBottomWidth: this.props.size,
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: this.props.color,
}}/>
</View>
);
}
}

View file

@ -146,20 +146,27 @@ export default class CustomIntroSlider extends React.Component<Props, State> {
</View> </View>
<Animatable.View <Animatable.View
animation={"fadeIn"}> animation={"fadeIn"}>
{index !== 0 && index !== this.introSlides.length -1 {index !== 0 && index !== this.introSlides.length - 1
? <Animatable.View ?
animation={"pulse"} <Mascot
iterationCount={"infinite"}
duration={2000}
style={{ style={{
marginLeft: 30, marginLeft: 30,
marginBottom: 0, marginBottom: 0,
width: 100, width: 100,
marginTop: -30, marginTop: -30,
}}> }}
<Mascot emotion={item.mascotStyle} size={100}/> emotion={item.mascotStyle}
</Animatable.View> : null} animated={true}
entryAnimation={{
animation: "slideInLeft",
duration: 500
}}
loopAnimation={{
animation: "pulse",
iterationCount: "infinite",
duration: 2000,
}}
/> : null}
<View style={{ <View style={{
marginLeft: 50, marginLeft: 50,
width: 0, width: 0,
@ -204,23 +211,23 @@ export default class CustomIntroSlider extends React.Component<Props, State> {
getEndView = () => { getEndView = () => {
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<View <Mascot
style={styles.center}> style={{
<Mascot ...styles.center,
size={250} height: "80%"
emotion={MASCOT_STYLE.COOL} }}
animated={true} emotion={MASCOT_STYLE.COOL}
entryAnimation={{ animated={true}
animation: "slideInDown", entryAnimation={{
duration: 2000, animation: "slideInDown",
}} duration: 2000,
loopAnimation={{ }}
animation: "pulse", loopAnimation={{
duration: 2000, animation: "pulse",
iterationCount: "infinite" duration: 2000,
}} iterationCount: "infinite"
/> }}
</View> />
</View> </View>
); );
} }
@ -228,55 +235,52 @@ export default class CustomIntroSlider extends React.Component<Props, State> {
getWelcomeView = () => { getWelcomeView = () => {
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<View <Mascot
style={styles.center}> style={{
<Mascot ...styles.center,
size={250} height: "80%"
emotion={MASCOT_STYLE.NORMAL} }}
animated={true} emotion={MASCOT_STYLE.NORMAL}
entryAnimation={{ animated={true}
animation: "bounceIn", entryAnimation={{
duration: 2000, animation: "bounceIn",
}} duration: 2000,
/> }}
<Animatable.Text />
useNativeDriver={true} <Animatable.Text
animation={"fadeInUp"} useNativeDriver={true}
duration={500} animation={"fadeInUp"}
duration={500}
style={{ style={{
color: "#fff", color: "#fff",
textAlign: "center", textAlign: "center",
fontSize: 25, fontSize: 25,
}}> }}>
PABLO PABLO
</Animatable.Text> </Animatable.Text>
<Animatable.View <Animatable.View
useNativeDriver={true} useNativeDriver={true}
animation={"fadeInUp"} animation={"fadeInUp"}
duration={500} duration={500}
delay={200} delay={200}
style={{
position: "absolute",
bottom: 30,
right: "20%",
width: 50,
height: 50,
}}>
<MaterialCommunityIcons
style={{ style={{
position: "absolute", ...styles.center,
top: 210, transform: [{rotateZ: "70deg"}],
left: 160, }}
width: 50, name={"undo"}
height: 50, color={'#fff'}
}}> size={40}/>
<MaterialCommunityIcons </Animatable.View>
style={{
marginLeft: "auto",
marginRight: "auto",
marginTop: "auto",
marginBottom: "auto",
transform: [{rotateZ: "70deg"}],
}}
name={"undo"}
color={'#fff'}
size={40}/>
</Animatable.View>
</View>
</View> </View>
) )
} }
@ -403,5 +407,5 @@ const styles = StyleSheet.create({
marginBottom: 'auto', marginBottom: 'auto',
marginRight: 'auto', marginRight: 'auto',
marginLeft: 'auto', marginLeft: 'auto',
} },
}); });

View file

@ -78,7 +78,7 @@ class CustomTabBar extends React.Component<Props, State> {
canPreventDefault: true, canPreventDefault: true,
}); });
if (route.name === "home" && !event.defaultPrevented) if (route.name === "home" && !event.defaultPrevented)
this.props.navigation.navigate('tetris'); this.props.navigation.navigate('game-start');
} }
/** /**

View file

@ -100,6 +100,11 @@ export default class AsyncStorageManager {
default: '1', default: '1',
current: '', current: '',
}, },
gameStartShowBanner: {
key: 'gameStartShowBanner',
default: '1',
current: '',
},
proxiwashWatchedMachines: { proxiwashWatchedMachines: {
key: 'proxiwashWatchedMachines', key: 'proxiwashWatchedMachines',
default: '[]', default: '[]',
@ -131,6 +136,11 @@ export default class AsyncStorageManager {
]), ]),
current: '', current: '',
}, },
gameScores: {
key: 'gameScores',
default: '[]',
current: '',
},
}; };
/** /**

View file

@ -1,14 +1,14 @@
// @flow // @flow
import AsyncStorageManager from "./AsyncStorageManager"; import AsyncStorageManager from "./AsyncStorageManager";
import {DarkTheme, DefaultTheme, Theme} from 'react-native-paper'; import {DarkTheme, DefaultTheme} from 'react-native-paper';
import AprilFoolsManager from "./AprilFoolsManager"; import AprilFoolsManager from "./AprilFoolsManager";
import {Appearance} from 'react-native-appearance'; import {Appearance} from 'react-native-appearance';
const colorScheme = Appearance.getColorScheme(); const colorScheme = Appearance.getColorScheme();
export type CustomTheme = { export type CustomTheme = {
...Theme, ...DefaultTheme,
colors: { colors: {
primary: string, primary: string,
accent: string, accent: string,
@ -56,6 +56,10 @@ export type CustomTheme = {
tetrisJ: string, tetrisJ: string,
tetrisL: string, tetrisL: string,
gameGold: string,
gameSilver: string,
gameBronze: string,
// Mascot Popup // Mascot Popup
mascotMessageArrow: string, mascotMessageArrow: string,
}, },
@ -119,8 +123,7 @@ export default class ThemeManager {
tutorinsaColor: '#f93943', tutorinsaColor: '#f93943',
// Tetris // Tetris
tetrisBackground: '#e6e6e6', tetrisBackground: '#f0f0f0',
tetrisBorder: '#2f2f2f',
tetrisScore: '#e2bd33', tetrisScore: '#e2bd33',
tetrisI: '#3cd9e6', tetrisI: '#3cd9e6',
tetrisO: '#ffdd00', tetrisO: '#ffdd00',
@ -130,6 +133,10 @@ export default class ThemeManager {
tetrisJ: '#2a67e3', tetrisJ: '#2a67e3',
tetrisL: '#da742d', tetrisL: '#da742d',
gameGold: "#ffd610",
gameSilver: "#7b7b7b",
gameBronze: "#a15218",
// Mascot Popup // Mascot Popup
mascotMessageArrow: "#dedede", mascotMessageArrow: "#dedede",
}, },
@ -182,8 +189,7 @@ export default class ThemeManager {
tutorinsaColor: '#f93943', tutorinsaColor: '#f93943',
// Tetris // Tetris
tetrisBackground: '#2c2c2c', tetrisBackground: '#181818',
tetrisBorder: '#1b1b1b',
tetrisScore: '#e2d707', tetrisScore: '#e2d707',
tetrisI: '#30b3be', tetrisI: '#30b3be',
tetrisO: '#c1a700', tetrisO: '#c1a700',
@ -193,6 +199,10 @@ export default class ThemeManager {
tetrisJ: '#0f37b9', tetrisJ: '#0f37b9',
tetrisL: '#b96226', tetrisL: '#b96226',
gameGold: "#ffd610",
gameSilver: "#7b7b7b",
gameBronze: "#a15218",
// Mascot Popup // Mascot Popup
mascotMessageArrow: "#323232", mascotMessageArrow: "#323232",
}, },

View file

@ -8,7 +8,7 @@ import DebugScreen from '../screens/About/DebugScreen';
import {createStackNavigator, TransitionPresets} from "@react-navigation/stack"; import {createStackNavigator, TransitionPresets} from "@react-navigation/stack";
import i18n from "i18n-js"; import i18n from "i18n-js";
import TabNavigator from "./TabNavigator"; import TabNavigator from "./TabNavigator";
import TetrisScreen from "../screens/Tetris/TetrisScreen"; import GameMainScreen from "../screens/Game/screens/GameMainScreen";
import VoteScreen from "../screens/Amicale/VoteScreen"; import VoteScreen from "../screens/Amicale/VoteScreen";
import LoginScreen from "../screens/Amicale/LoginScreen"; import LoginScreen from "../screens/Amicale/LoginScreen";
import {Platform} from "react-native"; import {Platform} from "react-native";
@ -27,6 +27,8 @@ import EquipmentScreen from "../screens/Amicale/Equipment/EquipmentListScreen";
import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen"; import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen";
import EquipmentConfirmScreen from "../screens/Amicale/Equipment/EquipmentConfirmScreen"; import EquipmentConfirmScreen from "../screens/Amicale/Equipment/EquipmentConfirmScreen";
import DashboardEditScreen from "../screens/Other/Settings/DashboardEditScreen"; import DashboardEditScreen from "../screens/Other/Settings/DashboardEditScreen";
import GameStartScreen from "../screens/Game/screens/GameStartScreen";
import GameEndScreen from "../screens/Game/screens/GameEndScreen";
const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS; const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS;
@ -92,8 +94,22 @@ function MainStackComponent(props: { createTabNavigator: () => React.Node }) {
}} }}
/> />
<MainStack.Screen <MainStack.Screen
name="tetris" name="game-start"
component={TetrisScreen} component={GameStartScreen}
options={{
title: i18n.t("screens.game.title"),
}}
/>
<MainStack.Screen
name="game-main"
component={GameMainScreen}
options={{
title: i18n.t("screens.game.title"),
}}
/>
<MainStack.Screen
name="game-end"
component={GameEndScreen}
options={{ options={{
title: i18n.t("screens.game.title"), title: i18n.t("screens.game.title"),
}} }}

View file

@ -117,8 +117,10 @@ function HomeStackComponent(initialRoute: string | null, defaultData: { [key: st
}, },
headerTitle: (props) => <View style={{flexDirection: "row"}}> headerTitle: (props) => <View style={{flexDirection: "row"}}>
<Mascot <Mascot
style={{
width: 50
}}
emotion={MASCOT_STYLE.RANDOM} emotion={MASCOT_STYLE.RANDOM}
size={50}
animated={true} animated={true}
entryAnimation={{ entryAnimation={{
animation: "bounceIn", animation: "bounceIn",

View file

@ -158,15 +158,18 @@ class ProfileScreen extends React.Component<Props, State> {
<Card style={styles.card}> <Card style={styles.card}>
<Card.Title <Card.Title
title={i18n.t("screens.profile.welcomeTitle", {name: this.data.first_name})} title={i18n.t("screens.profile.welcomeTitle", {name: this.data.first_name})}
left={() => <Mascot left={() =>
emotion={MASCOT_STYLE.COOL} <Mascot
size={60} style={{
animated={true} width: 60
entryAnimation={{ }}
animation: "bounceIn", emotion={MASCOT_STYLE.COOL}
duration: 1000 animated={true}
}} entryAnimation={{
/>} animation: "bounceIn",
duration: 1000
}}
/>}
titleStyle={{marginLeft: 10}} titleStyle={{marginLeft: 10}}
/> />
<Card.Content> <Card.Content>

View file

@ -1,10 +1,14 @@
// @flow // @flow
export type coordinates = { import type {CustomTheme} from "../../../managers/ThemeManager";
export type Coordinates = {
x: number, x: number,
y: number, y: number,
} }
type Shape = Array<Array<number>>;
/** /**
* Abstract class used to represent a BaseShape. * Abstract class used to represent a BaseShape.
* Abstract classes do not exist by default in Javascript: we force it by throwing errors in the constructor * Abstract classes do not exist by default in Javascript: we force it by throwing errors in the constructor
@ -12,16 +16,18 @@ export type coordinates = {
*/ */
export default class BaseShape { export default class BaseShape {
#currentShape: Array<Array<number>>; #currentShape: Shape;
#rotation: number; #rotation: number;
position: coordinates; position: Coordinates;
theme: CustomTheme;
/** /**
* Prevent instantiation if classname is BaseShape to force class to be abstract * Prevent instantiation if classname is BaseShape to force class to be abstract
*/ */
constructor() { constructor(theme: CustomTheme) {
if (this.constructor === BaseShape) if (this.constructor === BaseShape)
throw new Error("Abstract class can't be instantiated"); throw new Error("Abstract class can't be instantiated");
this.theme = theme;
this.#rotation = 0; this.#rotation = 0;
this.position = {x: 0, y: 0}; this.position = {x: 0, y: 0};
this.#currentShape = this.getShapes()[this.#rotation]; this.#currentShape = this.getShapes()[this.#rotation];
@ -41,16 +47,14 @@ export default class BaseShape {
* *
* Used by tests to read private fields * Used by tests to read private fields
*/ */
getShapes(): Array<Array<Array<number>>> { getShapes(): Array<Shape> {
throw new Error("Method 'getShapes()' must be implemented"); throw new Error("Method 'getShapes()' must be implemented");
} }
/** /**
* Gets this object's current shape. * Gets this object's current shape.
*
* Used by tests to read private fields
*/ */
getCurrentShape(): Array<Array<number>> { getCurrentShape(): Shape {
return this.#currentShape; return this.#currentShape;
} }
@ -59,9 +63,9 @@ export default class BaseShape {
* This will return an array of coordinates representing the positions of the cells used by this object. * This will return an array of coordinates representing the positions of the cells used by this object.
* *
* @param isAbsolute Should we take into account the current position of the object? * @param isAbsolute Should we take into account the current position of the object?
* @return {Array<coordinates>} This object cells coordinates * @return {Array<Coordinates>} This object cells coordinates
*/ */
getCellsCoordinates(isAbsolute: boolean): Array<coordinates> { getCellsCoordinates(isAbsolute: boolean): Array<Coordinates> {
let coordinates = []; let coordinates = [];
for (let row = 0; row < this.#currentShape.length; row++) { for (let row = 0; row < this.#currentShape.length; row++) {
for (let col = 0; col < this.#currentShape[row].length; col++) { for (let col = 0; col < this.#currentShape[row].length; col++) {

View file

@ -1,19 +1,17 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeI extends BaseShape { export default class ShapeI extends BaseShape {
#colors: Object; constructor(theme: CustomTheme) {
super(theme);
constructor(colors: Object) {
super();
this.position.x = 3; this.position.x = 3;
this.#colors = colors;
} }
getColor(): string { getColor(): string {
return this.#colors.tetrisI; return this.theme.colors.tetrisI;
} }
getShapes() { getShapes() {

View file

@ -1,19 +1,17 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeJ extends BaseShape { export default class ShapeJ extends BaseShape {
#colors: Object; constructor(theme: CustomTheme) {
super(theme);
constructor(colors: Object) {
super();
this.position.x = 3; this.position.x = 3;
this.#colors = colors;
} }
getColor(): string { getColor(): string {
return this.#colors.tetrisJ; return this.theme.colors.tetrisJ;
} }
getShapes() { getShapes() {

View file

@ -1,19 +1,17 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeL extends BaseShape { export default class ShapeL extends BaseShape {
#colors: Object; constructor(theme: CustomTheme) {
super(theme);
constructor(colors: Object) {
super();
this.position.x = 3; this.position.x = 3;
this.#colors = colors;
} }
getColor(): string { getColor(): string {
return this.#colors.tetrisL; return this.theme.colors.tetrisL;
} }
getShapes() { getShapes() {

View file

@ -1,19 +1,17 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeO extends BaseShape { export default class ShapeO extends BaseShape {
#colors: Object; constructor(theme: CustomTheme) {
super(theme);
constructor(colors: Object) {
super();
this.position.x = 4; this.position.x = 4;
this.#colors = colors;
} }
getColor(): string { getColor(): string {
return this.#colors.tetrisO; return this.theme.colors.tetrisO;
} }
getShapes() { getShapes() {

View file

@ -1,19 +1,17 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeS extends BaseShape { export default class ShapeS extends BaseShape {
#colors: Object; constructor(theme: CustomTheme) {
super(theme);
constructor(colors: Object) {
super();
this.position.x = 3; this.position.x = 3;
this.#colors = colors;
} }
getColor(): string { getColor(): string {
return this.#colors.tetrisS; return this.theme.colors.tetrisS;
} }
getShapes() { getShapes() {

View file

@ -1,19 +1,17 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeT extends BaseShape { export default class ShapeT extends BaseShape {
#colors: Object; constructor(theme: CustomTheme) {
super(theme);
constructor(colors: Object) {
super();
this.position.x = 3; this.position.x = 3;
this.#colors = colors;
} }
getColor(): string { getColor(): string {
return this.#colors.tetrisT; return this.theme.colors.tetrisT;
} }
getShapes() { getShapes() {

View file

@ -1,19 +1,17 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from "./BaseShape";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class ShapeZ extends BaseShape { export default class ShapeZ extends BaseShape {
#colors: Object; constructor(theme: CustomTheme) {
super(theme);
constructor(colors: Object) {
super();
this.position.x = 3; this.position.x = 3;
this.#colors = colors;
} }
getColor(): string { getColor(): string {
return this.#colors.tetrisZ; return this.theme.colors.tetrisZ;
} }
getShapes() { getShapes() {

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import GridManager from "../GridManager"; import GridManager from "../logic/GridManager";
import ScoreManager from "../ScoreManager"; import ScoreManager from "../logic/ScoreManager";
import Piece from "../Piece"; import Piece from "../logic/Piece";
let colors = { let colors = {
tetrisBackground: "#000002" tetrisBackground: "#000002"

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import Piece from "../Piece"; import Piece from "../logic/Piece";
import ShapeI from "../Shapes/ShapeI"; import ShapeI from "../Shapes/ShapeI";
let colors = { let colors = {

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import ScoreManager from "../ScoreManager"; import ScoreManager from "../logic/ScoreManager";
test('incrementScore', () => { test('incrementScore', () => {

View file

@ -3,30 +3,26 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import type {CustomTheme} from "../../../managers/ThemeManager";
export type Cell = {color: string, isEmpty: boolean, key: string};
type Props = { type Props = {
item: Object cell: Cell,
theme: CustomTheme,
} }
class Cell extends React.PureComponent<Props> { class CellComponent extends React.PureComponent<Props> {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
render() { render() {
const item = this.props.item; const item = this.props.cell;
return ( return (
<View <View
style={{ style={{
flex: 1, flex: 1,
backgroundColor: item.isEmpty ? 'transparent' : item.color, backgroundColor: item.isEmpty ? 'transparent' : item.color,
borderColor: item.isEmpty ? 'transparent' : this.colors.tetrisBorder, borderColor: 'transparent',
borderStyle: 'solid', borderRadius: 4,
borderRadius: 2,
borderWidth: 1, borderWidth: 1,
aspectRatio: 1, aspectRatio: 1,
}} }}
@ -38,4 +34,4 @@ class Cell extends React.PureComponent<Props> {
} }
export default withTheme(Cell); export default withTheme(CellComponent);

View file

@ -3,35 +3,26 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import Cell from "./Cell"; import type {Cell} from "./CellComponent";
import CellComponent from "./CellComponent";
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
export type Grid = Array<Array<CellComponent>>;
type Props = { type Props = {
navigation: Object,
grid: Array<Array<Object>>, grid: Array<Array<Object>>,
backgroundColor: string,
height: number, height: number,
width: number, width: number,
containerMaxHeight: number | string, style: ViewStyle,
containerMaxWidth: number | string,
} }
class Grid extends React.Component<Props> { class GridComponent extends React.Component<Props> {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
getRow(rowNumber: number) { getRow(rowNumber: number) {
let cells = this.props.grid[rowNumber].map(this.getCellRender); let cells = this.props.grid[rowNumber].map(this.getCellRender);
return ( return (
<View <View
style={{ style={{flexDirection: 'row',}}
flexDirection: 'row',
backgroundColor: this.props.backgroundColor,
}}
key={rowNumber.toString()} key={rowNumber.toString()}
> >
{cells} {cells}
@ -39,8 +30,8 @@ class Grid extends React.Component<Props> {
); );
} }
getCellRender = (item: Object) => { getCellRender = (item: Cell) => {
return <Cell item={item} key={item.key}/>; return <CellComponent cell={item}/>;
}; };
getGrid() { getGrid() {
@ -54,12 +45,9 @@ class Grid extends React.Component<Props> {
render() { render() {
return ( return (
<View style={{ <View style={{
flexDirection: 'column',
maxWidth: this.props.containerMaxWidth,
maxHeight: this.props.containerMaxHeight,
aspectRatio: this.props.width / this.props.height, aspectRatio: this.props.width / this.props.height,
marginLeft: 'auto', borderRadius: 4,
marginRight: 'auto', ...this.props.style
}}> }}>
{this.getGrid()} {this.getGrid()}
</View> </View>
@ -67,4 +55,4 @@ class Grid extends React.Component<Props> {
} }
} }
export default withTheme(Grid); export default withTheme(GridComponent);

View file

@ -0,0 +1,53 @@
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {withTheme} from 'react-native-paper';
import type {Grid} from "./GridComponent";
import GridComponent from "./GridComponent";
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
type Props = {
items: Array<Grid>,
style: ViewStyle
}
class Preview extends React.PureComponent<Props> {
getGrids() {
let grids = [];
for (let i = 0; i < this.props.items.length; i++) {
grids.push(this.getGridRender(this.props.items[i], i));
}
return grids;
}
getGridRender(item: Grid, index: number) {
return <GridComponent
width={item[0].length}
height={item.length}
grid={item}
style={{
marginRight: 5,
marginLeft: 5,
marginBottom: 5,
}}
key={index.toString()}
/>;
};
render() {
if (this.props.items.length > 0) {
return (
<View style={this.props.style}>
{this.getGrids()}
</View>
);
} else
return null;
}
}
export default withTheme(Preview);

View file

@ -3,6 +3,7 @@
import Piece from "./Piece"; import Piece from "./Piece";
import ScoreManager from "./ScoreManager"; import ScoreManager from "./ScoreManager";
import GridManager from "./GridManager"; import GridManager from "./GridManager";
import type {CustomTheme} from "../../../managers/ThemeManager";
export default class GameLogic { export default class GameLogic {
@ -45,20 +46,20 @@ export default class GameLogic {
#onClock: Function; #onClock: Function;
endCallback: Function; endCallback: Function;
#colors: Object; #theme: CustomTheme;
constructor(height: number, width: number, colors: Object) { constructor(height: number, width: number, theme: CustomTheme) {
this.#height = height; this.#height = height;
this.#width = width; this.#width = width;
this.#gameRunning = false; this.#gameRunning = false;
this.#gamePaused = false; this.#gamePaused = false;
this.#colors = colors; this.#theme = theme;
this.#autoRepeatActivationDelay = 300; this.#autoRepeatActivationDelay = 300;
this.#autoRepeatDelay = 50; this.#autoRepeatDelay = 50;
this.#nextPieces = []; this.#nextPieces = [];
this.#nextPiecesCount = 3; this.#nextPiecesCount = 3;
this.#scoreManager = new ScoreManager(); this.#scoreManager = new ScoreManager();
this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#colors); this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#theme);
} }
getHeight(): number { getHeight(): number {
@ -144,7 +145,7 @@ export default class GameLogic {
callback(this.#gridManager.getCurrentGrid()); callback(this.#gridManager.getCurrentGrid());
} }
this.#pressInInterval = setTimeout(() => this.#pressInInterval = setTimeout(() =>
this.movePressedRepeat(false, callback, x, y), this.movePressedRepeat(false, callback, x, y),
isInitial ? this.#autoRepeatActivationDelay : this.#autoRepeatDelay isInitial ? this.#autoRepeatActivationDelay : this.#autoRepeatDelay
); );
} }
@ -165,7 +166,8 @@ export default class GameLogic {
getNextPiecesPreviews() { getNextPiecesPreviews() {
let finalArray = []; let finalArray = [];
for (let i = 0; i < this.#nextPieces.length; i++) { for (let i = 0; i < this.#nextPieces.length; i++) {
finalArray.push(this.#gridManager.getEmptyGrid(4, 4)); const gridSize = this.#nextPieces[i].getCurrentShape().getCurrentShape()[0].length;
finalArray.push(this.#gridManager.getEmptyGrid(gridSize, gridSize));
this.#nextPieces[i].toGrid(finalArray[i], true); this.#nextPieces[i].toGrid(finalArray[i], true);
} }
@ -179,7 +181,7 @@ export default class GameLogic {
generateNextPieces() { generateNextPieces() {
while (this.#nextPieces.length < this.#nextPiecesCount) { while (this.#nextPieces.length < this.#nextPiecesCount) {
this.#nextPieces.push(new Piece(this.#colors)); this.#nextPieces.push(new Piece(this.#theme));
} }
} }
@ -219,7 +221,7 @@ export default class GameLogic {
this.#gameTime = 0; this.#gameTime = 0;
this.#scoreManager = new ScoreManager(); this.#scoreManager = new ScoreManager();
this.#gameTick = GameLogic.levelTicks[this.#scoreManager.getLevel()]; this.#gameTick = GameLogic.levelTicks[this.#scoreManager.getLevel()];
this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#colors); this.#gridManager = new GridManager(this.getWidth(), this.getHeight(), this.#theme);
this.#nextPieces = []; this.#nextPieces = [];
this.generateNextPieces(); this.generateNextPieces();
this.createTetromino(); this.createTetromino();

View file

@ -2,39 +2,37 @@
import Piece from "./Piece"; import Piece from "./Piece";
import ScoreManager from "./ScoreManager"; import ScoreManager from "./ScoreManager";
import type {coordinates} from './Shapes/BaseShape'; import type {Coordinates} from '../Shapes/BaseShape';
import type {Grid} from "../components/GridComponent";
import type {Cell} from "../components/CellComponent";
export type cell = {color: string, isEmpty: boolean, key: string}; import type {CustomTheme} from "../../../managers/ThemeManager";
export type grid = Array<Array<cell>>;
/** /**
* Class used to manage the game grid * Class used to manage the game grid
*
*/ */
export default class GridManager { export default class GridManager {
#currentGrid: grid; #currentGrid: Grid;
#colors: Object; #theme: CustomTheme;
/** /**
* Initializes a grid of the given size * Initializes a grid of the given size
* *
* @param width The grid width * @param width The grid width
* @param height The grid height * @param height The grid height
* @param colors Object containing current theme colors * @param theme Object containing current theme
*/ */
constructor(width: number, height: number, colors: Object) { constructor(width: number, height: number, theme: CustomTheme) {
this.#colors = colors; this.#theme = theme;
this.#currentGrid = this.getEmptyGrid(height, width); this.#currentGrid = this.getEmptyGrid(height, width);
} }
/** /**
* Get the current grid * Get the current grid
* *
* @return {grid} The current grid * @return {Grid} The current grid
*/ */
getCurrentGrid(): grid { getCurrentGrid(): Grid {
return this.#currentGrid; return this.#currentGrid;
} }
@ -42,13 +40,13 @@ export default class GridManager {
* Get a new empty grid line of the given size * Get a new empty grid line of the given size
* *
* @param width The line size * @param width The line size
* @return {Array<cell>} * @return {Array<Cell>}
*/ */
getEmptyLine(width: number): Array<cell> { getEmptyLine(width: number): Array<Cell> {
let line = []; let line = [];
for (let col = 0; col < width; col++) { for (let col = 0; col < width; col++) {
line.push({ line.push({
color: this.#colors.tetrisBackground, color: this.#theme.colors.tetrisBackground,
isEmpty: true, isEmpty: true,
key: col.toString(), key: col.toString(),
}); });
@ -61,9 +59,9 @@ export default class GridManager {
* *
* @param width The grid width * @param width The grid width
* @param height The grid height * @param height The grid height
* @return {grid} A new empty grid * @return {Grid} A new empty grid
*/ */
getEmptyGrid(height: number, width: number): grid { getEmptyGrid(height: number, width: number): Grid {
let grid = []; let grid = [];
for (let row = 0; row < height; row++) { for (let row = 0; row < height; row++) {
grid.push(this.getEmptyLine(width)); grid.push(this.getEmptyLine(width));
@ -91,21 +89,21 @@ export default class GridManager {
* Gets the lines to clear around the given piece's coordinates. * Gets the lines to clear around the given piece's coordinates.
* The piece's coordinates are used for optimization and to prevent checking the whole grid. * The piece's coordinates are used for optimization and to prevent checking the whole grid.
* *
* @param coord The piece's coordinates to check lines at * @param pos The piece's coordinates to check lines at
* @return {Array<number>} An array containing the line numbers to clear * @return {Array<number>} An array containing the line numbers to clear
*/ */
getLinesToClear(coord: Array<coordinates>): Array<number> { getLinesToClear(pos: Array<Coordinates>): Array<number> {
let rows = []; let rows = [];
for (let i = 0; i < coord.length; i++) { for (let i = 0; i < pos.length; i++) {
let isLineFull = true; let isLineFull = true;
for (let col = 0; col < this.#currentGrid[coord[i].y].length; col++) { for (let col = 0; col < this.#currentGrid[pos[i].y].length; col++) {
if (this.#currentGrid[coord[i].y][col].isEmpty) { if (this.#currentGrid[pos[i].y][col].isEmpty) {
isLineFull = false; isLineFull = false;
break; break;
} }
} }
if (isLineFull && rows.indexOf(coord[i].y) === -1) if (isLineFull && rows.indexOf(pos[i].y) === -1)
rows.push(coord[i].y); rows.push(pos[i].y);
} }
return rows; return rows;
} }

View file

@ -1,12 +1,14 @@
import ShapeL from "./Shapes/ShapeL"; import ShapeL from "../Shapes/ShapeL";
import ShapeI from "./Shapes/ShapeI"; import ShapeI from "../Shapes/ShapeI";
import ShapeJ from "./Shapes/ShapeJ"; import ShapeJ from "../Shapes/ShapeJ";
import ShapeO from "./Shapes/ShapeO"; import ShapeO from "../Shapes/ShapeO";
import ShapeS from "./Shapes/ShapeS"; import ShapeS from "../Shapes/ShapeS";
import ShapeT from "./Shapes/ShapeT"; import ShapeT from "../Shapes/ShapeT";
import ShapeZ from "./Shapes/ShapeZ"; import ShapeZ from "../Shapes/ShapeZ";
import type {coordinates} from './Shapes/BaseShape'; import type {Coordinates} from '../Shapes/BaseShape';
import type {grid} from './GridManager'; import BaseShape from "../Shapes/BaseShape";
import type {Grid} from "../components/GridComponent";
import type {CustomTheme} from "../../../managers/ThemeManager";
/** /**
* Class used as an abstraction layer for shapes. * Class used as an abstraction layer for shapes.
@ -24,26 +26,26 @@ export default class Piece {
ShapeT, ShapeT,
ShapeZ, ShapeZ,
]; ];
#currentShape: Object; #currentShape: BaseShape;
#colors: Object; #theme: CustomTheme;
/** /**
* Initializes this piece's color and shape * Initializes this piece's color and shape
* *
* @param colors Object containing current theme colors * @param theme Object containing current theme
*/ */
constructor(colors: Object) { constructor(theme: CustomTheme) {
this.#currentShape = this.getRandomShape(colors); this.#currentShape = this.getRandomShape(theme);
this.#colors = colors; this.#theme = theme;
} }
/** /**
* Gets a random shape object * Gets a random shape object
* *
* @param colors Object containing current theme colors * @param theme Object containing current theme
*/ */
getRandomShape(colors: Object) { getRandomShape(theme: CustomTheme) {
return new this.#shapes[Math.floor(Math.random() * 7)](colors); return new this.#shapes[Math.floor(Math.random() * 7)](theme);
} }
/** /**
@ -51,13 +53,13 @@ export default class Piece {
* *
* @param grid The grid to remove the piece from * @param grid The grid to remove the piece from
*/ */
removeFromGrid(grid: grid) { removeFromGrid(grid: Grid) {
const coord: Array<coordinates> = this.#currentShape.getCellsCoordinates(true); const pos: Array<Coordinates> = this.#currentShape.getCellsCoordinates(true);
for (let i = 0; i < coord.length; i++) { for (let i = 0; i < pos.length; i++) {
grid[coord[i].y][coord[i].x] = { grid[pos[i].y][pos[i].x] = {
color: this.#colors.tetrisBackground, color: this.#theme.colors.tetrisBackground,
isEmpty: true, isEmpty: true,
key: grid[coord[i].y][coord[i].x].key key: grid[pos[i].y][pos[i].x].key
}; };
} }
} }
@ -68,13 +70,13 @@ export default class Piece {
* @param grid The grid to add the piece to * @param grid The grid to add the piece to
* @param isPreview Should we use this piece's current position to determine the cells? * @param isPreview Should we use this piece's current position to determine the cells?
*/ */
toGrid(grid: grid, isPreview: boolean) { toGrid(grid: Grid, isPreview: boolean) {
const coord: Array<coordinates> = this.#currentShape.getCellsCoordinates(!isPreview); const pos: Array<Coordinates> = this.#currentShape.getCellsCoordinates(!isPreview);
for (let i = 0; i < coord.length; i++) { for (let i = 0; i < pos.length; i++) {
grid[coord[i].y][coord[i].x] = { grid[pos[i].y][pos[i].x] = {
color: this.#currentShape.getColor(), color: this.#currentShape.getColor(),
isEmpty: false, isEmpty: false,
key: grid[coord[i].y][coord[i].x].key key: grid[pos[i].y][pos[i].x].key
}; };
} }
} }
@ -87,15 +89,15 @@ export default class Piece {
* @param height The grid's height * @param height The grid's height
* @return {boolean} If the position is valid * @return {boolean} If the position is valid
*/ */
isPositionValid(grid: grid, width: number, height: number) { isPositionValid(grid: Grid, width: number, height: number) {
let isValid = true; let isValid = true;
const coord: Array<coordinates> = this.#currentShape.getCellsCoordinates(true); const pos: Array<Coordinates> = this.#currentShape.getCellsCoordinates(true);
for (let i = 0; i < coord.length; i++) { for (let i = 0; i < pos.length; i++) {
if (coord[i].x >= width if (pos[i].x >= width
|| coord[i].x < 0 || pos[i].x < 0
|| coord[i].y >= height || pos[i].y >= height
|| coord[i].y < 0 || pos[i].y < 0
|| !grid[coord[i].y][coord[i].x].isEmpty) { || !grid[pos[i].y][pos[i].x].isEmpty) {
isValid = false; isValid = false;
break; break;
} }
@ -114,7 +116,7 @@ export default class Piece {
* @param freezeCallback Callback to use if the piece should freeze itself * @param freezeCallback Callback to use if the piece should freeze itself
* @return {boolean} True if the move was valid, false otherwise * @return {boolean} True if the move was valid, false otherwise
*/ */
tryMove(x: number, y: number, grid: grid, width: number, height: number, freezeCallback: Function) { tryMove(x: number, y: number, grid: Grid, width: number, height: number, freezeCallback: () => void) {
if (x > 1) x = 1; // Prevent moving from more than one tile if (x > 1) x = 1; // Prevent moving from more than one tile
if (x < -1) x = -1; if (x < -1) x = -1;
if (y > 1) y = 1; if (y > 1) y = 1;
@ -143,7 +145,7 @@ export default class Piece {
* @param height The grid's height * @param height The grid's height
* @return {boolean} True if the rotation was valid, false otherwise * @return {boolean} True if the rotation was valid, false otherwise
*/ */
tryRotate(grid: grid, width: number, height: number) { tryRotate(grid: Grid, width: number, height: number) {
this.removeFromGrid(grid); this.removeFromGrid(grid);
this.#currentShape.rotate(true); this.#currentShape.rotate(true);
if (!this.isPositionValid(grid, width, height)) { if (!this.isPositionValid(grid, width, height)) {
@ -158,9 +160,13 @@ export default class Piece {
/** /**
* Gets this piece used cells coordinates * Gets this piece used cells coordinates
* *
* @return {Array<coordinates>} An array of coordinates * @return {Array<Coordinates>} An array of coordinates
*/ */
getCoordinates(): Array<coordinates> { getCoordinates(): Array<Coordinates> {
return this.#currentShape.getCellsCoordinates(true); return this.#currentShape.getCellsCoordinates(true);
} }
getCurrentShape() {
return this.#currentShape;
}
} }

View file

@ -0,0 +1,26 @@
// @flow
import * as React from "react";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
import {withTheme} from "react-native-paper";
type Props = {
navigation: StackNavigationProp,
theme: CustomTheme,
}
type State = {
}
class GameEndScreen extends React.Component<Props, State> {
render() {
return (
null
);
}
}
export default withTheme(GameEndScreen);

View file

@ -0,0 +1,424 @@
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {Caption, IconButton, Text, withTheme} from 'react-native-paper';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import GameLogic from "../logic/GameLogic";
import type {Grid} from "../components/GridComponent";
import GridComponent from "../components/GridComponent";
import Preview from "../components/Preview";
import i18n from "i18n-js";
import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
import type {OptionsDialogButton} from "../../../components/Dialogs/OptionsDialog";
import OptionsDialog from "../../../components/Dialogs/OptionsDialog";
type Props = {
navigation: StackNavigationProp,
route: { params: { highScore: number }, ... },
theme: CustomTheme,
}
type State = {
grid: Grid,
gameRunning: boolean,
gameTime: number,
gameScore: number,
gameLevel: number,
dialogVisible: boolean,
dialogTitle: string,
dialogMessage: string,
dialogButtons: Array<OptionsDialogButton>,
onDialogDismiss: () => void,
}
class GameMainScreen extends React.Component<Props, State> {
logic: GameLogic;
highScore: number | null;
constructor(props) {
super(props);
this.logic = new GameLogic(20, 10, this.props.theme);
this.state = {
grid: this.logic.getCurrentGrid(),
gameRunning: false,
gameTime: 0,
gameScore: 0,
gameLevel: 0,
dialogVisible: false,
dialogTitle: "",
dialogMessage: "",
dialogButtons: [],
onDialogDismiss: () => {
},
};
if (this.props.route.params != null)
this.highScore = this.props.route.params.highScore;
}
componentDidMount() {
this.props.navigation.setOptions({
headerRight: this.getRightButton,
});
this.startGame();
}
getRightButton = () => {
return <MaterialHeaderButtons>
<Item title="pause" iconName="pause" onPress={this.togglePause}/>
</MaterialHeaderButtons>;
}
getFormattedTime(seconds: number) {
let date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setSeconds(seconds);
let format;
if (date.getHours())
format = date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();
else if (date.getMinutes())
format = date.getMinutes() + ':' + date.getSeconds();
else
format = date.getSeconds();
return format;
}
onTick = (score: number, level: number, newGrid: Grid) => {
this.setState({
gameScore: score,
gameLevel: level,
grid: newGrid,
});
}
onClock = (time: number) => {
this.setState({
gameTime: time,
});
}
updateGrid = (newGrid: Grid) => {
this.setState({
grid: newGrid,
});
}
updateGridScore = (newGrid: Grid, score: number) => {
this.setState({
grid: newGrid,
gameScore: score,
});
}
togglePause = () => {
this.logic.togglePause();
if (this.logic.isGamePaused())
this.showPausePopup();
}
onDialogDismiss = () => this.setState({dialogVisible: false});
showPausePopup = () => {
const onDismiss = () => {
this.togglePause();
this.onDialogDismiss();
};
this.setState({
dialogVisible: true,
dialogTitle: i18n.t("screens.game.pause"),
dialogMessage: i18n.t("screens.game.pauseMessage"),
dialogButtons: [
{
title: i18n.t("screens.game.restart.text"),
onPress: this.showRestartConfirm
},
{
title: i18n.t("screens.game.resume"),
onPress: onDismiss
}
],
onDialogDismiss: onDismiss,
});
}
showRestartConfirm = () => {
this.setState({
dialogVisible: true,
dialogTitle: i18n.t("screens.game.restart.confirm"),
dialogMessage: i18n.t("screens.game.restart.confirmMessage"),
dialogButtons: [
{
title: i18n.t("screens.game.restart.confirmYes"),
onPress: () => {
this.onDialogDismiss();
this.startGame();
}
},
{
title: i18n.t("screens.game.restart.confirmNo"),
onPress: this.showPausePopup
}
],
onDialogDismiss: this.showPausePopup,
});
}
showGameOverConfirm() {
let message = i18n.t("screens.game.gameOver.score") + this.state.gameScore + '\n';
message += i18n.t("screens.game.gameOver.level") + this.state.gameLevel + '\n';
message += i18n.t("screens.game.gameOver.time") + this.getFormattedTime(this.state.gameTime) + '\n';
const onDismiss = () => {
this.onDialogDismiss();
this.startGame();
};
this.setState({
dialogVisible: true,
dialogTitle: i18n.t("screens.game.gameOver.text"),
dialogMessage: message,
dialogButtons: [
{
title: i18n.t("screens.game.gameOver.exit"),
onPress: () => this.props.navigation.goBack()
},
{
title: i18n.t("screens.game.resume"),
onPress: onDismiss
}
],
onDialogDismiss: onDismiss,
});
}
startGame = () => {
this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
this.setState({
gameRunning: true,
});
}
onGameEnd = (time: number, score: number, isRestart: boolean) => {
this.setState({
gameTime: time,
gameScore: score,
gameRunning: false,
});
if (!isRestart)
this.props.navigation.replace(
"game-start",
{
score: this.state.gameScore,
level: this.state.gameLevel,
time: this.state.gameTime,
}
);
}
getStatusIcons() {
return (
<View style={{
flex: 1,
marginTop: "auto",
marginBottom: "auto"
}}>
<View style={{
marginLeft: 'auto',
marginRight: 'auto',
}}>
<Caption style={{
marginLeft: "auto",
marginRight: "auto",
marginBottom: 5,
}}>{i18n.t("screens.game.time")}</Caption>
<View style={{
flexDirection: "row"
}}>
<MaterialCommunityIcons
name={'timer'}
color={this.props.theme.colors.subtitle}
size={20}/>
<Text style={{
marginLeft: 5,
color: this.props.theme.colors.subtitle
}}>{this.getFormattedTime(this.state.gameTime)}</Text>
</View>
</View>
<View style={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 20,
}}>
<Caption style={{
marginLeft: "auto",
marginRight: "auto",
marginBottom: 5,
}}>{i18n.t("screens.game.level")}</Caption>
<View style={{
flexDirection: "row"
}}>
<MaterialCommunityIcons
name={'gamepad-square'}
color={this.props.theme.colors.text}
size={20}/>
<Text style={{
marginLeft: 5
}}>{this.state.gameLevel}</Text>
</View>
</View>
</View>
);
}
getScoreIcon() {
let highScore = this.highScore == null || this.state.gameScore > this.highScore
? this.state.gameScore
: this.highScore;
return (
<View style={{
marginTop: 10,
marginBottom: 10,
}}>
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
}}>
<Text style={{
marginLeft: 5,
fontSize: 20,
}}>{i18n.t("screens.game.score", {score: this.state.gameScore})}</Text>
<MaterialCommunityIcons
name={'star'}
color={this.props.theme.colors.tetrisScore}
size={20}
style={{
marginTop: "auto",
marginBottom: "auto",
marginLeft: 5
}}/>
</View>
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
marginTop: 5,
}}>
<Text style={{
marginLeft: 5,
fontSize: 10,
color: this.props.theme.colors.textDisabled
}}>{i18n.t("screens.game.highScore", {score: highScore})}</Text>
<MaterialCommunityIcons
name={'star'}
color={this.props.theme.colors.tetrisScore}
size={10}
style={{
marginTop: "auto",
marginBottom: "auto",
marginLeft: 5
}}/>
</View>
</View>
);
}
getControlButtons() {
return (
<View style={{
height: 80,
flexDirection: "row"
}}>
<IconButton
icon="rotate-right-variant"
size={40}
onPress={() => this.logic.rotatePressed(this.updateGrid)}
style={{flex: 1}}
/>
<View style={{
flexDirection: 'row',
flex: 4
}}>
<IconButton
icon="chevron-left"
size={40}
style={{flex: 1}}
onPress={() => this.logic.pressedOut()}
onPressIn={() => this.logic.leftPressedIn(this.updateGrid)}
/>
<IconButton
icon="chevron-right"
size={40}
style={{flex: 1}}
onPress={() => this.logic.pressedOut()}
onPressIn={() => this.logic.rightPressed(this.updateGrid)}
/>
</View>
<IconButton
icon="arrow-down-bold"
size={40}
onPressIn={() => this.logic.downPressedIn(this.updateGridScore)}
onPress={() => this.logic.pressedOut()}
style={{flex: 1}}
color={this.props.theme.colors.tetrisScore}
/>
</View>
);
}
render() {
return (
<View style={{flex: 1}}>
<View style={{
flex: 1,
flexDirection: "row",
}}>
{this.getStatusIcons()}
<View style={{flex: 4}}>
{this.getScoreIcon()}
<GridComponent
width={this.logic.getWidth()}
height={this.logic.getHeight()}
grid={this.state.grid}
style={{
backgroundColor: this.props.theme.colors.tetrisBackground,
flex: 1,
marginLeft: "auto",
marginRight: "auto",
}}
/>
</View>
<View style={{flex: 1}}>
<Preview
items={this.logic.getNextPiecesPreviews()}
style={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
}}
/>
</View>
</View>
{this.getControlButtons()}
<OptionsDialog
visible={this.state.dialogVisible}
title={this.state.dialogTitle}
message={this.state.dialogMessage}
buttons={this.state.dialogButtons}
onDismiss={this.state.onDialogDismiss}
/>
</View>
);
}
}
export default withTheme(GameMainScreen);

View file

@ -0,0 +1,453 @@
// @flow
import * as React from "react";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
import {Button, Card, Divider, Headline, Paragraph, Text, withTheme} from "react-native-paper";
import {ScrollView, View} from "react-native";
import i18n from "i18n-js";
import Mascot, {MASCOT_STYLE} from "../../../components/Mascot/Mascot";
import MascotPopup from "../../../components/Mascot/MascotPopup";
import AsyncStorageManager from "../../../managers/AsyncStorageManager";
import type {Grid} from "../components/GridComponent";
import GridComponent from "../components/GridComponent";
import GridManager from "../logic/GridManager";
import Piece from "../logic/Piece";
import * as Animatable from "react-native-animatable";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import LinearGradient from "react-native-linear-gradient";
import SpeechArrow from "../../../components/Mascot/SpeechArrow";
type GameStats = {
score: number,
level: number,
time: number,
}
type Props = {
navigation: StackNavigationProp,
route: {
params: GameStats
},
theme: CustomTheme,
}
type State = {
mascotDialogVisible: boolean,
}
class GameStartScreen extends React.Component<Props, State> {
gridManager: GridManager;
scores: Array<number>;
gameStats: GameStats | null;
isHighScore: boolean;
state = {
mascotDialogVisible: AsyncStorageManager.getInstance().preferences.gameStartShowBanner.current === "1",
}
constructor(props: Props) {
super(props);
this.gridManager = new GridManager(4, 4, props.theme);
this.scores = JSON.parse(AsyncStorageManager.getInstance().preferences.gameScores.current);
this.scores.sort((a, b) => b - a);
if (this.props.route.params != null)
this.recoverGameScore();
}
recoverGameScore() {
this.gameStats = this.props.route.params;
this.isHighScore = this.scores.length === 0 || this.gameStats.score > this.scores[0];
for (let i = 0; i < 3; i++) {
if (this.scores.length > i && this.gameStats.score > this.scores[i]) {
this.scores.splice(i, 0, this.gameStats.score);
break;
} else if (this.scores.length <= i) {
this.scores.push(this.gameStats.score);
break;
}
}
if (this.scores.length > 3)
this.scores.splice(3, 1);
AsyncStorageManager.getInstance().savePref(
AsyncStorageManager.getInstance().preferences.gameScores.key,
JSON.stringify(this.scores)
);
}
hideMascotDialog = () => {
AsyncStorageManager.getInstance().savePref(
AsyncStorageManager.getInstance().preferences.gameStartShowBanner.key,
'0'
);
this.setState({mascotDialogVisible: false})
};
getPiecesBackground() {
let gridList = [];
for (let i = 0; i < 18; i++) {
gridList.push(this.gridManager.getEmptyGrid(4, 4));
const piece = new Piece(this.props.theme);
piece.toGrid(gridList[i], true);
}
return (
<View style={{
position: "absolute",
width: "100%",
height: "100%",
}}>
{gridList.map((item: Grid, index: number) => {
const size = 10 + Math.floor(Math.random() * 30);
const top = Math.floor(Math.random() * 100);
const rot = Math.floor(Math.random() * 360);
const left = (index % 6) * 20;
const animDelay = size * 20;
const animDuration = 2 * (2000 - (size * 30));
return (
<Animatable.View
animation={"fadeInDownBig"}
delay={animDelay}
duration={animDuration}
key={index.toString()}
style={{
width: size + "%",
position: "absolute",
top: top + "%",
left: left + "%",
}}
>
<View style={{
transform: [{rotateZ: rot + "deg"}],
}}>
<GridComponent
width={4}
height={4}
grid={item}
style={{
marginRight: 5,
marginLeft: 5,
marginBottom: 5,
}}
/>
</View>
</Animatable.View>
);
})}
</View>
);
}
getPostGameContent(stats: GameStats) {
return (
<View style={{
flex: 1
}}>
<Mascot
emotion={this.isHighScore ? MASCOT_STYLE.LOVE : MASCOT_STYLE.NORMAL}
animated={this.isHighScore}
style={{
width: this.isHighScore ? "50%" : "30%",
marginLeft: this.isHighScore ? "auto" : null,
marginRight: this.isHighScore ? "auto" : null,
}}/>
<SpeechArrow
style={{marginLeft: this.isHighScore ? "60%" : "20%"}}
size={20}
color={this.props.theme.colors.mascotMessageArrow}
/>
<Card style={{
borderColor: this.props.theme.colors.mascotMessageArrow,
borderWidth: 2,
marginLeft: 20,
marginRight: 20,
}}>
<Card.Content>
<Headline
style={{
textAlign: "center",
color: this.isHighScore
? this.props.theme.colors.gameGold
: this.props.theme.colors.primary
}}>
{this.isHighScore
? i18n.t("screens.game.newHighScore")
: i18n.t("screens.game.gameOver.text")}
</Headline>
<Divider/>
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
marginTop: 10,
marginBottom: 10,
}}>
<Text style={{
fontSize: 20,
}}>
{i18n.t("screens.game.score", {score: stats.score})}
</Text>
<MaterialCommunityIcons
name={'star'}
color={this.props.theme.colors.tetrisScore}
size={30}
style={{
marginLeft: 5
}}/>
</View>
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
}}>
<Text>{i18n.t("screens.game.level")}</Text>
<MaterialCommunityIcons
style={{
marginRight: 5,
marginLeft: 5,
}}
name={"gamepad-square"}
size={20}
color={this.props.theme.colors.textDisabled}
/>
<Text>
{stats.level}
</Text>
</View>
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
}}>
<Text>{i18n.t("screens.game.time")}</Text>
<MaterialCommunityIcons
style={{
marginRight: 5,
marginLeft: 5,
}}
name={"timer"}
size={20}
color={this.props.theme.colors.textDisabled}
/>
<Text>
{stats.time}
</Text>
</View>
</Card.Content>
</Card>
</View>
)
}
getWelcomeText() {
return (
<View>
<Mascot emotion={MASCOT_STYLE.COOL} style={{
width: "40%",
marginLeft: "auto",
marginRight: "auto",
}}/>
<SpeechArrow
style={{marginLeft: "60%"}}
size={20}
color={this.props.theme.colors.mascotMessageArrow}
/>
<Card style={{
borderColor: this.props.theme.colors.mascotMessageArrow,
borderWidth: 2,
marginLeft: 10,
marginRight: 10,
}}>
<Card.Content>
<Headline
style={{
textAlign: "center",
color: this.props.theme.colors.primary
}}>
{i18n.t("screens.game.welcomeTitle")}
</Headline>
<Divider/>
<Paragraph
style={{
textAlign: "center",
marginTop: 10,
}}>
{i18n.t("screens.game.welcomeMessage")}
</Paragraph>
</Card.Content>
</Card>
</View>
);
}
getPodiumRender(place: 1 | 2 | 3, score: string) {
let icon = "podium-gold";
let color = this.props.theme.colors.gameGold;
let fontSize = 20;
let size = 70;
if (place === 2) {
icon = "podium-silver";
color = this.props.theme.colors.gameSilver;
fontSize = 18;
size = 60;
} else if (place === 3) {
icon = "podium-bronze";
color = this.props.theme.colors.gameBronze;
fontSize = 15;
size = 50;
}
return (
<View style={{
marginLeft: place === 2 ? 20 : "auto",
marginRight: place === 3 ? 20 : "auto",
flexDirection: "column",
alignItems: "center",
justifyContent: "flex-end",
}}>
{
this.isHighScore && place === 1
?
<Animatable.View
animation={"swing"}
iterationCount={"infinite"}
duration={2000}
delay={1000}
useNativeDriver={true}
style={{
position: "absolute",
top: -20
}}
>
<Animatable.View
animation={"pulse"}
iterationCount={"infinite"}
useNativeDriver={true}
>
<MaterialCommunityIcons
name={"decagram"}
color={this.props.theme.colors.gameGold}
size={150}
/>
</Animatable.View>
</Animatable.View>
: null
}
<MaterialCommunityIcons
name={icon}
color={this.isHighScore && place === 1 ? "#fff" : color}
size={size}
/>
<Text style={{
textAlign: "center",
fontWeight: place === 1 ? "bold" : null,
fontSize: fontSize,
}}>{score}</Text>
</View>
);
}
getTopScoresRender() {
const gold = this.scores.length > 0
? this.scores[0]
: "-";
const silver = this.scores.length > 1
? this.scores[1]
: "-";
const bronze = this.scores.length > 2
? this.scores[2]
: "-";
return (
<View style={{
marginBottom: 20,
marginTop: 20
}}>
{this.getPodiumRender(1, gold.toString())}
<View style={{
flexDirection: "row",
marginLeft: "auto",
marginRight: "auto",
}}>
{this.getPodiumRender(3, bronze.toString())}
{this.getPodiumRender(2, silver.toString())}
</View>
</View>
);
}
getMainContent() {
return (
<LinearGradient
style={{flex: 1}}
colors={[
this.props.theme.colors.background + "00",
this.props.theme.colors.background
]}
start={{x: 0, y: 0.1}}
end={{x: 0.1, y: 1}}
>
<View style={{flex: 1}}>
{
this.gameStats != null
? this.getPostGameContent(this.gameStats)
: this.getWelcomeText()
}
<Button
icon={"play"}
mode={"contained"}
onPress={() => this.props.navigation.replace(
"game-main",
{
highScore: this.scores.length > 0
? this.scores[0]
: null
}
)}
style={{
marginLeft: "auto",
marginRight: "auto",
marginTop: 10,
}}
>
{i18n.t("screens.game.play")}
</Button>
{this.getTopScoresRender()}
</View>
</LinearGradient>
)
}
keyExtractor = (item: number) => item.toString();
render() {
return (
<View style={{flex: 1}}>
{this.getPiecesBackground()}
<ScrollView>
{this.getMainContent()}
<MascotPopup
visible={this.state.mascotDialogVisible}
title={i18n.t("screens.game.mascotDialog.title")}
message={i18n.t("screens.game.mascotDialog.message")}
icon={"gamepad-variant"}
buttons={{
action: null,
cancel: {
message: i18n.t("screens.game.mascotDialog.button"),
icon: "check",
onPress: this.hideMascotDialog,
}
}}
emotion={MASCOT_STYLE.COOL}
/>
</ScrollView>
</View>
);
}
}
export default withTheme(GameStartScreen);

View file

@ -1,299 +0,0 @@
// @flow
import * as React from 'react';
import {Alert, View} from 'react-native';
import {IconButton, Text, withTheme} from 'react-native-paper';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import GameLogic from "./GameLogic";
import Grid from "./components/Grid";
import Preview from "./components/Preview";
import i18n from "i18n-js";
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
type Props = {
navigation: Object,
}
type State = {
grid: Array<Array<Object>>,
gameRunning: boolean,
gameTime: number,
gameScore: number,
gameLevel: number,
}
class TetrisScreen extends React.Component<Props, State> {
colors: Object;
logic: GameLogic;
onTick: Function;
onClock: Function;
onGameEnd: Function;
updateGrid: Function;
updateGridScore: Function;
constructor(props) {
super(props);
this.colors = props.theme.colors;
this.logic = new GameLogic(20, 10, this.colors);
this.state = {
grid: this.logic.getCurrentGrid(),
gameRunning: false,
gameTime: 0,
gameScore: 0,
gameLevel: 0,
};
this.onTick = this.onTick.bind(this);
this.onClock = this.onClock.bind(this);
this.onGameEnd = this.onGameEnd.bind(this);
this.updateGrid = this.updateGrid.bind(this);
this.updateGridScore = this.updateGridScore.bind(this);
this.props.navigation.addListener('blur', this.onScreenBlur.bind(this));
this.props.navigation.addListener('focus', this.onScreenFocus.bind(this));
}
componentDidMount() {
const rightButton = this.getRightButton.bind(this);
this.props.navigation.setOptions({
headerRight: rightButton,
});
this.startGame();
}
getRightButton() {
return <MaterialHeaderButtons>
<Item title="pause" iconName="pause" onPress={() => this.togglePause()}/>
</MaterialHeaderButtons>;
}
/**
* Remove any interval on un-focus
*/
onScreenBlur() {
if (!this.logic.isGamePaused())
this.logic.togglePause();
}
onScreenFocus() {
if (!this.logic.isGameRunning())
this.startGame();
else if (this.logic.isGamePaused())
this.showPausePopup();
}
getFormattedTime(seconds: number) {
let date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setSeconds(seconds);
let format;
if (date.getHours())
format = date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds();
else if (date.getMinutes())
format = date.getMinutes() + ':' + date.getSeconds();
else
format = date.getSeconds();
return format;
}
onTick(score: number, level: number, newGrid: Array<Array<Object>>) {
this.setState({
gameScore: score,
gameLevel: level,
grid: newGrid,
});
}
onClock(time: number) {
this.setState({
gameTime: time,
});
}
updateGrid(newGrid: Array<Array<Object>>) {
this.setState({
grid: newGrid,
});
}
updateGridScore(newGrid: Array<Array<Object>>, score: number) {
this.setState({
grid: newGrid,
gameScore: score,
});
}
togglePause() {
this.logic.togglePause();
if (this.logic.isGamePaused())
this.showPausePopup();
}
showPausePopup() {
Alert.alert(
i18n.t("screens.game.pause"),
i18n.t("screens.game.pauseMessage"),
[
{text: i18n.t("screens.game.restart.text"), onPress: () => this.showRestartConfirm()},
{text: i18n.t("screens.game.resume"), onPress: () => this.togglePause()},
],
{cancelable: false},
);
}
showRestartConfirm() {
Alert.alert(
i18n.t("screens.game.restart.confirm"),
i18n.t("screens.game.restart.confirmMessage"),
[
{text: i18n.t("screens.game.restart.confirmNo"), onPress: () => this.showPausePopup()},
{text: i18n.t("screens.game.restart.confirmYes"), onPress: () => this.startGame()},
],
{cancelable: false},
);
}
showGameOverConfirm() {
let message = i18n.t("screens.game.gameOver.score") + this.state.gameScore + '\n';
message += i18n.t("screens.game.gameOver.level") + this.state.gameLevel + '\n';
message += i18n.t("screens.game.gameOver.time") + this.getFormattedTime(this.state.gameTime) + '\n';
Alert.alert(
i18n.t("screens.game.gameOver.text"),
message,
[
{text: i18n.t("screens.game.gameOver.exit"), onPress: () => this.props.navigation.goBack()},
{text: i18n.t("screens.game.restart.text"), onPress: () => this.startGame()},
],
{cancelable: false},
);
}
startGame() {
this.logic.startGame(this.onTick, this.onClock, this.onGameEnd);
this.setState({
gameRunning: true,
});
}
onGameEnd(time: number, score: number, isRestart: boolean) {
this.setState({
gameTime: time,
gameScore: score,
gameRunning: false,
});
if (!isRestart)
this.showGameOverConfirm();
}
render() {
return (
<View style={{
width: '100%',
height: '100%',
}}>
<View style={{
flexDirection: 'row',
position: 'absolute',
top: 5,
left: 10,
}}>
<MaterialCommunityIcons
name={'timer'}
color={this.colors.subtitle}
size={20}/>
<Text style={{
marginLeft: 5,
color: this.colors.subtitle
}}>{this.getFormattedTime(this.state.gameTime)}</Text>
</View>
<View style={{
flexDirection: 'row',
position: 'absolute',
top: 50,
left: 10,
}}>
<MaterialCommunityIcons
name={'gamepad'}
color={this.colors.text}
size={20}/>
<Text style={{
marginLeft: 5
}}>{this.state.gameLevel}</Text>
</View>
<View style={{
flexDirection: 'row',
marginRight: 'auto',
marginLeft: 'auto',
}}>
<MaterialCommunityIcons
name={'star'}
color={this.colors.tetrisScore}
size={30}/>
<Text style={{
marginLeft: 5,
fontSize: 22,
}}>{this.state.gameScore}</Text>
</View>
<Grid
width={this.logic.getWidth()}
height={this.logic.getHeight()}
containerMaxHeight={'80%'}
containerMaxWidth={'60%'}
grid={this.state.grid}
backgroundColor={this.colors.tetrisBackground}
/>
<View style={{
position: 'absolute',
top: 50,
right: 5,
}}>
<Preview
next={this.logic.getNextPiecesPreviews()}
/>
</View>
<View style={{
position: 'absolute',
bottom: 0,
flexDirection: 'row',
width: '100%',
}}>
<IconButton
icon="rotate-right-variant"
size={40}
onPress={() => this.logic.rotatePressed(this.updateGrid)}
style={{marginRight: 'auto'}}
/>
<View style={{
flexDirection: 'row',
}}>
<IconButton
icon="arrow-left"
size={40}
onPress={() => this.logic.pressedOut()}
onPressIn={() => this.logic.leftPressedIn(this.updateGrid)}
/>
<IconButton
icon="arrow-right"
size={40}
onPress={() => this.logic.pressedOut()}
onPressIn={() => this.logic.rightPressed(this.updateGrid)}
/>
</View>
<IconButton
icon="arrow-down"
size={40}
onPressIn={() => this.logic.downPressedIn(this.updateGridScore)}
onPress={() => this.logic.pressedOut()}
style={{marginLeft: 'auto'}}
color={this.colors.tetrisScore}
/>
</View>
</View>
);
}
}
export default withTheme(TetrisScreen);

View file

@ -1,57 +0,0 @@
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {withTheme} from 'react-native-paper';
import Grid from "./Grid";
type Props = {
next: Object,
}
class Preview extends React.PureComponent<Props> {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
getGrids() {
let grids = [];
for (let i = 0; i < this.props.next.length; i++) {
grids.push(
this.getGridRender(this.props.next[i], i)
);
}
return grids;
}
getGridRender(item: Object, index: number) {
return <Grid
width={item[0].length}
height={item.length}
grid={item}
containerMaxHeight={50}
containerMaxWidth={50}
backgroundColor={'transparent'}
key={index.toString()}
/>;
};
render() {
if (this.props.next.length > 0) {
return (
<View>
{this.getGrids()}
</View>
);
} else
return null;
}
}
export default withTheme(Preview);