Last active
January 16, 2018 08:10
-
-
Save busypeoples/ee924fcbd17616dcfc1da3b4f030686e to your computer and use it in GitHub Desktop.
ReactGame Example: React + Flow
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment