diff --git a/.flowconfig b/.flowconfig
index efe3848..52ab208 100644
--- a/.flowconfig
+++ b/.flowconfig
@@ -1,2 +1,3 @@
[ignore]
-.*/node_modules/fbjs/*
\ No newline at end of file
+.*/node_modules/fbjs/*
+.*/node_modules/react-motion/*
\ No newline at end of file
diff --git a/dist/index.html b/dist/index.html
index f497f6c..2ee33f4 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -3,12 +3,10 @@
Shell game
+
-
-
-
-
+
diff --git a/package.json b/package.json
index 6aa6efe..5f49cad 100644
--- a/package.json
+++ b/package.json
@@ -1,63 +1,36 @@
{
- "name": "react-custom-home",
- "version": "0.0.0",
- "description": "A customisable home screen",
+ "name": "shell-game",
+ "version": "0.0.1",
+ "description": "A simple, React based, game of shells ",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server --progress --content-base dist/",
"test": "jest"
},
- "jest": {
- "testPathDirs": [
- "/src"
- ],
- "moduleFileExtensions": [
- "js",
- "jsx",
- "json"
- ],
- "moduleDirectories": [
- "node_modules"
- ],
- "modulePaths": [
- "/src"
- ],
- "moduleNameMapper": {
- "^.+\\.(css|less|scss)$": "/src/__mocks__/styleMock.js",
- "^.+\\.(gif|ttf|eot|svg)$": "/src/__mocks__/fileMock.js"
- }
- },
"author": "Kai Moseley",
"license": "ISC",
"devDependencies": {
"babel-eslint": "^6.1.2",
"babel-loader": "^6.2.5",
- "babel-plugin-syntax-async-functions": "^6.13.0",
- "babel-plugin-transform-async-to-generator": "^6.8.0",
"babel-plugin-transform-flow-strip-types": "^6.14.0",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-preset-es2015": "^6.14.0",
"babel-preset-react": "^6.11.1",
- "body-parser": "^1.15.2",
"css-loader": "^0.24.0",
- "enzyme": "^2.4.1",
"eslint": "^3.2.2",
"eslint-loader": "^1.5.0",
"eslint-plugin-babel": "^3.3.0",
"eslint-plugin-flowtype": "^2.18.1",
"eslint-plugin-react": "^6.0.0",
- "express": "^4.14.0",
"flow-bin": "^0.32.0",
"jest": "^15.1.1",
- "lodash": "^4.15.0",
"node-sass": "^3.8.0",
"react": "^15.3.1",
- "react-css-modules": "^3.7.9",
"react-dom": "^15.3.1",
- "react-redux": "^4.4.5",
"sass-loader": "^4.0.0",
"style-loader": "^0.13.1",
"webpack": "^1.13.2",
- "webpack-dev-server": "^1.16.1"
+ "webpack-dev-server": "^1.16.1",
+ "react-motion": "^0.4.4"
}
}
diff --git a/src/components/Game.js b/src/components/Game.js
deleted file mode 100644
index f8499ae..0000000
--- a/src/components/Game.js
+++ /dev/null
@@ -1,34 +0,0 @@
-//@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) => (
-
- { props.shells.map(shell =>
-
props.shellClickHandler(shell.id) }
- >
- Hi! I'm a shell! (id { shell.id })
-
- )}
-
Play!
-
Shuffle!
-
{ props.output }
-
{ props.ballShellId }
-
-);
\ No newline at end of file
diff --git a/src/components/Game/game.scss b/src/components/Game/game.scss
new file mode 100644
index 0000000..8955a83
--- /dev/null
+++ b/src/components/Game/game.scss
@@ -0,0 +1,37 @@
+.board {
+ box-shadow: 0 0 3px 1px rgba(0,0,0,0.4);
+ background-color: #efefef;
+ margin: 0 auto 1rem;
+ text-align: center;
+ overflow: hidden;
+ height: 150px;
+ position: relative;
+}
+
+.play {
+ background-color: #295e80;
+ padding: 0.75rem 1rem;
+ color: #fff;
+ font-size: 1.2rem;
+ border: 1px solid #fff;
+ cursor: pointer;
+ box-shadow: 0 0 5px 0 rgba(0,0,0,0.7), inset 60px 0 50px -50px rgba(33, 199, 255, 0.48), inset -60px 0 50px -50px rgba(33, 199, 255, 0.48);
+ transition-property: box-shadow;
+ transition-duration: 350ms;
+
+ &:hover {
+ box-shadow: 0 0 3px 0 rgba(0,0,0,0.7), inset 75px 0 50px -50px rgba(33, 199, 255, 0.48), inset -75px 0 50px -50px rgba(33, 199, 255, 0.48);
+ transition-property: box-shadow;
+ transition-duration: 350ms;
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ }
+}
+
+.output {
+ border-top: 1px solid #295e80;
+ border-bottom: 1px solid #295e80;
+ padding: 1rem;
+}
\ No newline at end of file
diff --git a/src/components/Game/index.js b/src/components/Game/index.js
new file mode 100644
index 0000000..184bd19
--- /dev/null
+++ b/src/components/Game/index.js
@@ -0,0 +1,44 @@
+//@flow
+import React from 'react';
+import { Shell as ShellComponent } from '../Shell';
+import styles from './game.scss';
+
+type Shell = {
+ id: number,
+}
+
+type GameProps = {
+ shells: Shell[],
+ shellClickHandler: (id: number) => void,
+ startGameClickHandler: () => void,
+ output: string,
+ ballShellId: number,
+ inProgress: boolean,
+ shuffled: boolean,
+}
+
+type onClick = (event: Object) => void
+
+export const Game = (props: GameProps) => (
+
+
+ { props.shells.map((shell, idx) =>
+ props.shellClickHandler(shell.id) }
+ position={ 10 + (idx * 110) }
+ containsBall={ props.ballShellId === shell.id }
+ displayBall={ !props.shuffling && !props.shuffled }
+ />
+ )}
+
+
+ Play!
+
+
{ props.output }
+
+);
diff --git a/src/components/GameContainer.js b/src/components/GameContainer.js
index 384b284..2641b0b 100644
--- a/src/components/GameContainer.js
+++ b/src/components/GameContainer.js
@@ -14,16 +14,20 @@ import { Game } from './Game';
type GameContainerState = {
ballShellId: number,
shells: Shell[],
- inProgress: boolean,
output: string,
+ shuffleCount: number,
+ shuffled: boolean,
+ shuffling: boolean,
+ inProgress: boolean,
};
type GameContainerProps = {
numberOfShells: number,
+ shuffles: number,
};
type Shell = {
- position: number,
+ id: number,
};
//======================
@@ -32,6 +36,12 @@ type Shell = {
export class GameContainer extends Component {
state: GameContainerState;
props: GameContainerProps;
+ selectRandomShell: ?() => number;
+ _initializeGame: (numberOfShells: number) => void;
+ _startGame: () => void;
+ _setWinningShell: () => void;
+ _shellClickHandler: (shellId: number) => void;
+ _shuffleShells: () => Promise;
constructor() {
super();
@@ -39,7 +49,9 @@ export class GameContainer extends Component {
ballShellId: 0,
shells: [],
inProgress: false,
+ shuffleCount: 0,
shuffled: false,
+ shuffling: false,
output: `Click on 'Play' to begin!`,
};
this.selectRandomShell = null;
@@ -55,9 +67,16 @@ export class GameContainer extends Component {
}
_initializeGame(numberOfShells: number): void {
+ //This is designed to initialize the game whenever the component first mounts, or
+ //whenever a new set of options are passed in via props.
this.setState({
ballShellId: 0,
- shells: createShells(numberOfShells)
+ shells: createShells(numberOfShells),
+ output: `Click on 'Play' to begin!`,
+ shuffleCount: 0,
+ shuffling: false,
+ shuffled: false,
+ inProgress: false,
});
this.selectRandomShell = createRandomNumberFn(0, numberOfShells - 1);
}
@@ -66,8 +85,15 @@ export class GameContainer extends Component {
this._setWinningShell();
this.setState({
inProgress: true,
- output: `Click a shell to see if you guessed correctly!`
+ shuffleCount: 0,
+ output: 'Keep an eye on the ball!',
+ shuffling: false,
+ shuffled: false,
});
+ setTimeout(() => this._shuffleShells().then(() =>
+ this.setState({
+ output: 'Pick a shell!',
+ })), 1000);
}
_setWinningShell(): void {
@@ -77,10 +103,10 @@ export class GameContainer extends Component {
});
}
- _shellClickHandler(shellId: number) {
+ _shellClickHandler(shellId: number): void {
//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!`});
+ if (this.state.shuffling) return this.setState({ output: `Have patience, young padawan`});
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,
@@ -88,18 +114,37 @@ export class GameContainer extends Component {
});
}
- _shuffleShells() {
+ _shuffleShells(): Promise {
+ //Shuffle the shells in a time based loop (the animation functionality uses springs, so there's no strict timer
+ //on how long the animation plays for).
this.setState({
- shells: randomizeArrayOrder(this.state.shells),
- shuffled: true,
+ shuffling: true,
+ output: 'Shuffling...',
});
+
+ //Return a promise as this is an async operation
+ return new Promise(res => {
+ const shuffle = () => {
+ const shuffleCount = this.state.shuffleCount + 1;
+ this.setState({
+ shells: randomizeArrayOrder(this.state.shells),
+ shuffled: shuffleCount == this.props.shuffles,
+ shuffleCount,
+ shuffling: shuffleCount < this.props.shuffles,
+ });
+ if (this.state.shuffled) return res(true);
+ return setTimeout(shuffle, 1000);
+ };
+ setTimeout(shuffle, 1000);
+ });
+
}
componentWillMount() {
this._initializeGame(this.props.numberOfShells);
}
- componentWillReceiveProps(newProps: GameProps) {
+ componentWillReceiveProps(newProps: GameContainerProps) {
if(newProps.numberOfShells !== this.props.numberOfShells) {
this._initializeGame(newProps.numberOfShells);
}
@@ -112,9 +157,9 @@ export class GameContainer extends Component {
output={ this.state.output }
ballShellId={ this.state.ballShellId }
inProgress={ this.state.inProgress }
+ shuffling={ this.state.shuffling }
shuffled={ this.state.shuffled }
shellClickHandler={ this._shellClickHandler }
- shuffleShellsClickHandler={ this._shuffleShells }
startGameClickHandler={ this._startGame }
/>
);
diff --git a/src/components/Options/index.js b/src/components/Options/index.js
new file mode 100644
index 0000000..af2623a
--- /dev/null
+++ b/src/components/Options/index.js
@@ -0,0 +1,38 @@
+//@flow
+import React from 'react';
+import styles from './options.scss';
+
+type OptionsProps = {
+ numberOfShells: number,
+ shuffles: number,
+ onInputChange: (event: Object) => void,
+};
+
+export const Options = (props: OptionsProps) => (
+
+
+ Number of shells:
+
+ 3
+ 4
+ 5
+
+
+
+ Number of shuffles:
+
+ 3
+ 5
+ 7
+
+
+
+);
diff --git a/src/components/Options/options.scss b/src/components/Options/options.scss
new file mode 100644
index 0000000..76c817c
--- /dev/null
+++ b/src/components/Options/options.scss
@@ -0,0 +1,9 @@
+.input {
+
+ margin-bottom: 1rem;
+
+ select {
+ margin-left: 0.5rem;
+ font-size: 1.2rem;
+ }
+}
\ No newline at end of file
diff --git a/src/components/Shell/index.js b/src/components/Shell/index.js
new file mode 100644
index 0000000..965489a
--- /dev/null
+++ b/src/components/Shell/index.js
@@ -0,0 +1,31 @@
+//@flow
+import React from 'react';
+import styles from './shell.scss';
+import { Motion, spring } from 'react-motion';
+
+type ShellProps = {
+ onClick: () => void,
+ position: number,
+ containsBall: boolean,
+ displayBall: boolean,
+}
+
+export const Shell = (props: ShellProps) =>
+
+ { ({ x }) => (
+
+
+ { props.containsBall && props.displayBall &&
+
}
+
+
+ )}
+ ;
+
+
diff --git a/src/components/Shell/shell.scss b/src/components/Shell/shell.scss
new file mode 100644
index 0000000..e4de1d3
--- /dev/null
+++ b/src/components/Shell/shell.scss
@@ -0,0 +1,27 @@
+.shell {
+ background-color: #295e80;
+ box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.4), inset 0 60px 50px -50px rgba(33, 199, 255, 0.48), inset 0 -60px 50px -50px rgba(33, 199, 255, 0.48);
+ height: 100px;
+ width: 100px;
+ display: inline-block;
+ transition: 350ms;
+ cursor: pointer;
+ position: absolute;
+ top: 25px;
+
+ &:hover {
+ box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.4), inset 0 90px 50px -50px rgba(33, 199, 255, 0.48), inset 0 -90px 50px -50px rgba(33, 199, 255, 0.48);
+ transition: 350ms;
+ }
+}
+
+.ball {
+ background-color: #b00000;
+ height: 50px;
+ width: 50px;
+ border-radius: 25px;
+ box-shadow: 0 0 10px 3px rgba(0,0,0,0.4), inset 0 0 20px 2px rgba(255,255,255,0.4);
+ position: absolute;
+ top: 25px;
+ left: 25px
+}
\ No newline at end of file
diff --git a/src/components/ShellGameApp.js b/src/components/ShellGameApp.js
deleted file mode 100644
index a70dd3d..0000000
--- a/src/components/ShellGameApp.js
+++ /dev/null
@@ -1,26 +0,0 @@
-//@flow
-import React from 'react';
-import { GameContainer } from './GameContainer';
-
-type ShellGameAppProps = {
- numberOfShells: number,
- onInputChange: (event: Object) => {},
-};
-
-export const ShellGameApp = (props: ShellGameAppProps) => (
-
-
-
- Number of shells:
-
- 3
- 4
- 5
-
-
-
-);
diff --git a/src/components/ShellGameApp/index.js b/src/components/ShellGameApp/index.js
new file mode 100644
index 0000000..3248685
--- /dev/null
+++ b/src/components/ShellGameApp/index.js
@@ -0,0 +1,27 @@
+//@flow
+import React from 'react';
+import { GameContainer } from '../GameContainer';
+import { Options } from '../Options';
+import styles from './shellGameApp.scss';
+
+type ShellGameAppProps = {
+ numberOfShells: number,
+ shuffles: number,
+ onInputChange: (event: Object) => void,
+};
+
+export const ShellGameApp = (props: ShellGameAppProps) => (
+
+
The game of shells
+
+
+
Built using React, and React-motion for the shuffling animations.
+
+);
diff --git a/src/components/ShellGameApp/shellGameApp.scss b/src/components/ShellGameApp/shellGameApp.scss
new file mode 100644
index 0000000..b74c61a
--- /dev/null
+++ b/src/components/ShellGameApp/shellGameApp.scss
@@ -0,0 +1,13 @@
+.app {
+ font-family: 'Roboto', sans-serif;
+ text-align: center;
+ font-size: 1.2rem;
+ width: 700px;
+ margin: 5rem auto;
+ box-shadow: 0 0 3px 1px rgba(0,0,0,0.4);
+ padding: 1.5rem 0;
+}
+
+.info {
+ font-size: 0.9rem;
+}
diff --git a/src/components/ShellGameAppContainer.js b/src/components/ShellGameAppContainer.js
index 34016c2..69c8a9f 100644
--- a/src/components/ShellGameAppContainer.js
+++ b/src/components/ShellGameAppContainer.js
@@ -4,21 +4,23 @@ import { ShellGameApp } from './ShellGameApp';
type ShellGameAppContainerState = {
numberOfShells: number,
+ shuffles: number,
}
export class ShellGameAppContainer extends Component {
state: ShellGameAppContainerState;
+ _handleInputUpdate: (event: Object) => void;
constructor() {
super();
this.state = {
numberOfShells: 3,
+ shuffles: 5,
};
-
this._handleInputUpdate = this._handleInputUpdate.bind(this);
}
- _handleInputUpdate(e) {
+ _handleInputUpdate(e: Object): void {
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,
@@ -29,6 +31,7 @@ export class ShellGameAppContainer extends Component {
return (
)
diff --git a/src/components/index.js b/src/components/index.js
index 15544de..00757e2 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -2,10 +2,14 @@ import { Game } from './Game';
import { GameContainer } from './GameContainer';
import { ShellGameApp } from './ShellGameApp';
import { ShellGameAppContainer } from './ShellGameAppContainer';
+import { Shell } from './Shell';
+import { Options } from './Options';
export {
Game,
GameContainer,
ShellGameApp,
ShellGameAppContainer,
+ Shell,
+ Options,
}
diff --git a/src/utils/gameUtils.js b/src/utils/gameUtils.js
index 35f4991..2ddcf73 100644
--- a/src/utils/gameUtils.js
+++ b/src/utils/gameUtils.js
@@ -1,5 +1,7 @@
//@flow
-
+type Shell = {
+ id: number,
+};
//======================
// Utility functions
//======================
@@ -7,7 +9,7 @@ export function createRandomNumberFn(minimum: number, maximum: number): () => nu
return () => minimum + Math.floor(Math.random() * ((maximum-minimum) + 1));
}
-export function randomizeArrayOrder(array: array): array {
+export function randomizeArrayOrder(array: Shell[]): Shell[] {
let oldArray = [ ...array ];
let newArr = [];
@@ -20,9 +22,8 @@ export function randomizeArrayOrder(array: array): array {
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. */
+ in order to properly animate the shells later, a fixed id is desirable.
+ */
let newShellArr = [];
for (let i=0; i