├── src ├── main.ts └── main.spec.ts ├── public ├── style.css ├── index.html └── normilize.css ├── .nvmrc ├── .editorconfig ├── jest.config.js ├── tsconfig.json ├── README.md ├── package.json ├── webpack.config.js ├── .gitignore └── .eslintrc.js /src/main.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.16.0 2 | -------------------------------------------------------------------------------- /src/main.spec.ts: -------------------------------------------------------------------------------- 1 | describe('>>> Fake test', () => { 2 | it('should be true', () => { 3 | expect(true).toBe(true) 4 | }) 5 | }) 6 | 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The Game 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | moduleNameMapper: { 4 | '^@/(.*)$': '/src/$1' 5 | }, 6 | moduleFileExtensions: ['js', 'ts'], 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | setupFiles: [], 11 | transformIgnorePatterns: [ 12 | '/node_modules/' 13 | ], 14 | testPathIgnorePatterns: [ 15 | '__testUtils__' 16 | ], 17 | clearMocks: true, 18 | restoreMocks: true, 19 | testMatch: [ 20 | '**/*.spec.(js|ts)' 21 | ], 22 | roots: [ 23 | '/src' 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es6", 5 | "outDir": "./dist/", 6 | "moduleResolution": "node", 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "esnext", 10 | "esnext.asynciterable", 11 | "dom" 12 | ], 13 | "importHelpers": true, 14 | "esModuleInterop": true, 15 | "strictPropertyInitialization": false, 16 | "allowJs": true, 17 | "sourceMap": true, 18 | "strict": true, 19 | "baseUrl": "src", 20 | "paths": { 21 | "@/*": [ 22 | "./*" 23 | ] 24 | }, 25 | "types": [ 26 | "jest" 27 | ], 28 | }, 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building a Game With TypeScript 2 | > In this [series of tutorials](https://medium.com/@gregsolo/gamedev-patterns-and-algorithms-in-action-with-typescript-d29b913858e) we are discussing how to build a simple turn-based game from scratch using TypeScript, no external libraries, SOLID architecture, incremental approach and unit testing. 3 | 4 | ## Project setup 5 | - install Node.js (the version is specified in package.json) 6 | - if you have nvm installed: 7 | ``` 8 | nvm use 9 | ``` 10 | - Install dependencies 11 | ``` 12 | npm install 13 | ``` 14 | 15 | ## Compiles and hot-reloads for development 16 | ``` 17 | npm start 18 | ``` 19 | 20 | ## Run your unit tests 21 | ``` 22 | npm t 23 | ``` 24 | ## Lints and fixes files 25 | ``` 26 | npm run lint 27 | ``` 28 | 29 | ## Tech stack 30 | - TypeScript 31 | - Jest / ESLint 32 | 33 | ## Links 34 | [Complete tutorial](https://medium.com/@gregsolo/gamedev-patterns-and-algorithms-in-action-with-typescript-d29b913858e) 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tic-tac-toe-ts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "webpack-dev-server --config webpack.config.js", 8 | "test": "jest", 9 | "lint": "eslint src/**" 10 | }, 11 | "engines": { 12 | "node": "16.16.0", 13 | "npm": "8.11.0" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@babel/cli": "^7.18.6", 19 | "@babel/core": "^7.18.6", 20 | "@types/jest": "^28.1.4", 21 | "@typescript-eslint/eslint-plugin": "^5.30.5", 22 | "@typescript-eslint/parser": "^5.0.0", 23 | "babel-jest": "^28.1.2", 24 | "babel-loader": "^8.2.5", 25 | "babel-preset-env": "^1.7.0", 26 | "canvas": "^2.9.3", 27 | "clean-webpack-plugin": "^4.0.0", 28 | "eslint": "^8.19.0", 29 | "html-webpack-plugin": "^5.5.0", 30 | "jest": "^28.1.2", 31 | "jest-environment-jsdom": "^28.1.2", 32 | "pre-commit": "^1.2.2", 33 | "ts-jest": "^28.0.5", 34 | "ts-loader": "^9.3.1", 35 | "typescript": "^4.3.0", 36 | "webpack": "^5.73.0", 37 | "webpack-cli": "^4.10.0", 38 | "webpack-dev-server": "^4.9.3" 39 | }, 40 | "pre-commit": [ 41 | "lint", 42 | "test" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const path = require('path') 4 | const webpack = require('webpack') 5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 6 | const HtmlWebpackPlugin = require('html-webpack-plugin') 7 | 8 | module.exports = { 9 | entry: { 10 | app: `${path.join(__dirname, 'src')}/main.ts` 11 | }, 12 | output: { 13 | filename: '[name].[hash].js', 14 | path: path.resolve(__dirname, 'dist'), 15 | publicPath: '/' 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.ts/, 21 | use: 'ts-loader', 22 | exclude: /node_modules/, 23 | }, { 24 | test: /\.js$/, 25 | exclude: /(node_modules)/, 26 | use: { 27 | loader: 'babel-loader' 28 | } 29 | } 30 | ] 31 | }, 32 | mode: 'development', 33 | devtool: 'source-map', 34 | devServer: { 35 | hot: true, 36 | historyApiFallback: true 37 | }, 38 | resolve: { 39 | extensions: ['.ts', '.js'], 40 | alias: { 41 | '@': path.join(__dirname, 'src') 42 | } 43 | }, 44 | plugins: [ 45 | new webpack.HotModuleReplacementPlugin(), 46 | new CleanWebpackPlugin(), 47 | new HtmlWebpackPlugin({ 48 | template: `${path.join(__dirname, 'public')}/index.html`, 49 | scriptLoading: 'defer' 50 | }), 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # Mac OSX 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | 92 | // VS Code setting 93 | .vscode 94 | 95 | // Cypress 96 | cypress/screenshots 97 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | jest: true, 6 | es6: true 7 | }, 8 | parserOptions: { 9 | parser: '@typescript-eslint/parser' 10 | }, 11 | extends: [ 12 | 'eslint:recommended', 13 | 'plugin:@typescript-eslint/recommended' 14 | ], 15 | // add your custom rules here 16 | rules: { 17 | 'semi': [ 18 | 'error', 19 | 'never' 20 | ], 21 | 'quotes': [ 22 | 'error', 23 | 'single' 24 | ], 25 | '@typescript-eslint/explicit-function-return-type': [ 26 | 'error', 27 | { 28 | allowExpressions: false, 29 | allowTypedFunctionExpressions: true, 30 | allowHigherOrderFunctions: true, 31 | } 32 | ], 33 | '@typescript-eslint/naming-convention': [ 34 | 'error', 35 | { 36 | 'selector': 'interface', 37 | 'format': ['PascalCase'], 38 | 'custom': { 39 | 'regex': '^I[A-Z]', 40 | 'match': true 41 | } 42 | } 43 | ], 44 | 'no-unused-vars': 'off', // note you must disable the base rule as it can report incorrect errors 45 | '@typescript-eslint/no-unused-vars': [ 46 | 'error', 47 | { 48 | 'vars': 'all', 49 | 'args': 'none', 50 | 'ignoreRestSiblings': false 51 | } 52 | ], 53 | 'no-useless-constructor': 'off', 54 | '@typescript-eslint/no-useless-constructor': 'error', 55 | '@typescript-eslint/member-delimiter-style': [ 56 | 'error', 57 | { 58 | 'multiline': { 59 | 'delimiter': 'none', 60 | 'requireLast': false 61 | }, 62 | 'singleline': { 63 | 'delimiter': 'semi', 64 | 'requireLast': false 65 | } 66 | } 67 | ] 68 | }, 69 | overrides: [ 70 | { 71 | 'files': ['*.js'], 72 | rules: { 73 | '@typescript-eslint/explicit-function-return-type': 'off', 74 | } 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /public/normilize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } 350 | --------------------------------------------------------------------------------