Introduce initial functionality - 'movable' shell components along with 'ball' tracking
This commit is contained in:
parent
1d990f63f2
commit
caa65a7984
|
@ -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>
|
File diff suppressed because one or more lines are too long
|
@ -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>
|
||||
);
|
|
@ -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 }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
|
@ -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 }
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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'));
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue