phpsmarter
1/16/2018 - 8:10 AM

ReactGame Example: React + Flow

ReactGame Example: React + Flow

// @flow
import React from 'react';
import { render } from 'react-dom';

type Field = 0 | 1;

type Position = [Field, Field, Field, Field];

type Unit = [Position, Position, Position, Position];

type Units = Array<Unit>;

type Board = Array<Array<Field>>;

const unit1: Unit = [[0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0]];

const unit2: Unit = [[0, 0, 1, 0], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 0]];

const unit3: Unit = [[0, 0, 0, 0], [0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]];

const unit4: Unit = [[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]];

const unit5: Unit = [[0, 0, 0, 0], [0, 0, 1, 1], [0, 1, 1, 0], [0, 0, 0, 0]];

const unit6: Unit = [[0, 0, 1, 0], [0, 0, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0]];

const unit7: Unit = [[0, 0, 0, 0], [0, 1, 1, 0], [0, 0, 1, 1], [0, 0, 0, 0]];

const units: Units = [unit1, unit2, unit3, unit4, unit5, unit6, unit7];

/*
 constants
 */
const numberOfRows = 20;

const numberOfColumns = 10;

const msTimeout = 300;

/*
 Helper functions
 */
const rotateLeft = (unit: Unit): Unit => {
	let [
		[a1, a2, a3, a4],
		[b1, b2, b3, b4],
		[c1, c2, c3, c4],
		[d1, d2, d3, d4]
	] = unit;
	return [
		[a4, b4, c4, d4],
		[a3, b3, c3, d3],
		[a2, b2, c2, d2],
		[a1, b1, c1, d1]
	];
};

const rotateRight = (unit: Unit): Unit => {
	const [
		[a1, a2, a3, a4],
		[b1, b2, b3, b4],
		[c1, c2, c3, c4],
		[d1, d2, d3, d4]
	] = unit;
	return [
		[d1, c1, b1, a1],
		[d2, c2, b2, a2],
		[d3, c3, b3, a3],
		[d4, c4, b4, a4]
	];
};

let isIntersecting = (
	rows: Board,
	unit: Unit,
	x: number,
	y: number
): boolean => {
	const foundIntersections = unit.filter((row, i) => {
		const found = row.filter((col, j) => {
			return (
				col === 1 &&
				(y + i >= numberOfRows ||
					x + j < 0 ||
					x + j >= numberOfColumns ||
					rows[y + i][x + j] === 1)
			);
		});
		return found.length > 0;
	});

	return foundIntersections.length > 0;
};

const emptyRow: Array<Field> = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

const createRows = (
	rows: number = numberOfRows,
	cols: number = numberOfColumns
): Board => {
	return Array.from(Array(rows).keys()).map(row => emptyRow);
};

const updateBoard = (rows: Board, unit: Unit, x: number, y: number): Board => {
	let newRows = rows.map(row => row.map(col => col));
	unit.forEach((row, i) =>
		row.forEach((col, j) => {
			if (col === 1) {
				newRows[y + i][x + j] = 1;
			}
		})
	);
	return newRows;
};

const hasEmptyField = (row: Array<Field>): boolean => {
	const found = row.filter(col => col === 0);
	return found.length > 0;
};

const removeFinishedRows = (board: Board): Board =>
	board.reduce((result, row) => {
		return hasEmptyField(row) ? [...result, [...row]] : result;
	}, []);

const randomizeUnit = (units: Units): Unit =>
	units[Math.floor(Math.random() * units.length)];

const getBackgroundColor = col => {
	switch (col) {
		case 1:
			return '#000';
		default:
			return '#fff';
	}
};

const GameStateTypes = {
	Initial: 'Initial',
	Play: 'Play',
	Pause: 'Pause',
	End: 'End'
};

type GameState = $Values<typeof GameStateTypes>;

/*
   <Board />
 */

type BoardProps = {
	rows: Board | Unit,
	gameState?: GameState
};

const gameStateInit = GameStateTypes.Initial;

