├── .gitignore
├── Procfile
├── README.md
├── assets
├── tic-tac-toe-js-data-flow.png
└── tic-tac-toe.png
├── css
└── scss
│ ├── app
│ └── _app.scss
│ ├── globals
│ ├── _defaults.scss
│ ├── _helpers.scss
│ ├── _mixins.scss
│ ├── _print.scss
│ ├── _reset.scss
│ └── _variables.scss
│ └── style.scss
├── gulpfile.js
├── index.js
├── package.json
├── public
├── css
│ └── style.css
├── favicon-o.ico
├── favicon.ico
├── icons
│ ├── tic-tac-toe-js-icon-114.png
│ ├── tic-tac-toe-js-icon-120.png
│ ├── tic-tac-toe-js-icon-144.png
│ ├── tic-tac-toe-js-icon-152.png
│ ├── tic-tac-toe-js-icon-180.png
│ ├── tic-tac-toe-js-icon-72.png
│ ├── tic-tac-toe-js-icon-72@2x.png
│ ├── tic-tac-toe-js-icon-76.png
│ ├── tic-tac-toe-js-icon-76@2x.png
│ ├── tic-tac-toe-js-icon.png
│ └── tic-tac-toe-js-icon@2x.png
├── img
│ ├── startup-1242x2148.png
│ ├── startup-640x1096.png
│ └── startup-750x1294.png
├── index.html
└── js
│ └── app.js
└── src
├── actions.js
├── fiveicon-view.js
├── game.js
├── grid-view.js
├── initializer.js
├── middlewares
├── define-winner.js
└── logger.js
├── score-view.js
├── store.js
├── utils.js
└── winner-service.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled source #
2 | ###################
3 | *.sass-cache
4 | *.com
5 | *.class
6 | *.dll
7 | *.exe
8 | *.o
9 | *.so
10 |
11 | # Packages #
12 | ############
13 | # it's better to unpack these files and commit the raw source
14 | # git has its own built in compression methods
15 | *.7z
16 | *.dmg
17 | *.gz
18 | *.iso
19 | *.jar
20 | *.rar
21 | *.tar
22 | *.zip
23 |
24 | # Logs and databases #
25 | ######################
26 | *.log
27 | *.sql
28 | *.sqlite
29 |
30 | # OS generated files #
31 | ######################
32 | .DS_Store
33 | npm-debug.log
34 | .DS_Store?
35 | ._*
36 | .Spotlight-V100
37 | .Trashes
38 | # Icon?
39 | ehthumbs.db
40 | Thumbs.db
41 | node_modules
42 | /*.env
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node index.js
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tic-Tac-Toe.js
2 |
3 | Tic-Tac-Toe game written in vanilla javascript using redux-like approach.
4 |
5 | #### [Medium article](https://medium.com/@ramonvictor/tic-tac-toe-js-redux-pattern-in-plain-javascript-fffe37f7c47a) / [Github.io page](http://ramonvictor.github.io/tic-tac-toe-js/) / [Play the game](https://rocky-ocean-52527.herokuapp.com/)
6 |
7 |
8 |
9 | ## How the game applies Redux pattern?
10 |
11 | It uses the unidirectional data flow:
12 |
13 |
14 |
15 | ### The key principles
16 |
17 | **1. Single source of truth**
18 |
19 | One single [store.js](https://github.com/ramonvictor/tic-tac-toe-js/blob/master/src/store.js):
20 |
21 | ```javascript
22 | function Store() {
23 | this.state = {};
24 | this.state = this.update(this.state, {});
25 | // `this.update()` will return the initial state:
26 | // ----------------------------------------------
27 | // {
28 | // grid: ['', '', '', '', '', '', '', '', ''],
29 | // turn: 'x',
30 | // score: { x: 0, o: 0 },
31 | // winnerSequence: [],
32 | // turnCounter: 0,
33 | // player: ''
34 | // }
35 | }
36 | ```
37 |
38 | **2. State is read-only**
39 |
40 | [Game.js](https://github.com/ramonvictor/tic-tac-toe-js/blob/master/src/game.js) dispatches actions whenever needed:
41 |
42 | ```javascript
43 | this.$table.addEventListener('click', function(event) {
44 | var state = store.getState();
45 | // [Prevent dispatch under certain conditions]
46 | // Otherwise, trigger `SET_X` or `SET_O`
47 | store.dispatch({
48 | type: state.turn === 'x' ? 'SET_X' : 'SET_O',
49 | index: parseInt(index, 10)
50 | });
51 | });
52 | ```
53 |
54 | **3. Changes are made with pure functions**
55 |
56 | [Store.js](https://github.com/ramonvictor/tic-tac-toe-js/blob/master/src/store.js): reducers receive actions and return new state.
57 |
58 | ```javascript
59 | // Reducer (pure function)
60 | function updatePlayer(player, action) {
61 | switch (action.type) {
62 | case 'PICK_SIDE':
63 | return action.side;
64 | default:
65 | return player || '';
66 | }
67 | }
68 |
69 | // Call reducer on Store.update()
70 | Store.prototype.update = function(state, action) {
71 | return {
72 | player: updatePlayer(state.player, action)
73 | // [...other cool stuff here]
74 | };
75 | };
76 | ```
77 |
78 | **4. After update, UI can render latest state**
79 |
80 | [Game.js](https://github.com/ramonvictor/tic-tac-toe-js/blob/master/src/game.js) handles UI changes:
81 |
82 | ```javascript
83 | var store = require('./store');
84 | var gridView = require('./grid-view');
85 |
86 | TicTacToe.prototype.eventListeners = function() {
87 | store.subscribe(this.render.bind(this));
88 | };
89 |
90 | TicTacToe.prototype.render = function(prevState, state) {
91 | // You can even check whether new state is different
92 | if (prevState.grid !== state.grid) {
93 | this.gridView.render('grid', state.grid);
94 | }
95 | };
96 | ```
97 |
98 | Further details about implementation you can [find on this page](http://ramonvictor.github.io/tic-tac-toe-js/).
99 |
100 | ## Browser support
101 |
102 | The game has been tested in the following platforms:
103 |
104 | Latest | Latest | 10+ | Latest |
105 | --- | --- | --- | --- |
106 |  |  |  | 
107 |
108 |
109 | ## Development stack
110 | - Server: NodeJS / Express / Socket.io
111 | - Client: VanillaJS / Redux
112 | - Tools: Gulp / Webpack / Sass / Heroku
113 |
114 | ## Did you find a bug?
115 |
116 | Please report on the [issues tab](https://github.com/ramonvictor/tic-tac-toe-js/issues).
117 |
--------------------------------------------------------------------------------
/assets/tic-tac-toe-js-data-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/assets/tic-tac-toe-js-data-flow.png
--------------------------------------------------------------------------------
/assets/tic-tac-toe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/assets/tic-tac-toe.png
--------------------------------------------------------------------------------
/css/scss/app/_app.scss:
--------------------------------------------------------------------------------
1 | body{
2 | font-size: 14px;
3 | font-family: Helvetica, Arial, 'Sans serif';
4 | color: #555;
5 | background: #38485f;
6 | }
7 |
8 | .container{
9 | width: 500px;
10 | margin: 0 auto;
11 | }
12 |
13 | .header{
14 | font-size: 18px;
15 | }
16 |
17 | .footer{
18 | padding: 30px 0;
19 | color: #96a8c1;
20 | text-align: center;
21 | position: relative;
22 |
23 | a {
24 | color: #96a8c1;
25 | text-decoration: none;
26 | transition: all .2s linear;
27 | -webkit-tap-highlight-color: rgba(0,0,0,0);
28 |
29 | &:hover{
30 | color: #fff;
31 | }
32 | }
33 | }
34 |
35 | .group {
36 | zoom: 1;
37 | &:before, &:after {
38 | content: "";
39 | display: table;
40 | }
41 | &:after {
42 | clear: both;
43 | }
44 | }
45 |
46 | .room-id-label{
47 | border: 1px solid #324156;
48 | border-right: 0;
49 | padding: 12px 13px 11px;
50 | border-radius: 3px 0 0 3px;
51 | text-transform: uppercase;
52 | position: relative;
53 | top: -1px;
54 | -webkit-tap-highlight-color: rgba(0,0,0,0);
55 | }
56 |
57 | .room-id{
58 | max-width: 100px;
59 | font-size: 16px;
60 | background: #324156;
61 | border: none;
62 | border-radius: 0 3px 3px 0;
63 | margin-bottom: 25px;
64 | padding: 12px 18px 11px;
65 | color: #96A8C1;
66 | outline: none;
67 | -webkit-tap-highlight-color: rgba(0,0,0,0);
68 | transition: all .2s linear;
69 |
70 | &:focus{
71 | background: darken(#324156, 3%);
72 | }
73 | }
74 |
75 | .refresh-icon{
76 | font-size: 16px;
77 | color: #96A8C1;
78 | padding: 9px 14px;
79 | text-align: center;
80 | border: 1px solid #324156;
81 | border-radius: 3px;
82 | cursor: pointer;
83 | background: none;
84 | outline: none;
85 | transition: all .2s linear;
86 | -webkit-tap-highlight-color: rgba(0,0,0,0);
87 |
88 | &:hover{
89 | color: #aebdd3;
90 | border-color: #aebdd3;
91 | }
92 | }
93 |
94 | .tic-tac-toe-table{
95 | width: 500px;
96 | height: 500px;
97 | }
98 |
99 | .tic-tac-toe-table-cell{
100 | border: 3px solid #283344;
101 | width: 33.3%;
102 | height: 33.3%;
103 | position: relative;
104 | }
105 |
106 | .turn-display{
107 | overflow: hidden;
108 | white-space: nowrap;
109 |
110 | & > li {
111 | padding: 40px 15px;
112 | width: 50%;
113 | float: left;
114 | position: relative;
115 | box-sizing: border-box;
116 | }
117 |
118 | .score{
119 | color: #95a7c1;
120 | display: inline-block;
121 | background: #273342;
122 | border-radius: 14px;
123 | min-width: 40px;
124 | padding: 0 10px;
125 | text-align: center;
126 | position: absolute;
127 | top: 50%;
128 | border-left: 1px solid darken(#273342, 10%);
129 | border-top: 1px solid darken(#273342, 10%);
130 | margin-top: -13px;
131 | height: 28px;
132 | line-height: 26px;
133 | }
134 | }
135 |
136 | .is-x .score {
137 | right: 90px;
138 | }
139 |
140 | .is-o .score {
141 | left: 90px;
142 | }
143 |
144 |
145 | .turn-player {
146 | width: 60px;
147 | height: 60px;
148 | border-radius: 50%;
149 | background: #54667f;
150 | position: relative;
151 | box-shadow: 3px 3px 0 0 #344359;
152 |
153 | .is-o &{
154 | float: left;
155 | }
156 |
157 | .is-x &{
158 | float: right;
159 | }
160 |
161 | & > .o{
162 | border-width: 5px;
163 | width: 30px;
164 | height: 30px;
165 | margin: -15px 0 0 -15px;
166 | position: absolute;
167 | left: 50%;
168 | top: 50%;
169 | z-index: 2;
170 | }
171 |
172 | & > .x{
173 | margin: 0;
174 | z-index: 2;
175 |
176 | &:before,
177 | &:after{
178 | width: 5px;
179 | height: 30px;
180 | top: 15px;
181 | left: 34px;
182 | }
183 | }
184 |
185 | .is-selected &:before {
186 | content: '';
187 | position: absolute;
188 | top: 0;
189 | left: 0;
190 | width: 60px;
191 | height: 60px;
192 | content: '';
193 | border-radius: 50%;
194 | border: 3px solid transparent;
195 | border-top-color: rgba(255,255,255,.3);
196 | border-bottom-color: rgba(255,255,255,.3);
197 | animation: spinner 1s ease infinite;
198 | -webkit-animation: spinner 1s ease infinite;
199 | }
200 |
201 | }
202 |
203 |
204 | .x{
205 | &:before, &:after{
206 | content: '';
207 | display: block;
208 | width: 15px;
209 | height: 110px;
210 | background: #fff;
211 | border-radius: 2px;
212 | position: absolute;
213 | left: 50%;
214 | top: 24px;
215 | margin-left: -7px;
216 | }
217 |
218 | &:before {
219 | -webkit-transform: rotate(45deg);
220 | -moz-transform: rotate(45deg);
221 | transform: rotate(45deg);
222 | }
223 |
224 | &:after{
225 | -webkit-transform: rotate(-45deg);
226 | -moz-transform: rotate(-45deg);
227 | transform: rotate(-45deg);
228 | }
229 |
230 | &.is-winner-cell:after,
231 | &.is-winner-cell:before{
232 | background: #5bd1ab;
233 | }
234 | }
235 |
236 | .is-winner-cell {
237 | animation-name: shakeme;
238 | animation-duration: 0.8s;
239 | transform-origin: 50% 50%;
240 | animation-iteration-count: infinite;
241 | animation-timing-function: linear;
242 | -webkit-animation-name: shakeme;
243 | -webkit-animation-duration: 0.8s;
244 | -webkit-transform-origin: 50% 50%;
245 | -webkit-animation-iteration-count: infinite;
246 | -webkit-animation-timing-function: linear;
247 | }
248 |
249 | .o{
250 | width: 95px;
251 | height: 95px;
252 | border-radius: 50%;
253 | border: 15px solid #fff;
254 | position: absolute;
255 | left: 50%;
256 | top: 50%;
257 | margin: -47px 0 0 -47px;
258 |
259 | &.is-winner-cell{
260 | border-color: #5bd1ab;
261 | }
262 | }
263 |
264 |
265 | .pop-over{
266 | width: 70%;
267 | background: #fff;
268 | border-radius: 3px;
269 | position: absolute;
270 | left: 0;
271 | bottom: 125px;
272 | margin-left: 15%;
273 | box-sizing: border-box;
274 | padding: 20px 25px;
275 | color: #666d79;
276 | z-index: 5;
277 | opacity: 1;
278 | transition: all .2s linear;
279 | box-shadow: 3px 3px 0 0 rgba(0,0,0,.1);
280 | display: none;
281 |
282 | p {
283 | line-height: 150%;
284 | }
285 |
286 | &:before{
287 | content: '';
288 | display: block;
289 | width: 0;
290 | height: 0;
291 | border: 8px solid transparent;
292 | border-top-color: #fff;
293 | position: absolute;
294 | bottom: -16px;
295 | left: 50%;
296 | margin-left: -4px;
297 | }
298 |
299 | &.hide {
300 | opacity: 0;
301 | }
302 | }
303 |
304 | .pop-over-close{
305 | position: absolute;
306 | top: 0;
307 | right: 0;
308 | width: 30px;
309 | display: block;
310 | height: 30px;
311 | line-height: 30px;
312 | text-align: center;
313 | cursor: pointer;
314 | font-size: 18px;
315 | opacity: 1;
316 | }
317 |
318 |
319 | // Mobile
320 | // --------
321 |
322 | @media only screen and (max-device-width: 499px) {
323 | .container {
324 | width: 100%;
325 | margin: 0;
326 | padding: 0 25px;
327 | box-sizing: border-box;
328 | }
329 |
330 | .tic-tac-toe-table{
331 | width: 100%;
332 | height: auto;
333 | -webkit-tap-highlight-color: rgba(0,0,0,0);
334 | }
335 |
336 | .tic-tac-toe-table-cell{
337 | position: relative;
338 |
339 | &:after{
340 | content: '';
341 | display: block;
342 | position: absolute;
343 | top: 0;
344 | right: 0;
345 | bottom: 0;
346 | left: 0;
347 | }
348 |
349 | &:before{
350 | content: '';
351 | display: block;
352 | padding-top: 100%;
353 | width: 1px;
354 | float: left;
355 | }
356 |
357 | .x{
358 | width: 60px;
359 | height: 60px;
360 | position: absolute;
361 | left: 50%;
362 | top: 50%;
363 | margin: -30px 0 0 -30px;
364 |
365 | &:before, &:after{
366 | width: 10px;
367 | height: 65px;
368 | top: -3px;
369 | left: 25px;
370 | margin: 0;
371 | }
372 | }
373 |
374 | .o{
375 | width: 60px;
376 | height: 60px;
377 | border-width: 10px;
378 | position: absolute;
379 | left: 50%;
380 | top: 50%;
381 | margin: -30px 0 0 -30px;
382 | }
383 | }
384 |
385 | .pop-over{
386 | width: 90%;
387 | margin-left: 5%;
388 | }
389 | }
390 |
391 |
392 | // Animations
393 | // -----------
394 |
395 | @-webkit-keyframes shakeme {
396 | 0% { -webkit-transform: translate(2px, 1px) rotate(0deg); }
397 | 10% { -webkit-transform: translate(-1px, -2px) rotate(-1deg); }
398 | 20% { -webkit-transform: translate(-3px, 0px) rotate(1deg); }
399 | 30% { -webkit-transform: translate(0px, 2px) rotate(0deg); }
400 | 40% { -webkit-transform: translate(1px, -1px) rotate(1deg); }
401 | 50% { -webkit-transform: translate(-1px, 2px) rotate(-1deg); }
402 | 60% { -webkit-transform: translate(-3px, 1px) rotate(0deg); }
403 | 70% { -webkit-transform: translate(2px, 1px) rotate(-1deg); }
404 | 80% { -webkit-transform: translate(-1px, -1px) rotate(1deg); }
405 | 90% { -webkit-transform: translate(2px, 2px) rotate(0deg); }
406 | 100% { -webkit-transform: translate(1px, -2px) rotate(-1deg); }
407 | }
408 |
409 | @keyframes shakeme {
410 | 0% { -webkit-transform: translate(2px, 1px) rotate(0deg); }
411 | 10% { -webkit-transform: translate(-1px, -2px) rotate(-1deg); }
412 | 20% { -webkit-transform: translate(-3px, 0px) rotate(1deg); }
413 | 30% { -webkit-transform: translate(0px, 2px) rotate(0deg); }
414 | 40% { -webkit-transform: translate(1px, -1px) rotate(1deg); }
415 | 50% { -webkit-transform: translate(-1px, 2px) rotate(-1deg); }
416 | 60% { -webkit-transform: translate(-3px, 1px) rotate(0deg); }
417 | 70% { -webkit-transform: translate(2px, 1px) rotate(-1deg); }
418 | 80% { -webkit-transform: translate(-1px, -1px) rotate(1deg); }
419 | 90% { -webkit-transform: translate(2px, 2px) rotate(0deg); }
420 | 100% { -webkit-transform: translate(1px, -2px) rotate(-1deg); }
421 | }
422 |
423 | @keyframes fade-and-hide {
424 | 0% {
425 | display: block;
426 | opacity: 1;
427 | }
428 | 99% {
429 | display: block;
430 | }
431 | 100% {
432 | display: none;
433 | opacity: 0;
434 | }
435 | }
436 |
437 | @-webkit-keyframes fade-and-hide {
438 | 0% {
439 | display: block;
440 | opacity: 1;
441 | }
442 | 99% {
443 | display: block;
444 | }
445 | 100% {
446 | display: none;
447 | opacity: 0;
448 | }
449 | }
450 |
451 |
452 | @keyframes spinner {
453 | to {transform: rotate(360deg);}
454 | }
455 |
456 | @-webkit-keyframes spinner {
457 | to {-webkit-transform: rotate(360deg);}
458 | }
459 |
460 | // IE10+ CSS styles
461 | @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
462 | .tic-tac-toe-table-cell{
463 | vertical-align: middle;
464 | text-align: center;
465 | }
466 |
467 | .tic-tac-toe-table .o{
468 | margin: 0;
469 | position: static;
470 | left: auto;
471 | top: auto;
472 | display: inline-block;
473 | }
474 | }
--------------------------------------------------------------------------------
/css/scss/globals/_defaults.scss:
--------------------------------------------------------------------------------
1 | @import "compass/css3/transition";
2 |
3 | html {
4 | font-size: 100%;
5 | overflow-y: scroll;
6 | }
7 | body {
8 | font-size: 13px;
9 | line-height: 1
10 | }
11 | body, button, input, select, textarea {
12 | font-family: Arial,sans-serif;
13 | color: #929292;
14 | font-size: $font-size-default;
15 | }
16 | ::-moz-selection {
17 | background: #45b2ad;
18 | color: #fff;
19 | text-shadow:none
20 | }
21 | ::selection {
22 | background: #45b2ad;
23 | color: #fff;
24 | text-shadow: none
25 | }
26 | a {
27 | color: blue;
28 | text-decoration: none;
29 | @include single-transition(color, .2s, linear);
30 | }
31 | a:focus {
32 | outline: thin dotted
33 | }
34 | a:hover,a:active {
35 | outline: 0;
36 | text-decoration: none
37 | }
38 | img{
39 | vertical-align: middle;
40 | }
41 |
42 | h1, h2, h3, h4, h5 {
43 | line-height: 140%;
44 | margin-bottom: .5em;
45 | }
46 |
47 | p {
48 | line-height: 150%;
49 | margin-bottom: .7em;
50 | }
--------------------------------------------------------------------------------
/css/scss/globals/_helpers.scss:
--------------------------------------------------------------------------------
1 | // rv classes
2 |
3 | .hide { display: none }
4 | .show { display: block }
5 | .clr { clear: both }
6 | .left { float: left }
7 | .right { float: right }
8 | .skip {
9 | text-indent: 100%;
10 | white-space: nowrap;
11 | overflow: hidden;
12 | display: block;
13 | }
14 |
15 | .group {
16 | zoom: 1;
17 | &:before, &:after {
18 | content: "";
19 | display: table;
20 | }
21 | &:after {
22 | clear: both;
23 | }
24 | }
--------------------------------------------------------------------------------
/css/scss/globals/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin rv-v-align( $h ){
2 | height: $h;
3 | line-height: $h;
4 | }
5 |
6 | @mixin rv-square( $dimensions ){
7 | width: $dimensions;
8 | height: $dimensions;
9 | }
10 |
11 | @mixin rv-sticky( $top: false, $right: false, $bottom: false, $left: false ){
12 | position: absolute;
13 | @if $top != false {
14 | top : $top;
15 | }
16 | @if $right != false {
17 | right : $right;
18 | }
19 | @if $bottom != false {
20 | bottom : $bottom;
21 | }
22 | @if $left != false {
23 | left : $left;
24 | }
25 | }
26 |
27 | @mixin rv-dimentions($w, $h){
28 | width: $w;
29 | height: $h;
30 | }
31 |
32 | // ---------------------------------------------------------
33 | // USAGE:
34 | // 1. @import "icons/*.png";
35 | // 2. @each $icon-name in icon-file-name, icon-file-name-2 {
36 | // @include rv-icon-set( "icons", $icon-name );
37 | // }
38 | @mixin rv-icon-set( $icons-slug, $icon-name ){
39 | .#{$icon-name} {
40 | display: inline-block;
41 | width: icons-sprite-width( $icon-name );
42 | height: icons-sprite-height( $icon-name );
43 | @include icons-sprite( $icon-name );
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/css/scss/globals/_print.scss:
--------------------------------------------------------------------------------
1 | @media print {
2 | * {
3 | background:transparent!important;
4 | color:black!important;
5 | box-shadow:none !important;
6 | text-shadow:none!important;
7 | filter:none!important;
8 | -ms-filter:none !important
9 | }
10 | a,a:visited {
11 | text-decoration:underline
12 | }
13 | a[href]:after {
14 | content:" (" attr(href) ")"
15 | }
16 | abbr[title]:after {
17 | content:" (" attr(title) ")"
18 | }
19 | .ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after {
20 | content:""
21 | }
22 | pre,blockquote {
23 | border:1px solid #999;
24 | page-break-inside:avoid
25 | }
26 | thead {
27 | display:table-header-group
28 | }
29 | tr,img {
30 | page-break-inside:avoid
31 | }
32 | img {
33 | max-width:100%!important
34 | }
35 | @page {
36 | margin:0.5cm
37 | }
38 | p,h2,h3 {
39 | orphans:3;
40 | widows:3
41 | }
42 | h2,h3 {
43 | page-break-after: avoid
44 | }
45 | }
--------------------------------------------------------------------------------
/css/scss/globals/_reset.scss:
--------------------------------------------------------------------------------
1 | html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,address,big,cite,code,em,img,small,strong,sub,sup,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,header,nav,article,section,dialog,figure,aside,footer {
2 | margin:0;
3 | padding:0;
4 | border:0;
5 | font-size:100%;
6 | vertical-align:baseline;
7 | background:transparent
8 | }
9 | ol,ul {
10 | list-style:none
11 | }
12 | blockquote,q {
13 | quotes:none
14 | }
15 | blockquote:before,blockquote:after,q:before,q:after {
16 | content:'';
17 | content:none
18 | }
19 | ins {
20 | text-decoration:none
21 | }
22 | del {
23 | text-decoration:line-through
24 | }
25 | table {
26 | border-collapse:collapse;
27 | border-spacing:0
28 | }
29 | hr {
30 | display:none
31 | }
32 | a {
33 | overflow:hidden
34 | }
35 | abbr[title] {
36 | border-bottom:1px dotted
37 | }
38 | b,strong {
39 | font-weight:bold
40 | }
41 | blockquote {
42 | margin:1em 40px
43 | }
44 | pre {
45 | white-space:pre;
46 | white-space:pre-wrap;
47 | word-wrap:break-word
48 | }
49 | small {
50 | font-size:85%
51 | }
52 | img {
53 | -ms-interpolation-mode:bicubic
54 | }
55 | label {
56 | cursor:pointer
57 | }
58 | button,input,select,textarea {
59 | font-size:100%;
60 | margin:0;
61 | vertical-align:baseline
62 | }
63 | textarea {
64 | overflow:auto;
65 | vertical-align:top
66 | }
67 | button,input {
68 | line-height:normal
69 | }
70 | input[type="search"] {
71 | box-sizing: content-box;
72 | }
73 | input[type="checkbox"],input[type="radio"] {
74 | box-sizing: border-box;
75 | }
76 | button {
77 | width:auto;
78 | overflow:visible
79 | }
80 | button::-moz-focus-inner,input[type="reset"]::-moz-focus-inner,input[type="button"]::-moz-focus-inner,input[type="submit"]::-moz-focus-inner,input[type="file"]>input[type="button"]::-moz-focus-inner {
81 | border:0;
82 | padding:0;
83 | margin:0;
84 | }
85 | .lt-ie8 button,.lt-ie8 input {
86 | overflow:visible
87 | }
88 | .lt-ie8 button,.lt-ie8 input,.lt-ie8 select,.lt-ie8 textarea {
89 | vertical-align: middle
90 | }
91 |
92 | // border-box
93 | *, *:before, *:after {
94 | -webkit-box-sizing: border-box;
95 | -moz-box-sizing: border-box;
96 | box-sizing: border-box;
97 | }
--------------------------------------------------------------------------------
/css/scss/globals/_variables.scss:
--------------------------------------------------------------------------------
1 | $font-size-default: 12px;
--------------------------------------------------------------------------------
/css/scss/style.scss:
--------------------------------------------------------------------------------
1 | /* ===================================
2 | * Project: Canvas Fireworks
3 | * Author: Ramon Victor | @ramonvictor
4 | * Date: March 2016
5 | * ===================================
6 | */
7 |
8 | @import "globals/variables";
9 | @import "globals/reset";
10 |
11 | @import "app/app";
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | /*******************************************************************************
2 | 1. DEPENDENCIES
3 | *******************************************************************************/
4 |
5 | var gulp = require('gulp'); // gulp core
6 | var compass = require('gulp-compass'); // compass compiler
7 | var uglify = require('gulp-uglify'); // uglifies the js
8 | var jshint = require('gulp-jshint'); // check if js is ok
9 | var rename = require("gulp-rename"); // rename files
10 | var concat = require('gulp-concat'); // concatinate js
11 | var notify = require('gulp-notify'); // send notifications to osx
12 | var plumber = require('gulp-plumber'); // disable interuption
13 | var stylish = require('jshint-stylish'); // make errors look good in shell
14 | var minifycss = require('gulp-minify-css'); // minify the css files
15 | var browserSync = require('browser-sync').create(); // inject code to all devices
16 | var autoprefixer = require('gulp-autoprefixer'); // sets missing browserprefixes
17 | var nodemon = require('gulp-nodemon');
18 | var webpack = require('webpack');
19 | var webpackStream = require('webpack-stream');
20 | var port = process.env.PORT || 3000;
21 |
22 | /*******************************************************************************
23 | 2. FILE DESTINATIONS (RELATIVE TO ASSSETS FOLDER)
24 | *******************************************************************************/
25 |
26 | var target = {
27 | sass_src : 'css/scss/**/*.scss', // all sass files
28 | css_dest : 'public/css', // where to put minified css
29 | css_output : 'public/css/*.css', // where to put minified css
30 | sass_folder : 'css/scss', // where to put minified css
31 | js_concat_src : [ // all js files that should be concatinated
32 | 'src/utils.js',
33 | 'src/store.js',
34 | 'src/winner-service.js',
35 | 'src/score-view.js',
36 | 'src/grid-view.js',
37 | 'src/fiveicon-view.js',
38 | 'src/game.js',
39 | 'src/initializer.js'
40 | ],
41 | js_dest : 'public/js', // where to put minified js
42 | css_img : 'public/css/i'
43 | };
44 |
45 |
46 | /*******************************************************************************
47 | 3. COMPASS TASK
48 | *******************************************************************************/
49 |
50 | gulp.task('compass', function() {
51 | gulp.src(target.sass_src)
52 | .pipe(plumber())
53 | .pipe(compass({
54 | css: target.css_dest,
55 | sass: target.sass_folder,
56 | image: target.css_img
57 | }))
58 | .pipe(autoprefixer(
59 | 'last 2 version',
60 | '> 1%',
61 | 'ios 6',
62 | 'android 4'
63 | ))
64 | .pipe(minifycss())
65 | .pipe(gulp.dest(target.css_dest));
66 | });
67 |
68 |
69 | /*******************************************************************************
70 | 4. JS TASKS
71 | *******************************************************************************/
72 |
73 | // lint my custom js
74 | gulp.task('js-lint', function() {
75 | gulp.src(target.js_concat_src) // get the files
76 | .pipe(jshint()) // lint the files
77 | .pipe(jshint.reporter(stylish)) // present the results in a beautiful way
78 | });
79 |
80 | /*******************************************************************************
81 | 5. BROWSER SYNC
82 | *******************************************************************************/
83 | gulp.task('browser-sync', function() {
84 | browserSync.init({
85 | proxy: 'http://localhost:' + port,
86 | files: ['public/**/*.*'],
87 | port: 5000
88 | });
89 | });
90 |
91 | // Reference: https://gist.github.com/sogko/b53d33d4f3b40d3b4b2e
92 | gulp.task('nodemon', function(cb) {
93 | return nodemon({
94 | script: 'index.js'
95 | }).once('start', cb);
96 | });
97 |
98 | gulp.task('webpack', function() {
99 | return gulp.src(target.js_concat_src)
100 | .pipe(webpackStream({
101 | output: {
102 | filename: 'app.js'
103 | }
104 | }))
105 | .pipe(gulp.dest(target.js_dest));
106 | });
107 |
108 | /*******************************************************************************
109 | 1. GULP TASKS
110 | *******************************************************************************/
111 | // gulp.task('watch', function() {
112 | // gulp.watch(target.sass_src, ['compass']).on('change', browserSync.reload);
113 | // gulp.watch(target.css_output).on('change', browserSync.reload);
114 | // gulp.watch(target.js_concat_src, ['js-lint']).on('change', browserSync.reload);
115 | // gulp.watch(target.js_concat_src, ['webpack']).on('change', browserSync.reload);
116 | // });
117 |
118 | gulp.task('default', ['compass', 'js-lint', 'webpack', 'nodemon']);
119 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var app = express();
3 | var http = require('http').Server(app);
4 | var io = require('socket.io')(http);
5 | var port = process.env.PORT || 3000;
6 |
7 | app.use(express.static(__dirname + '/public'));
8 |
9 | app.get('/', function(req, res) {
10 | res.sendfile('index.html');
11 | });
12 |
13 | io.on('connection', function(socket) {
14 | socket.on('room', function(room) {
15 | socket.join(room);
16 | });
17 |
18 | socket.on('dispatch', function(data) {
19 | socket.broadcast.to(data.room)
20 | .emit('dispatch', data);
21 | });
22 | });
23 |
24 | http.listen(port);
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tic-tac-toe-js",
3 | "author": "Ramon Victor",
4 | "description": "Tic Tac Toe game using redux",
5 | "license": "MIT",
6 | "version": "1.0.0",
7 | "main": "web.js",
8 | "repository": "ramonvictor/tic-tac-toe-js",
9 | "dependencies": {
10 | "express": "^4.10.2",
11 | "socket.io": "^1.4.5"
12 | },
13 | "engines": {
14 | "node": "5.0.0"
15 | },
16 | "devDependencies": {
17 | "browser-sync": "^2.12.8",
18 | "gulp": "^3.9.1",
19 | "gulp-autoprefixer": "^3.1.0",
20 | "gulp-compass": "^2.1.0",
21 | "gulp-concat": "^2.6.0",
22 | "gulp-jshint": "^2.0.1",
23 | "gulp-minify-css": "^1.2.4",
24 | "gulp-nodemon": "^2.0.7",
25 | "gulp-notify": "^2.2.0",
26 | "gulp-plumber": "^1.1.0",
27 | "gulp-rename": "^1.2.2",
28 | "gulp-uglify": "^1.5.3",
29 | "jshint": "^2.9.2",
30 | "jshint-stylish": "^2.2.0",
31 | "webpack-stream": "^3.2.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/public/css/style.css:
--------------------------------------------------------------------------------
1 | a,abbr,address,article,aside,big,blockquote,body,caption,cite,code,dd,dialog,div,dl,dt,em,fieldset,figure,footer,form,h1,h2,h3,h4,h5,h6,header,html,iframe,img,label,legend,li,nav,object,ol,p,pre,section,small,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,ul{margin:0;padding:0;border:0;font-size:100%;vertical-align:baseline;background:0 0}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:'';content:none}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0}hr{display:none}a{overflow:hidden}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:1em 40px}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}small{font-size:85%}img{-ms-interpolation-mode:bicubic}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline}textarea{overflow:auto;vertical-align:top}button,input{line-height:normal}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}button{width:auto;overflow:visible}button::-moz-focus-inner,input[type=button]::-moz-focus-inner,input[type=file]>input[type=button]::-moz-focus-inner,input[type=reset]::-moz-focus-inner,input[type=submit]::-moz-focus-inner{border:0;padding:0;margin:0}.lt-ie8 button,.lt-ie8 input{overflow:visible}.lt-ie8 button,.lt-ie8 input,.lt-ie8 select,.lt-ie8 textarea{vertical-align:middle}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}body{font-size:14px;font-family:Helvetica,Arial,'Sans serif';color:#555;background:#38485f}.container{width:500px;margin:0 auto}.header{font-size:18px}.footer{padding:30px 0;color:#96a8c1;text-align:center;position:relative}.footer a{color:#96a8c1;text-decoration:none;-webkit-transition:all .2s linear;transition:all .2s linear;-webkit-tap-highlight-color:transparent}.footer a:hover{color:#fff}.group{zoom:1}.group:after,.group:before{content:"";display:table}.group:after{clear:both}.room-id-label{border:1px solid #324156;border-right:0;padding:12px 13px 11px;border-radius:3px 0 0 3px;text-transform:uppercase;position:relative;top:-1px;-webkit-tap-highlight-color:transparent}.room-id{max-width:100px;font-size:16px;background:#324156;border:0;border-radius:0 3px 3px 0;margin-bottom:25px;padding:12px 18px 11px;color:#96A8C1;outline:0;-webkit-tap-highlight-color:transparent;-webkit-transition:all .2s linear;transition:all .2s linear}.room-id:focus{background:#2c3a4c}.refresh-icon{font-size:16px;color:#96A8C1;padding:9px 14px;text-align:center;border:1px solid #324156;border-radius:3px;cursor:pointer;background:0 0;outline:0;-webkit-transition:all .2s linear;transition:all .2s linear;-webkit-tap-highlight-color:transparent}.refresh-icon:hover{color:#aebdd3;border-color:#aebdd3}.tic-tac-toe-table{width:500px;height:500px}.tic-tac-toe-table-cell{border:3px solid #283344;width:33.3%;height:33.3%;position:relative}.turn-display{overflow:hidden;white-space:nowrap}.turn-display>li{padding:40px 15px;width:50%;float:left;position:relative;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.turn-display .score{color:#95a7c1;display:inline-block;background:#273342;border-radius:14px;min-width:40px;padding:0 10px;text-align:center;position:absolute;top:50%;border-left:1px solid #141a22;border-top:1px solid #141a22;margin-top:-13px;height:28px;line-height:26px}.is-x .score{right:90px}.is-o .score{left:90px}.turn-player{width:60px;height:60px;border-radius:50%;background:#54667f;position:relative;-webkit-box-shadow:3px 3px 0 0 #344359;box-shadow:3px 3px 0 0 #344359}.is-o .turn-player{float:left}.is-x .turn-player{float:right}.turn-player>.o{border-width:5px;width:30px;height:30px;margin:-15px 0 0 -15px;position:absolute;left:50%;top:50%;z-index:2}.turn-player>.x{margin:0;z-index:2}.turn-player>.x:after,.turn-player>.x:before{width:5px;height:30px;top:15px;left:34px}.is-selected .turn-player:before{position:absolute;top:0;left:0;width:60px;height:60px;content:'';border-radius:50%;border:3px solid transparent;border-top-color:rgba(255,255,255,.3);border-bottom-color:rgba(255,255,255,.3);animation:spinner 1s ease infinite;-webkit-animation:spinner 1s ease infinite}.x:after,.x:before{content:'';display:block;width:15px;height:110px;background:#fff;border-radius:2px;position:absolute;left:50%;top:24px;margin-left:-7px}.x:before{-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.x:after{-webkit-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg)}.x.is-winner-cell:after,.x.is-winner-cell:before{background:#5bd1ab}.is-winner-cell{animation-name:shakeme;animation-duration:.8s;-ms-transform-origin:50% 50%;transform-origin:50% 50%;animation-iteration-count:infinite;animation-timing-function:linear;-webkit-animation-name:shakeme;-webkit-animation-duration:.8s;-webkit-transform-origin:50% 50%;-webkit-animation-iteration-count:infinite;-webkit-animation-timing-function:linear}.o{width:95px;height:95px;border-radius:50%;border:15px solid #fff;position:absolute;left:50%;top:50%;margin:-47px 0 0 -47px}.o.is-winner-cell{border-color:#5bd1ab}.pop-over{width:70%;background:#fff;border-radius:3px;position:absolute;left:0;bottom:125px;margin-left:15%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:20px 25px;color:#666d79;z-index:5;opacity:1;-webkit-transition:all .2s linear;transition:all .2s linear;-webkit-box-shadow:3px 3px 0 0 rgba(0,0,0,.1);box-shadow:3px 3px 0 0 rgba(0,0,0,.1);display:none}.pop-over p{line-height:150%}.pop-over:before{content:'';display:block;width:0;height:0;border:8px solid transparent;border-top-color:#fff;position:absolute;bottom:-16px;left:50%;margin-left:-4px}.pop-over.hide{opacity:0}.pop-over-close{position:absolute;top:0;right:0;width:30px;display:block;height:30px;line-height:30px;text-align:center;cursor:pointer;font-size:18px;opacity:1}@media only screen and (max-device-width:499px){.container{width:100%;margin:0;padding:0 25px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.tic-tac-toe-table{width:100%;height:auto;-webkit-tap-highlight-color:transparent}.tic-tac-toe-table-cell{position:relative}.tic-tac-toe-table-cell:after{content:'';display:block;position:absolute;top:0;right:0;bottom:0;left:0}.tic-tac-toe-table-cell:before{content:'';display:block;padding-top:100%;width:1px;float:left}.tic-tac-toe-table-cell .x{width:60px;height:60px;position:absolute;left:50%;top:50%;margin:-30px 0 0 -30px}.tic-tac-toe-table-cell .x:after,.tic-tac-toe-table-cell .x:before{width:10px;height:65px;top:-3px;left:25px;margin:0}.tic-tac-toe-table-cell .o{width:60px;height:60px;border-width:10px;position:absolute;left:50%;top:50%;margin:-30px 0 0 -30px}.pop-over{width:90%;margin-left:5%}}@-webkit-keyframes shakeme{0%{-webkit-transform:translate(2px,1px) rotate(0deg)}10%{-webkit-transform:translate(-1px,-2px) rotate(-1deg)}20%{-webkit-transform:translate(-3px,0) rotate(1deg)}30%{-webkit-transform:translate(0px,2px) rotate(0deg)}40%{-webkit-transform:translate(1px,-1px) rotate(1deg)}50%{-webkit-transform:translate(-1px,2px) rotate(-1deg)}60%{-webkit-transform:translate(-3px,1px) rotate(0deg)}70%{-webkit-transform:translate(2px,1px) rotate(-1deg)}80%{-webkit-transform:translate(-1px,-1px) rotate(1deg)}90%{-webkit-transform:translate(2px,2px) rotate(0deg)}100%{-webkit-transform:translate(1px,-2px) rotate(-1deg)}}@keyframes shakeme{0%{-webkit-transform:translate(2px,1px) rotate(0deg)}10%{-webkit-transform:translate(-1px,-2px) rotate(-1deg)}20%{-webkit-transform:translate(-3px,0) rotate(1deg)}30%{-webkit-transform:translate(0px,2px) rotate(0deg)}40%{-webkit-transform:translate(1px,-1px) rotate(1deg)}50%{-webkit-transform:translate(-1px,2px) rotate(-1deg)}60%{-webkit-transform:translate(-3px,1px) rotate(0deg)}70%{-webkit-transform:translate(2px,1px) rotate(-1deg)}80%{-webkit-transform:translate(-1px,-1px) rotate(1deg)}90%{-webkit-transform:translate(2px,2px) rotate(0deg)}100%{-webkit-transform:translate(1px,-2px) rotate(-1deg)}}@keyframes fade-and-hide{0%{display:block;opacity:1}99%{display:block}100%{display:none;opacity:0}}@-webkit-keyframes fade-and-hide{0%{display:block;opacity:1}99%{display:block}100%{display:none;opacity:0}}@keyframes spinner{to{-webkit-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes spinner{to{-webkit-transform:rotate(360deg)}}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.tic-tac-toe-table-cell{vertical-align:middle;text-align:center}.tic-tac-toe-table .o{margin:0;position:static;left:auto;top:auto;display:inline-block}}
--------------------------------------------------------------------------------
/public/favicon-o.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/favicon-o.ico
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/favicon.ico
--------------------------------------------------------------------------------
/public/icons/tic-tac-toe-js-icon-114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/icons/tic-tac-toe-js-icon-114.png
--------------------------------------------------------------------------------
/public/icons/tic-tac-toe-js-icon-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/icons/tic-tac-toe-js-icon-120.png
--------------------------------------------------------------------------------
/public/icons/tic-tac-toe-js-icon-144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/icons/tic-tac-toe-js-icon-144.png
--------------------------------------------------------------------------------
/public/icons/tic-tac-toe-js-icon-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/icons/tic-tac-toe-js-icon-152.png
--------------------------------------------------------------------------------
/public/icons/tic-tac-toe-js-icon-180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/icons/tic-tac-toe-js-icon-180.png
--------------------------------------------------------------------------------
/public/icons/tic-tac-toe-js-icon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/icons/tic-tac-toe-js-icon-72.png
--------------------------------------------------------------------------------
/public/icons/tic-tac-toe-js-icon-72@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/icons/tic-tac-toe-js-icon-72@2x.png
--------------------------------------------------------------------------------
/public/icons/tic-tac-toe-js-icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/icons/tic-tac-toe-js-icon-76.png
--------------------------------------------------------------------------------
/public/icons/tic-tac-toe-js-icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/icons/tic-tac-toe-js-icon-76@2x.png
--------------------------------------------------------------------------------
/public/icons/tic-tac-toe-js-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/icons/tic-tac-toe-js-icon.png
--------------------------------------------------------------------------------
/public/icons/tic-tac-toe-js-icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/icons/tic-tac-toe-js-icon@2x.png
--------------------------------------------------------------------------------
/public/img/startup-1242x2148.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/img/startup-1242x2148.png
--------------------------------------------------------------------------------
/public/img/startup-640x1096.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/img/startup-640x1096.png
--------------------------------------------------------------------------------
/public/img/startup-750x1294.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ramonvictor/tic-tac-toe-js/aab37a8cadfaff7bc5fb12c5949f1607885eaff7/public/img/startup-750x1294.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
53 | | 54 | | 55 | |
58 | | 59 | | 60 | |
63 | | 64 | | 65 | |