Introduce initial functionality - 'movable' shell components along with 'ball' tracking

This commit is contained in:
Kai Moseley 2016-09-20 20:25:42 +01:00
parent 1d990f63f2
commit caa65a7984
10 changed files with 22263 additions and 9 deletions

15
dist/index.html vendored Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Shell game</title>
</head>
<body>
<div id="react-shell-game">
</div>
</body>
<script src="/shell-game.bundle.js"></script>
</html>

21972
dist/shell-game.bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

34
src/components/Game.js Normal file
View File

@ -0,0 +1,34 @@
//@flow
import React from 'react';
type Shell = {
position: number,
}
type GameProps = {
shells: Shell[],
shellClickHandler: (id: number) => {},
shuffleShellsClickHandler: (event: Object) => {},
startGameClickHandler: (event: Object) => {},
output: string,
ballShellId: number,
inProgress: boolean,
shuffled: boolean,
}
export const Game = (props: GameProps) => (
<div>
{ props.shells.map(shell =>
<div
key={ shell.id }
onClick={ () => props.shellClickHandler(shell.id) }
>
Hi! I'm a shell! (id { shell.id })
</div>
)}
<button onClick={ props.startGameClickHandler } disabled={ props.inProgress }>Play!</button>
<button onClick={ props.shuffleShellsClickHandler } disabled={ props.inProgress && props.shuffled || !props.inProgress }>Shuffle!</button>
<p>{ props.output }</p>
<p>{ props.ballShellId }</p>
</div>
);

View File

@ -0,0 +1,123 @@
//@flow
import React, { Component } from 'react';
import {
createRandomNumberFn,
createShells,
randomizeArrayOrder,
} from '../utils/gameUtils';
import { Game } from './Game';
//=====================
// Flow types
//=====================
type GameContainerState = {
ballShellId: number,
shells: Shell[],
inProgress: boolean,
output: string,
};
type GameContainerProps = {
numberOfShells: number,
};
type Shell = {
position: number,
};
//======================
// Game component
//======================
export class GameContainer extends Component {
state: GameContainerState;
props: GameContainerProps;
constructor() {
super();
this.state = {
ballShellId: 0,
shells: [],
inProgress: false,
shuffled: false,
output: `Click on 'Play' to begin!`,
};
this.selectRandomShell = null;
//In general it's a good idea to bind methods which refer to this in the
//constructor. It means, amongst other things, that the functions can be
//used as direct event handlers rather than wrapping them in a function.
this._initializeGame = this._initializeGame.bind(this);
this._startGame = this._startGame.bind(this);
this._setWinningShell = this._setWinningShell.bind(this);
this._shellClickHandler = this._shellClickHandler.bind(this);
this._shuffleShells = this._shuffleShells.bind(this);
}
_initializeGame(numberOfShells: number): void {
this.setState({
ballShellId: 0,
shells: createShells(numberOfShells)
});
this.selectRandomShell = createRandomNumberFn(0, numberOfShells - 1);
}
_startGame(): void {
this._setWinningShell();
this.setState({
inProgress: true,
output: `Click a shell to see if you guessed correctly!`
});
}
_setWinningShell(): void {
if (!this.selectRandomShell) throw new Error('selectRandomShell not properly initialized!');
this.setState({
ballShellId: this.selectRandomShell(),
});
}
_shellClickHandler(shellId: number) {
//We only need to check if the shell is a winning shell if it is in progress and has been shuffled
if (!this.state.inProgress) return this.setState({ output: `Click on 'Play' to begin!`});
if (!this.state.shuffled) return this.setState({ output: `Click on 'Shuffle' to shuffle the shells!`});
this.setState({
output: this.state.ballShellId === shellId ? `You won! Click 'Play' to play again` : `Better luck next time! Click 'Play' to play again`,
inProgress: false,
shuffled: false,
});
}
_shuffleShells() {
this.setState({
shells: randomizeArrayOrder(this.state.shells),
shuffled: true,
});
}
componentWillMount() {
this._initializeGame(this.props.numberOfShells);
}
componentWillReceiveProps(newProps: GameProps) {
if(newProps.numberOfShells !== this.props.numberOfShells) {
this._initializeGame(newProps.numberOfShells);
}
}
render() {
return (
<Game
shells={ this.state.shells }
output={ this.state.output }
ballShellId={ this.state.ballShellId }
inProgress={ this.state.inProgress }
shuffled={ this.state.shuffled }
shellClickHandler={ this._shellClickHandler }
shuffleShellsClickHandler={ this._shuffleShells }
startGameClickHandler={ this._startGame }
/>
);
}
}