const GameBoard = ({ rows, gameState = gameStateInit }: BoardProps) => (
	<div style={{ opacity: gameState === GameStateTypes.End ? '.5' : '1' }}>
		{rows.map((row, i) => (
			<div key={`key-${i}`} style={{ display: 'inline' }}>
				{row.map((col, j) => (
					<div
						key={`col-${i}-${j}`}
						style={{
							width: '30px',
							height: '30px',
							float: 'left',
							border: '1px solid #eee',
							background: getBackgroundColor(col)
						}}
					/>
				))}
				<br style={{ clear: 'both' }} />
			</div>
		))}
	</div>
);

const getGameStateInfo = (gameState: GameState): string => {
	switch (gameState) {
		case GameStateTypes.Initial:
			return 'Press the spacebar to start';
		case GameStateTypes.Play:
			return 'Press the spacebar to Pause';
		case GameStateTypes.Pause:
			return 'Press the spacebar to Continue';
		case GameStateTypes.End:
			return 'Game Over! Press the spacebar to Restart';
		default:
			return 'Press the spacebar to start';
	}
};

/*
   <Info />
 */

type InfoProps = {
	score: number,
	next: Unit,
	gameState: GameState
};

const Info = ({ score, next, gameState }: InfoProps) => (
	<div>
		<h3>ReasonML Experiment</h3>
		<br />
		<div>{getGameStateInfo(gameState)}</div>
		<br />
		<div>
			{' '}
			<h4>Score: {score}</h4>{' '}
		</div>
		<br />
		<div>
			Next: <br /> <br /> <GameBoard rows={next} />{' '}
		</div>
		<br />
		<div>
			How to Play:
			<br />
			<br />
			<div>a/d: rotate (left/right)</div>
			<div>j/k: navigate (left/right)</div>
			<div>s: navigate (down)</div>
		</div>
	</div>
);

const Toggle = 'Toggle';
const Tick = 'Tick';
const MoveLeft = 'MoveLeft';
const MoveRight = 'MoveRight';
const MoveDown = 'MoveDown';
const RotateLeft = 'RotateLeft';
const RotateRight = 'RotateRight';
const Drop = 'Drop';

/*
   Game
 */
type Actions =
	| 'Toggle'
	| 'Tick'
	| 'MoveLeft'
	| 'MoveRight'
	| 'MoveDown'
	| 'RotateLeft'
	| 'RotateRight'
	| 'Drop';

const runEvent = (event: SyntheticKeyboardEvent<HTMLElement>, reduce) => {
	const key = event.charCode;
	switch (key) {
		case 32:
			reduce(() => Toggle);
			break;
		case 97:
			reduce(() => RotateLeft);
			break;
		case 100:
			reduce(() => RotateRight);
			break;
		case 106:
			reduce(() => MoveLeft);
			break;
		case 107:
			reduce(() => MoveRight);
			break;
		case 115:
			reduce(() => MoveDown);
			break;
		default:
		// Do Nothing...
	}
};

type GameComponentState = {
	gameState: GameState,
	score: number,
	board: Board,
	unit: Unit,
	next: Unit,
	posX: number,
	posY: number
};

const initializeState = (units): GameComponentState => ({
	gameState: GameStateTypes.Initial,
	score: 0,
	unit: randomizeUnit(units),
	next: randomizeUnit(units),
	posX: 3,
	posY: 0,
	board: [...createRows()]
});

const divStyleLeft = {
	width: '30%',
	float: 'left',
	padding: '3%',
	minWidth: '450px'
};

let divStyleRight = {
	float: 'left',
	paddingLeft: '3%',
	paddingRight: '3%',
	paddingTop: '1%',
	paddingBottom: '5%'
};

let mainStyling = {
	outlineColor: '#fff',
	fontSize: '1.5em',
	width: '90%'
};

const play = reduce => () => {
	reduce(() => Tick);
};

