diff --git a/README.md b/README.md
index 2c6db42..ae40356 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,8 @@
##### [Raytracer](raytracer/README.md)
+##### [React Tic-Tac-Toe](react-tic-tac-toe/README.md)
+
##### [Simple](simple/README.md)
##### [SystemJS](systemjs/README.md)
diff --git a/react-tic-tac-toe/.gitignore b/react-tic-tac-toe/.gitignore
new file mode 100644
index 0000000..1ca8433
--- /dev/null
+++ b/react-tic-tac-toe/.gitignore
@@ -0,0 +1,8 @@
+# compiled and bundled source
+dist
+
+# Node modules
+node_modules
+
+# type definitions
+typings
diff --git a/react-tic-tac-toe/README.md b/react-tic-tac-toe/README.md
new file mode 100644
index 0000000..467af11
--- /dev/null
+++ b/react-tic-tac-toe/README.md
@@ -0,0 +1,17 @@
+# React Tic Tac Toe
+
+A game example built using [TypeScript](https://github.com/Microsoft/TypeScript) and [React](https://github.com/facebook/react), following guidelines from [react-webpack guide](https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/quick-start/react-webpack.md) from TypeScript handbook.
+
+## Build
+
+```
+npm install -g typescript webpack typings
+npm install
+npm link typescript
+typings install
+webpack
+```
+
+## Run
+
+Open ```index.html```.
diff --git a/react-tic-tac-toe/css/style.css b/react-tic-tac-toe/css/style.css
new file mode 100644
index 0000000..70c73cc
--- /dev/null
+++ b/react-tic-tac-toe/css/style.css
@@ -0,0 +1,118 @@
+html {
+ box-sizing: border-box;
+}
+* {
+ box-sizing: inherit;
+}
+
+.app {
+ font-family: 'Open Sans', sans-serif;
+ margin: 100px;
+ width: 500px;
+ margin-left: auto;
+ margin-right: auto;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.board {
+ position: relative;
+ width: 500px;
+ height:500px;
+}
+
+.cell {
+ float: left;
+ width: 33.3333%;
+ height: 33.3333%;
+ line-height: 166.67px;
+ color: black;
+ font-size: 90pt;
+ text-align: center;
+ border-color: orangered;
+ border-width: 3px;
+}
+.cell.top {
+ border-bottom-style:solid;
+}
+.cell.bottom {
+ border-top-style:solid;
+}
+.cell.left {
+ border-right-style:solid;
+}
+.cell.right {
+ border-left-style:solid;
+}
+
+.X{
+ animation-name: appear;
+ animation-duration: .3s;
+}
+.O{
+ animation-name: appear;
+ animation-duration: .3s;
+ animation-delay:.3s;
+ animation-fill-mode: forwards;
+ opacity: 0;
+}
+@keyframes appear {
+ from { font-size: 90pt; opacity: 0;}
+ to { font-size: 100pt; opacity: 1;}
+}
+
+.description{
+ cursor:pointer;
+ font-size:25px;
+ font-weight:bold;
+ padding:15px 0px;
+ position: relative;
+ display: inline-block;
+ width: 200px;
+ text-align: center;
+ margin-top: 30px;
+ margin-right: -35px;
+}
+.t1{
+ margin-left: 60px;
+}
+.t2{
+ margin-right: 60px;
+}
+
+.gameStateBar {
+ text-align: center;
+ font-size: 60px;
+ font-weight: bold;
+ height: 60px;
+}
+
+.restartBtn {
+ box-shadow: 3px 3px 9px 2px #54a3f7;
+ background-color:#007dc1;
+ border-radius:28px;
+ border:1px solid #124d77;
+ cursor:pointer;
+ color:#ffffff;
+ font-size:25px;
+ font-weight:bold;
+ padding:15px 36px;
+ text-decoration:none;
+ text-shadow:0px 0px 7px #154682;
+ position: relative;
+ display: block;
+ margin: 40px auto;
+ width: 160px;
+ text-align: center;
+}
+.restartBtn:hover {
+ background-color:#0061a7;
+}
+.restartBtn:active {
+ position:relative;
+ top:1px;
+}
diff --git a/react-tic-tac-toe/index.html b/react-tic-tac-toe/index.html
new file mode 100644
index 0000000..b36afeb
--- /dev/null
+++ b/react-tic-tac-toe/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ TicTacToe with TypeScript and React
+
+
+
+
+
+
+
+
+
diff --git a/react-tic-tac-toe/layout.txt b/react-tic-tac-toe/layout.txt
new file mode 100644
index 0000000..9580be2
--- /dev/null
+++ b/react-tic-tac-toe/layout.txt
@@ -0,0 +1,18 @@
+TicTacToe /
+ |---- css/ // css style sheets
+ |---- dist/ // folder for typescript-compiled and webpack-bundled js files
+ |---- node_modules/ // dependent node modules
+ |---- src/ // .ts and .tsx source files
+ |---- app.tsx // the App React component
+ |---- board.tsx // the TicTacToe Board React component
+ |---- constants.ts // some shared constants and types
+ |---- gameStateBar.tsx // GameStatusBar React component
+ |---- restartBtn.tsx // RestartBtn React component
+ |---- typings/ // type definition .d.ts files
+ |---- index.html // web page for our app
+ |---- layout.html // project structure
+ |---- package.json // node package configuration file
+ |---- README.md // RAEDME file
+ |---- tsconfig.json // typescript configuration file
+ |---- typings.json // typings configuration file
+ |---- webpack.config.js // webpack configuration file
diff --git a/react-tic-tac-toe/package.json b/react-tic-tac-toe/package.json
new file mode 100644
index 0000000..1d15aa0
--- /dev/null
+++ b/react-tic-tac-toe/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "TicTacToe",
+ "version": "1.0.0",
+ "description": "Tic Tac Toe built with TypeScript and React",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "Limin Zhu",
+ "license": "ISC",
+ "dependencies": {
+ "react": "^0.14.7",
+ "react-dom": "^0.14.7"
+ },
+ "devDependencies": {
+ "source-map-loader": "^0.1.5",
+ "ts-loader": "^0.8.1"
+ }
+}
diff --git a/react-tic-tac-toe/src/app.tsx b/react-tic-tac-toe/src/app.tsx
new file mode 100644
index 0000000..d86ee83
--- /dev/null
+++ b/react-tic-tac-toe/src/app.tsx
@@ -0,0 +1,26 @@
+import * as React from "react";
+import * as ReactDOM from "react-dom";
+import { Board } from "./Board";
+import { RestartBtn } from "./RestartBtn";
+import { GameStateBar } from "./GameStateBar";
+import { GameState } from "./constants";
+
+class App extends React.Component {
+ render() {
+ return (
+
+
+
+ Player(X)
+ Computer(O)
+
+
+
+
+ )
+ }
+}
+
+ReactDOM.render(
+ , document.getElementById("content")
+);
diff --git a/react-tic-tac-toe/src/board.tsx b/react-tic-tac-toe/src/board.tsx
new file mode 100644
index 0000000..35e4f69
--- /dev/null
+++ b/react-tic-tac-toe/src/board.tsx
@@ -0,0 +1,193 @@
+import * as React from "react";
+import { CellValue, GameState, playerCell, aiCell } from "./constants";
+
+interface BoardState {
+ cells: CellValue[];
+ gameState: GameState;
+}
+
+export class Board extends React.Component {
+
+ constructor(props: void) {
+ super(props);
+ this.state = this.getInitState();
+ }
+
+ private getInitState(): BoardState {
+ let cells = Array.apply(null, Array(9)).map(() => "");
+ return {cells: cells, gameState: ""}
+ }
+
+ private resetState(): void {
+ this.setState(this.getInitState());
+ }
+
+ componentDidMount() {
+ window.addEventListener("restart", () => this.resetState());
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("restart", () => this.resetState());
+ }
+
+ // Fire a global event notifying GameState changes
+ private handleGameStateChange(newState: GameState) {
+ var event = new CustomEvent("gameStateChange", { "detail": this.state.gameState });
+ event.initEvent("gameStateChange", false, true);
+ window.dispatchEvent(event);
+ }
+
+ // check the game state - use the latest move
+ private checkGameState(cells: CellValue[], latestPos: number, latestVal: CellValue): GameState {
+ if (this.state.gameState !== "") {
+ return this.state.gameState;
+ }
+
+ // check row
+ let result = this.check3Cells(cells, 3 * Math.floor(latestPos / 3),
+ 3 * Math.floor(latestPos / 3) + 1, 3 * Math.floor(latestPos/3) + 2);
+ if (result) {
+ return result;
+ }
+
+ // check col
+ result = this.check3Cells(cells, latestPos % 3, latestPos % 3 + 3, latestPos % 3 + 6);
+ if (result) {
+ return result;
+ }
+
+ // check diag
+ result = this.check3Cells(cells, 0, 4, 8);
+ if (result) {
+ return result;
+ }
+ result = this.check3Cells(cells, 2, 4, 6);
+ if (result) {
+ return result;
+ }
+
+ // check draw - if all cells are filled
+ if (this.findAllEmptyCells(cells).length === 0) {
+ return "Draw";
+ }
+
+ return "";
+ }
+
+ // check if 3 cells have same non-empty val - return the winner state; otherwise undefined
+ private check3Cells(cells: CellValue[], pos0: number, pos1: number, pos2: number): GameState {
+ if (cells[pos0] === cells[pos1] &&
+ cells[pos1] === cells[pos2] &&
+ cells[pos0] !== "") {
+ if (cells[pos0] === "X") {
+ return "X Wins!";
+ }
+ return "O Wins!";
+ }
+ else {
+ return undefined;
+ }
+ }
+
+ // list all empty cell positions
+ private findAllEmptyCells(cells : CellValue[]): number[] {
+ return cells.map((v, i) => {
+ if (v === "") {
+ return i;
+ }
+ else {
+ return undefined;
+ }
+ }).filter(v => { return v !== undefined });
+ }
+
+ // make a move
+ private move(pos: number, val: CellValue, callback?: () => void): void {
+ if (this.state.gameState === "" &&
+ this.state.cells[pos] === "") {
+ let newCells = this.state.cells.slice();
+ newCells[pos] = val;
+ let oldState = this.state.gameState;
+ this.setState({cells: newCells, gameState: this.checkGameState(newCells, pos, val)}, () => {
+ if (this.state.gameState !== oldState) {
+ this.handleGameStateChange(this.state.gameState);
+ }
+ if (callback) {
+ callback.call(this);
+ }
+ });
+ }
+ }
+
+ // handle a new move from player
+ private handleNewPlayerMove(pos: number): void {
+ this.move(pos, playerCell, () => {
+ // AI make a random move following player's move
+ let emptyCells = this.findAllEmptyCells(this.state.cells);
+ let pos = emptyCells[Math.floor(Math.random() * emptyCells.length)];
+ this.move(pos, aiCell);
+ });
+ }
+
+ render() {
+ var cells = this.state.cells.map((v, i) => {
+ return (
+ this.handleNewPlayerMove(i)} />
+ )
+ } );
+
+ return (
+
+ {cells}
+
+ )
+ }
+}
+
+interface CellProps extends React.Props {
+ pos: number;
+ val: CellValue;
+ handleMove: () => void;
+}
+
+class Cell extends React.Component {
+
+ // position of cell to className
+ private posToClassName(pos: number): string {
+ let className = "cell";
+ switch (Math.floor(pos / 3)) {
+ case 0:
+ className += " top";
+ break;
+ case 2:
+ className += " bottom";
+ break;
+ default: break;
+ }
+ switch (pos % 3) {
+ case 0:
+ className += " left";
+ break;
+ case 2:
+ className += " right";
+ break;
+ default:
+ break;
+ }
+ return className;
+ }
+
+ private handleClick(e: React.MouseEvent) {
+ this.props.handleMove();
+ }
+
+ render() {
+ let name = this.props.val;
+ if (this.props.val === "") {
+ name = "";
+ }
+ return this.handleClick(e)}>
+ {this.props.val}
+
+ }
+}
diff --git a/react-tic-tac-toe/src/constants.ts b/react-tic-tac-toe/src/constants.ts
new file mode 100644
index 0000000..4dcf790
--- /dev/null
+++ b/react-tic-tac-toe/src/constants.ts
@@ -0,0 +1,4 @@
+export type GameState = "" | "X Wins!" | "O Wins!" | "Draw";
+export type CellValue = "" | "X" | "O";
+export const playerCell: CellValue = "X";
+export const aiCell: CellValue = "O";
diff --git a/react-tic-tac-toe/src/gameStateBar.tsx b/react-tic-tac-toe/src/gameStateBar.tsx
new file mode 100644
index 0000000..1b8a541
--- /dev/null
+++ b/react-tic-tac-toe/src/gameStateBar.tsx
@@ -0,0 +1,38 @@
+import * as React from "react";
+import { GameState } from "./constants";
+
+interface GameStateBarState {
+ gameState: GameState;
+}
+
+export class GameStateBar extends React.Component {
+
+ constructor(props: void) {
+ super(props);
+ this.state = {gameState: ""};
+ }
+
+ private handleGameStateChange(e: CustomEvent) {
+ this.setState({gameState: e.detail});
+ }
+
+ private handleRestart(e: Event) {
+ this.setState({gameState: ""});
+ }
+
+ componentDidMount() {
+ window.addEventListener("gameStateChange", (e: CustomEvent) => this.handleGameStateChange(e));
+ window.addEventListener("restart", e => this.handleRestart(e));
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener("gameStateChange", (e: CustomEvent) => this.handleGameStateChange(e));
+ window.removeEventListener("restart", e => this.handleRestart(e));
+ }
+
+ render() {
+ return (
+ {this.state.gameState}
+ )
+ }
+}
diff --git a/react-tic-tac-toe/src/restartBtn.tsx b/react-tic-tac-toe/src/restartBtn.tsx
new file mode 100644
index 0000000..d41726f
--- /dev/null
+++ b/react-tic-tac-toe/src/restartBtn.tsx
@@ -0,0 +1,17 @@
+import * as React from "react";
+
+export class RestartBtn extends React.Component {
+
+ // Fire a global event notifying restart of game
+ private handleClick(e: React.MouseEvent) {
+ var event = document.createEvent("Event");
+ event.initEvent("restart", false, true);
+ window.dispatchEvent(event);
+ }
+
+ render() {
+ return this.handleClick(e)}>
+ Restart
+ ;
+ }
+}
diff --git a/react-tic-tac-toe/tsconfig.json b/react-tic-tac-toe/tsconfig.json
new file mode 100644
index 0000000..15833b8
--- /dev/null
+++ b/react-tic-tac-toe/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "jsx": "react",
+ "outDir": "./dist",
+ "sourceMap": true,
+ "noImplicitAny": true,
+ "module": "commonjs",
+ "target": "es5"
+ },
+ "exclude": [
+ "node_modules"
+ ],
+ "files": [
+ "./typings/main.d.ts",
+ "./src/app.tsx",
+ "./src/board.tsx",
+ "./src/constants.ts",
+ "./src/gameStateBar.tsx",
+ "./src/restartBtn.tsx"
+ ]
+}
diff --git a/react-tic-tac-toe/typings.json b/react-tic-tac-toe/typings.json
new file mode 100644
index 0000000..d6e933f
--- /dev/null
+++ b/react-tic-tac-toe/typings.json
@@ -0,0 +1,6 @@
+{
+ "ambientDependencies": {
+ "react": "registry:dt/react#0.14.0+20160319053454",
+ "react-dom": "registry:dt/react-dom#0.14.0+20160316155526"
+ }
+}
diff --git a/react-tic-tac-toe/webpack.config.js b/react-tic-tac-toe/webpack.config.js
new file mode 100644
index 0000000..a30c095
--- /dev/null
+++ b/react-tic-tac-toe/webpack.config.js
@@ -0,0 +1,31 @@
+module.exports = {
+ entry: "./src/app.tsx",
+ output: {
+ filename: "./dist/bundle.js",
+ },
+
+ // Enable sourcemaps for debugging webpack's output.
+ devtool: "source-map",
+
+ resolve: {
+ // Add '.ts' and '.tsx' as resolvable extensions.
+ extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"]
+ },
+
+ module: {
+ loaders: [
+ // All files with a '.ts' or '.tsx' extension will be handled by 'ts-loader'.
+ { test: /\.tsx?$/, loader: "ts-loader" }
+ ],
+
+ preLoaders: [
+ // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
+ { test: /\.js$/, loader: "source-map-loader" }
+ ]
+ },
+
+ externals: {
+ "react": "React",
+ "react-dom": "ReactDOM",
+ }
+};
\ No newline at end of file
|