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