class Game extends React.Component<{}, GameComponentState> {
	intervalId: ?number;
	reducer: Function;
	constructor(props) {
		super(props);
		this.state = initializeState(units);
		this.intervalId = null;
		this.reducer = this.reducer.bind(this);
	}
	reducer(actionFn) {
		const action: Actions = actionFn();
		const state = this.state;
		switch (action) {
			case Toggle:
				switch (state.gameState) {
					case GameStateTypes.Play:
						this.setState(
							state => ({ gameState: GameStateTypes.Pause }),
							() => {
								if (this.intervalId) {
									clearInterval(this.intervalId);
									this.intervalId = null;
								}
							}
						);
						break;
					case GameStateTypes.End:
						this.setState(
							state => ({
								...initializeState(units),
								gameState: GameStateTypes.Play
							}),
							() => {
								if (!this.intervalId) {
									this.intervalId = setInterval(play(this.reducer), msTimeout);
								}
							}
						);
						break;
					default:
						this.setState(
							state => ({ gameState: GameStateTypes.Play }),
							() => {
								if (!this.intervalId) {
									this.intervalId = setInterval(play(this.reducer), msTimeout);
								}
							}
						);
				}
				break;
			case Tick:
				if (state.gameState === GameStateTypes.End) {
					if (this.intervalId) {
						clearInterval(this.intervalId);
						this.intervalId = null;
					}
				} else if (
					isIntersecting(state.board, state.unit, state.posX, state.posY + 1)
				) {
					const nextBoard = updateBoard(
						state.board,
						state.unit,
						state.posX,
						state.posY
					);

					const nextRows = removeFinishedRows(nextBoard);
					const rowsRemoved = numberOfRows - nextRows.length;
					const board = rowsRemoved
						? [...createRows(rowsRemoved), ...nextRows]
						: nextRows;
					const score =
						state.score + 10 + rowsRemoved * rowsRemoved * numberOfColumns * 3;
					const unit = state.next;
					const posX = numberOfColumns / 2 - 2;
					const posY = 0;
					const next = randomizeUnit(units);
					if (isIntersecting(board, state.next, numberOfColumns / 2 - 2, 0)) {
						this.setState(state => ({
							unit,
							posX,
							posY,
							next,
							score,
							board,
							gameState: GameStateTypes.End
						}));
					} else {
						this.setState(state => ({
							...state,
							unit,
							posX,
							posY,
							next,
							score,
							board
						}));
					}
				} else {
					this.setState(state => ({ posY: state.posY + 1 }));
				}
				break;
			case MoveLeft:
				if (
					state.gameState === GameStateTypes.Play &&
					!isIntersecting(state.board, state.unit, state.posX - 1, state.posY)
				) {
					this.setState(state => ({ posX: state.posX - 1 }));
				}
				break;
			case MoveRight:
				if (
					state.gameState === GameStateTypes.Play &&
					!isIntersecting(state.board, state.unit, state.posX + 1, state.posY)
				) {
					this.setState(state => ({ posX: state.posX + 1 }));
				}
				break;
			case MoveDown:
				if (
					state.gameState === GameStateTypes.Play &&
					!isIntersecting(state.board, state.unit, state.posX, state.posY + 1)
				) {
					this.setState(state => ({ posY: state.posY + 1 }));
				}
				break;
			case RotateLeft:
				const nextUnit = rotateLeft(state.unit);
				if (
					state.gameState === GameStateTypes.Play &&
					!isIntersecting(state.board, nextUnit, state.posX, state.posY)
				) {
					this.setState(state => ({ unit: nextUnit }));
				}
				break;
			case RotateRight:
				const newPiece = rotateRight(state.unit);
				if (
					state.gameState === GameStateTypes.Play &&
					!isIntersecting(state.board, newPiece, state.posX, state.posY)
				) {
					this.setState(state => ({ unit: newPiece }));
				}
				break;
			case Drop:
				/* Implement later on */
				break;
			default:
			// Do Nothing...
		}
	}

	render() {
		const { board, unit, posX, posY, score, gameState, next } = this.state;
		const displayRows =
			gameState === GameStateTypes.Initial
				? board
				: updateBoard(board, unit, posX, posY);
		return (
			<div
				onKeyPress={e => runEvent(e, this.reducer)}
				style={mainStyling}
				tabIndex="0"
			>
				Click anywhere on the screen for focus
				<div style={divStyleLeft}>
					<GameBoard rows={displayRows} gameState={gameState} />
				</div>
				<div style={divStyleRight}>
					<Info score={score} next={next} gameState={gameState} />
				</div>
			</div>
		);
	}
}

const root = document.getElementById('root');

if (root) {
	render(<Game />, root);
}