Browse Source

Added basic tetris functionality

Arnaud Vergnet 4 years ago
parent
commit
3aaf56a660

+ 10
- 1
components/Sidebar.js View File

@@ -6,6 +6,7 @@ import i18n from "i18n-js";
6 6
 import * as WebBrowser from 'expo-web-browser';
7 7
 import SidebarDivider from "./SidebarDivider";
8 8
 import SidebarItem from "./SidebarItem";
9
+import {TouchableRipple} from "react-native-paper";
9 10
 
10 11
 const deviceWidth = Dimensions.get("window").width;
11 12
 
@@ -154,9 +155,17 @@ export default class SideBar extends React.PureComponent<Props, State> {
154 155
     }
155 156
 
156 157
     render() {
158
+        const onPress = this.onListItemPress.bind(this, {route: 'TetrisScreen'});
157 159
         return (
158 160
             <View style={{height: '100%'}}>
159
-                <Image source={require("../assets/drawer-cover.png")} style={styles.drawerCover}/>
161
+                <TouchableRipple
162
+                    onPress={onPress}
163
+                >
164
+                    <Image
165
+                        source={require("../assets/drawer-cover.png")}
166
+                        style={styles.drawerCover}
167
+                    />
168
+                </TouchableRipple>
160 169
                 <FlatList
161 170
                     data={this.dataSet}
162 171
                     extraData={this.state}

+ 29
- 0
navigation/DrawerNavigator.js View File

@@ -9,6 +9,7 @@ import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
9 9
 import SelfMenuScreen from '../screens/SelfMenuScreen';
10 10
 import AvailableRoomScreen from "../screens/Websites/AvailableRoomScreen";
11 11
 import BibScreen from "../screens/Websites/BibScreen";
12
+import TetrisScreen from "../screens/Tetris/TetrisScreen";
12 13
 import DebugScreen from '../screens/About/DebugScreen';
13 14
 import Sidebar from "../components/Sidebar";
14 15
 import {createStackNavigator, TransitionPresets} from "@react-navigation/stack";
@@ -161,6 +162,30 @@ function BibStackComponent() {
161 162
     );
162 163
 }
163 164
 
165
+const TetrisStack = createStackNavigator();
166
+
167
+function TetrisStackComponent() {
168
+    return (
169
+        <TetrisStack.Navigator
170
+            initialRouteName="TetrisScreen"
171
+            headerMode="float"
172
+            screenOptions={defaultScreenOptions}
173
+        >
174
+            <TetrisStack.Screen
175
+                name="TetrisScreen"
176
+                component={TetrisScreen}
177
+                options={({navigation}) => {
178
+                    const openDrawer = getDrawerButton.bind(this, navigation);
179
+                    return {
180
+                        title: 'Tetris',
181
+                        headerLeft: openDrawer
182
+                    };
183
+                }}
184
+            />
185
+        </TetrisStack.Navigator>
186
+    );
187
+}
188
+
164 189
 const Drawer = createDrawerNavigator();
165 190
 
166 191
 function getDrawerContent(props) {
@@ -202,6 +227,10 @@ export default function DrawerNavigator() {
202 227
                 name="BibScreen"
203 228
                 component={BibStackComponent}
204 229
             />
230
+            <Drawer.Screen
231
+                name="TetrisScreen"
232
+                component={TetrisStackComponent}
233
+            />
205 234
         </Drawer.Navigator>
206 235
     );
207 236
 }

+ 158
- 0
screens/Tetris/GameLogic.js View File

@@ -0,0 +1,158 @@
1
+// @flow
2
+
3
+import Tetromino from "./Tetromino";
4
+
5
+export default class GameLogic {
6
+
7
+    currentGrid: Array<Array<Object>>;
8
+
9
+    height: number;
10
+    width: number;
11
+
12
+    gameRunning: boolean;
13
+    gameTime: number;
14
+    score: number;
15
+
16
+    currentObject: Tetromino;
17
+
18
+    gameTick: number;
19
+    gameTickInterval: IntervalID;
20
+
21
+    onTick: Function;
22
+
23
+    constructor(height: number, width: number) {
24
+        this.height = height;
25
+        this.width = width;
26
+        this.gameRunning = false;
27
+        this.gameTick = 250;
28
+    }
29
+
30
+    getHeight(): number {
31
+        return this.height;
32
+    }
33
+
34
+    getWidth(): number {
35
+        return this.width;
36
+    }
37
+
38
+    isGameRunning(): boolean {
39
+        return this.gameRunning;
40
+    }
41
+
42
+    getEmptyGrid() {
43
+        let grid = [];
44
+        for (let row = 0; row < this.getHeight(); row++) {
45
+            grid.push([]);
46
+            for (let col = 0; col < this.getWidth(); col++) {
47
+                grid[row].push({
48
+                    color: '#fff',
49
+                    isEmpty: true,
50
+                });
51
+            }
52
+        }
53
+        return grid;
54
+    }
55
+
56
+    getGridCopy() {
57
+        return JSON.parse(JSON.stringify(this.currentGrid));
58
+    }
59
+
60
+    getFinalGrid() {
61
+        let coord = this.currentObject.getCellsCoordinates();
62
+        let finalGrid = this.getGridCopy();
63
+        for (let i = 0; i < coord.length; i++) {
64
+            finalGrid[coord[i].y][coord[i].x] = {
65
+                color: this.currentObject.getColor(),
66
+                isEmpty: false,
67
+            };
68
+        }
69
+        return finalGrid;
70
+    }
71
+
72
+    freezeTetromino() {
73
+        let coord = this.currentObject.getCellsCoordinates();
74
+        for (let i = 0; i < coord.length; i++) {
75
+            this.currentGrid[coord[i].y][coord[i].x] = {
76
+                color: this.currentObject.getColor(),
77
+                isEmpty: false,
78
+            };
79
+        }
80
+    }
81
+
82
+    isTetrominoPositionValid() {
83
+        let isValid = true;
84
+        let coord = this.currentObject.getCellsCoordinates();
85
+        for (let i = 0; i < coord.length; i++) {
86
+            if (coord[i].x >= this.getWidth()
87
+                || coord[i].x < 0
88
+                || coord[i].y >= this.getHeight()
89
+                || coord[i].y < 0
90
+                || !this.currentGrid[coord[i].y][coord[i].x].isEmpty) {
91
+                isValid = false;
92
+                break;
93
+            }
94
+        }
95
+        return isValid;
96
+    }
97
+
98
+    tryMoveTetromino(x: number, y: number) {
99
+        if (x > 1) x = 1; // Prevent moving from more than one tile
100
+        if (x < -1) x = -1;
101
+        if (y > 1) y = 1;
102
+        if (y < -1) y = -1;
103
+        if (x !== 0 && y !== 0) y = 0; // Prevent diagonal movement
104
+
105
+        this.currentObject.move(x, y);
106
+        let isValid = this.isTetrominoPositionValid();
107
+
108
+        if (!isValid && x !== 0)
109
+            this.currentObject.move(-x, 0);
110
+        else if (!isValid && y !== 0) {
111
+            this.currentObject.move(0, -y);
112
+            this.freezeTetromino();
113
+            this.createTetromino();
114
+        }
115
+    }
116
+
117
+    onTick(callback: Function) {
118
+        this.gameTime++;
119
+        this.score++;
120
+        this.tryMoveTetromino(0, 1);
121
+        callback(this.gameTime, this.score, this.getFinalGrid());
122
+    }
123
+
124
+    rightPressed(callback: Function) {
125
+        this.tryMoveTetromino(1, 0);
126
+        callback(this.getFinalGrid());
127
+    }
128
+
129
+    leftPressed(callback: Function) {
130
+        this.tryMoveTetromino(-1, 0);
131
+        callback(this.getFinalGrid());
132
+    }
133
+
134
+    createTetromino() {
135
+        let shape = Math.floor(Math.random() * 7);
136
+        this.currentObject = new Tetromino(shape);
137
+        if (!this.isTetrominoPositionValid())
138
+            this.endGame();
139
+    }
140
+
141
+    endGame() {
142
+        console.log('Game Over!');
143
+        clearInterval(this.gameTickInterval);
144
+    }
145
+
146
+    startGame(callback: Function) {
147
+        if (this.gameRunning)
148
+            return;
149
+        this.gameRunning = true;
150
+        this.gameTime = 0;
151
+        this.score = 0;
152
+        this.currentGrid = this.getEmptyGrid();
153
+        this.createTetromino();
154
+        this.onTick = this.onTick.bind(this, callback);
155
+        this.gameTickInterval = setInterval(this.onTick, this.gameTick);
156
+    }
157
+
158
+}

+ 116
- 0
screens/Tetris/TetrisScreen.js View File

@@ -0,0 +1,116 @@
1
+// @flow
2
+
3
+import * as React from 'react';
4
+import {View} from 'react-native';
5
+import {IconButton, Text, withTheme} from 'react-native-paper';
6
+import GameLogic from "./GameLogic";
7
+import Grid from "./components/Grid";
8
+
9
+type Props = {
10
+    navigation: Object,
11
+}
12
+
13
+type State = {
14
+    grid: Array<Array<Object>>,
15
+    gameTime: number,
16
+    gameScore: number
17
+}
18
+
19
+class TetrisScreen extends React.Component<Props, State> {
20
+
21
+    colors: Object;
22
+
23
+    logic: GameLogic;
24
+    onTick: Function;
25
+    updateGrid: Function;
26
+
27
+    constructor(props) {
28
+        super(props);
29
+        this.colors = props.theme.colors;
30
+        this.logic = new GameLogic(20, 10);
31
+        this.state = {
32
+            grid: this.logic.getEmptyGrid(),
33
+            gameTime: 0,
34
+            gameScore: 0,
35
+        };
36
+        this.onTick = this.onTick.bind(this);
37
+        this.updateGrid = this.updateGrid.bind(this);
38
+        const onScreenBlur = this.onScreenBlur.bind(this);
39
+        this.props.navigation.addListener('blur', onScreenBlur);
40
+    }
41
+
42
+
43
+    /**
44
+     * Remove any interval on un-focus
45
+     */
46
+    onScreenBlur() {
47
+        this.logic.endGame();
48
+    }
49
+
50
+    onTick(time: number, score: number, newGrid: Array<Array<Object>>) {
51
+        this.setState({
52
+            gameTime: time,
53
+            gameScore: score,
54
+            grid: newGrid,
55
+        });
56
+    }
57
+
58
+    updateGrid(newGrid: Array<Array<Object>>) {
59
+        this.setState({
60
+            grid: newGrid,
61
+        });
62
+    }
63
+
64
+    startGame() {
65
+        if (!this.logic.isGameRunning()) {
66
+            this.logic.startGame(this.onTick);
67
+        }
68
+    }
69
+
70
+    render() {
71
+        return (
72
+            <View style={{
73
+                width: '100%',
74
+                height: '100%',
75
+            }}>
76
+                <Text style={{
77
+                    textAlign: 'center',
78
+                }}>
79
+                    Score: {this.state.gameScore}
80
+                </Text>
81
+                <Text style={{
82
+                    textAlign: 'center',
83
+                }}>
84
+                    time: {this.state.gameTime}
85
+                </Text>
86
+                <Grid
87
+                    width={this.logic.getWidth()}
88
+                    height={this.logic.getHeight()}
89
+                    grid={this.state.grid}
90
+                />
91
+                <View style={{
92
+                    flexDirection: 'row-reverse',
93
+                }}>
94
+                    <IconButton
95
+                        icon="arrow-right"
96
+                        size={40}
97
+                        onPress={() => this.logic.rightPressed(this.updateGrid)}
98
+                    />
99
+                    <IconButton
100
+                        icon="arrow-left"
101
+                        size={40}
102
+                        onPress={() => this.logic.leftPressed(this.updateGrid)}
103
+                    />
104
+                    <IconButton
105
+                        icon="format-rotate-90"
106
+                        size={40}
107
+                        onPress={() => this.startGame()}
108
+                    />
109
+                </View>
110
+            </View>
111
+        );
112
+    }
113
+
114
+}
115
+
116
+export default withTheme(TetrisScreen);

+ 82
- 0
screens/Tetris/Tetromino.js View File

@@ -0,0 +1,82 @@
1
+export default class Tetromino {
2
+
3
+    static types = {
4
+        'I': 0,
5
+        'O': 1,
6
+        'T': 2,
7
+        'S': 3,
8
+        'Z': 4,
9
+        'J': 5,
10
+        'L': 6,
11
+    };
12
+
13
+    static shapes = {
14
+        0: [
15
+            [1, 1, 1, 1]
16
+        ],
17
+        1: [
18
+            [1, 1],
19
+            [1, 1]
20
+        ],
21
+        2: [
22
+            [0, 1, 0],
23
+            [1, 1, 1],
24
+        ],
25
+        3: [
26
+            [0, 1, 1],
27
+            [1, 1, 0],
28
+        ],
29
+        4: [
30
+            [1, 1, 0],
31
+            [0, 1, 1],
32
+        ],
33
+        5: [
34
+            [1, 0, 0],
35
+            [1, 1, 1],
36
+        ],
37
+        6: [
38
+            [0, 0, 1],
39
+            [1, 1, 1],
40
+        ],
41
+    };
42
+
43
+    static colors = {
44
+        0: '#00f8ff',
45
+        1: '#ffe200',
46
+        2: '#b817ff',
47
+        3: '#0cff34',
48
+        4: '#ff000b',
49
+        5: '#1000ff',
50
+        6: '#ff9400',
51
+    }
52
+
53
+
54
+    currentType: Tetromino.types;
55
+    position: Object;
56
+
57
+    constructor(type: Tetromino.types) {
58
+        this.currentType = type;
59
+        this.position = {x: 0, y: 0};
60
+    }
61
+
62
+    getColor() {
63
+        return Tetromino.colors[this.currentType];
64
+    }
65
+
66
+    getCellsCoordinates() {
67
+        let coordinates = [];
68
+        for (let row = 0; row < Tetromino.shapes[this.currentType].length; row++) {
69
+            for (let col = 0; col < Tetromino.shapes[this.currentType][row].length; col++) {
70
+                if (Tetromino.shapes[this.currentType][row][col] === 1)
71
+                    coordinates.push({x: this.position.x + col, y: this.position.y + row});
72
+            }
73
+        }
74
+        return coordinates;
75
+    }
76
+
77
+    move(x: number, y: number) {
78
+        this.position.x += x;
79
+        this.position.y += y;
80
+    }
81
+
82
+}

+ 21
- 0
screens/Tetris/components/Cell.js View File

@@ -0,0 +1,21 @@
1
+// @flow
2
+
3
+import * as React from 'react';
4
+import {View} from 'react-native';
5
+import {withTheme} from 'react-native-paper';
6
+
7
+function Cell(props) {
8
+    const colors = props.theme.colors;
9
+    return (
10
+        <View style={{
11
+            flex: 1,
12
+            backgroundColor: props.color,
13
+            borderColor: props.isEmpty ? props.color : "#393939",
14
+            borderStyle: 'solid',
15
+            borderWidth: 1,
16
+            aspectRatio: 1,
17
+        }}/>
18
+    );
19
+}
20
+
21
+export default withTheme(Cell);

+ 63
- 0
screens/Tetris/components/Grid.js View File

@@ -0,0 +1,63 @@
1
+// @flow
2
+
3
+import * as React from 'react';
4
+import {View} from 'react-native';
5
+import {withTheme} from 'react-native-paper';
6
+import Cell from "./Cell";
7
+
8
+type Props = {
9
+    navigation: Object,
10
+    grid: Array<Array<Object>>,
11
+    height: number,
12
+    width: number,
13
+}
14
+
15
+class Grid extends React.Component<Props>{
16
+
17
+    colors: Object;
18
+
19
+    constructor(props) {
20
+        super(props);
21
+        this.colors = props.theme.colors;
22
+    }
23
+
24
+    getRow(rowNumber: number) {
25
+        let cells = [];
26
+        for (let i = 0; i < this.props.width; i++) {
27
+            let cell = this.props.grid[rowNumber][i];
28
+            cells.push(<Cell color={cell.color} isEmpty={cell.isEmpty}/>);
29
+        }
30
+        return(
31
+            <View style={{
32
+                flexDirection: 'row',
33
+                backgroundColor: '#fff'
34
+            }}>
35
+                {cells}
36
+            </View>
37
+        );
38
+    }
39
+
40
+    getGrid() {
41
+        let rows = [];
42
+        for (let i = 0; i < this.props.height; i++) {
43
+            rows.push(this.getRow(i));
44
+        }
45
+        return rows;
46
+    }
47
+
48
+    render() {
49
+        return (
50
+            <View style={{
51
+                flexDirection: 'column',
52
+                height: '80%',
53
+                aspectRatio: this.props.width/this.props.height,
54
+                marginLeft: 'auto',
55
+                marginRight: 'auto',
56
+            }}>
57
+                {this.getGrid()}
58
+            </View>
59
+        );
60
+    }
61
+}
62
+
63
+export default withTheme(Grid);

Loading…
Cancel
Save