View File

@ -0,0 +1,26 @@
//@flow
import React from 'react';
import { GameContainer } from './GameContainer';
type ShellGameAppProps = {
numberOfShells: number,
onInputChange: (event: Object) => {},
};
export const ShellGameApp = (props: ShellGameAppProps) => (
<div>
<GameContainer numberOfShells={ props.numberOfShells } />
<div>
<label htmlFor="numberOfShells">Number of shells: </label>
<select
name="numberOfShells"
value={ props.numberOfShells }
onChange={ props.onInputChange }
>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</div>
</div>
);

View File

@ -0,0 +1,36 @@
//@flow
import React, { Component } from 'react';
import { ShellGameApp } from './ShellGameApp';
type ShellGameAppContainerState = {
numberOfShells: number,
}
export class ShellGameAppContainer extends Component {
state: ShellGameAppContainerState;
constructor() {
super();
this.state = {
numberOfShells: 3,
};
this._handleInputUpdate = this._handleInputUpdate.bind(this);
}
_handleInputUpdate(e) {
if (!this.state.hasOwnProperty(e.target.name)) throw new Error('Form input name should be on the GameShellAppContainer state');
this.setState({
[e.target.name]: e.target.value,
});
}
render() {
return (
<ShellGameApp
numberOfShells={ this.state.numberOfShells }
onInputChange={ this._handleInputUpdate }
/>
)
}
}

11
src/components/index.js Normal file
View File

@ -0,0 +1,11 @@
import { Game } from './Game';
import { GameContainer } from './GameContainer';
import { ShellGameApp } from './ShellGameApp';
import { ShellGameAppContainer } from './ShellGameAppContainer';
export {
Game,
GameContainer,
ShellGameApp,
ShellGameAppContainer,
}

8
src/index.js Normal file
View File

@ -0,0 +1,8 @@
//@flow
import React from 'react';
import ReactDOM from 'react-dom';
import { ShellGameAppContainer } from './components';
ReactDOM.render(
<ShellGameAppContainer />
, document.getElementById('react-shell-game'));

33
src/utils/gameUtils.js Normal file
View File

@ -0,0 +1,33 @@
//@flow
//======================
// Utility functions
//======================
export function createRandomNumberFn(minimum: number, maximum: number): () => number {
return () => minimum + Math.floor(Math.random() * ((maximum-minimum) + 1));
}
export function randomizeArrayOrder(array: array): array {
let oldArray = [ ...array ];
let newArr = [];
while(oldArray.length) {
newArr = [...newArr, ...oldArray.splice(createRandomNumberFn(0, oldArray.length)(), 1)];
}
return newArr;
}
export function createShells(numberOfShells: number): Shell[] {
/* The shell containing the ball will be tracked independently of shells, but
in order to properly animate the shells later, a fixed id is needed. The position
will be used to track where the shell should be displayed, with the id
remaining fixed. */
let newShellArr = [];
for (let i=0; i<numberOfShells; i++) {
newShellArr = [ ...newShellArr, {
id: i,
}];
}
return newShellArr;
}

View File

@ -3,15 +3,12 @@ const path = require('path');
const _ = require('lodash');
const PATH_ESLINT = path.join(__dirname, '.eslintrc.js');
const PATH_SRC = path.join(__dirname, 'src/client/');
const PATH_ALIASES = _.mapValues({
'home-screen': 'src/client/home-screen',
}, relativePath => path.join(__dirname, relativePath));
const PATH_SRC = path.join(__dirname, 'src');
module.exports = {
context: __dirname,
entry: {
'home-screen': './src/client/home-screen'
'shell-game': PATH_SRC
},
devServer: {
inline: true,
@ -49,12 +46,11 @@ module.exports = {
},
resolve: {
extensions: ['', '.js', '.json', '.jsx'],
alias: PATH_ALIASES,
alias: [
{ 'shell-game': PATH_SRC }
],
},
plugins: [
new webpack.ProvidePlugin({
_: 'lodash'
}),
function () {
this.plugin('done', () =>
setTimeout(() => console.log('\nFinished at ' + (new Date).toLocaleTimeString() + '\n'), 10)