From 3a8ef7e8f201f34d32bab397b33964ed9d376b1d Mon Sep 17 00:00:00 2001 From: Bart Riemens Date: Tue, 10 Sep 2019 21:39:27 +0200 Subject: [PATCH] Update about page and documentation. Add allow wrapping option. --- README.md | 25 ++-- config/default.conf | 9 +- package.json | 3 +- public/favicon.ico | Bin 0 -> 103460 bytes public/img/docker.png | Bin 0 -> 2049 bytes public/img/font-awesome.png | Bin 0 -> 3347 bytes public/img/jest.png | Bin 0 -> 3401 bytes public/img/reactjs.png | Bin 0 -> 3523 bytes public/img/reduxjs.png | Bin 0 -> 3574 bytes public/img/snake.png | Bin 0 -> 3120 bytes public/img/styled-components.png | Bin 0 -> 3894 bytes public/img/webpack.png | Bin 0 -> 5516 bytes public/index.html | 9 +- src/components/Checkbox.js | 49 ++++++ src/components/Code.js | 10 +- src/components/ControlPanel.js | 21 ++- src/components/Highscore.js | 1 - src/components/Link.js | 14 +- src/components/PageSection.js | 11 +- src/components/Stage.js | 8 +- src/components/ThemeSelector.js | 1 + src/components/Title.js | 11 +- src/containers/About.js | 239 +++++++++++++++++++++++++----- src/containers/Empty.js | 7 + src/containers/Snake.js | 48 +++--- src/enums/keys.js | 1 + src/index.js | 6 +- src/redux/Module.js | 1 - src/redux/snake.js | 73 ++++++--- src/theming/theme.js | 2 + src/theming/themes/dark.js | 5 +- src/theming/themes/darkOcean.js | 9 +- src/theming/themes/default.js | 8 + src/theming/themes/forestNight.js | 52 +++++++ webpack.config.js | 9 ++ webpack.production.config.js | 14 +- 36 files changed, 535 insertions(+), 111 deletions(-) create mode 100644 public/favicon.ico create mode 100644 public/img/docker.png create mode 100644 public/img/font-awesome.png create mode 100644 public/img/jest.png create mode 100644 public/img/reactjs.png create mode 100644 public/img/reduxjs.png create mode 100644 public/img/snake.png create mode 100644 public/img/styled-components.png create mode 100644 public/img/webpack.png create mode 100644 src/components/Checkbox.js create mode 100644 src/containers/Empty.js create mode 100644 src/theming/themes/forestNight.js diff --git a/README.md b/README.md index bba2fee..89d814c 100644 --- a/README.md +++ b/README.md @@ -2,28 +2,34 @@ Play snake in your browser with HTML 5. This game is developed using React with hooks and Redux. All the game state and highscores are stored in local storage. +## Introduction + +This game has been created as a Crafity experiment to test out React, Redux and Styled Components and see if it is possible to create a simple web based game like snake. The for this game is open source and free to download and change. + +A couple of noticeable features in this game are resumable game play using local storage. The page can be reloaded or reopened any time and the game should continue from where it was left. + ## Feature Technologies -- React + React Router +- React +- React Router - Redux - Styled Components -- Webpack + Babel +- Webpack +- Babel +- Font Awesome - Jest - ESLint - pre-commit +- Docker -## Todo +## TODO -[ ] Create welcome / instructions page / about page -[ ] Link to repository -[x] Add license -[x] High score -[ ] Docker build script -[ ] Improve banner / popup windows +[ ] Make stage wrapping an optional feature [ ] Touch and mobile support ## License +``` The MIT License (MIT) Copyright (c) Crafity VOF (crafity.com) @@ -45,3 +51,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` diff --git a/config/default.conf b/config/default.conf index d2e2982..4cb9ad2 100644 --- a/config/default.conf +++ b/config/default.conf @@ -5,8 +5,15 @@ server { #charset koi8-r; #access_log /var/log/nginx/host.access.log main; + root /usr/share/nginx/html; + + location /redux/img { + alias /usr/share/nginx/html/img; + } + + location /img {} + location / { - alias /usr/share/nginx/html/; try_files $uri /index.html; } diff --git a/package.json b/package.json index ef804ed..a1559cd 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "dev": "webpack-dev-server --mode development --open", + "dev": "webpack-dev-server --mode development", "build": "webpack --optimize-minimize --config webpack.production.config.js", "webpack": "webpack --config webpack.config.js --watch", "lint": "eslint src", @@ -52,6 +52,7 @@ "react-redux": "^7.1.0", "react-router": "^5.0.1", "react-router-dom": "^5.0.1", + "react-router-hash-link": "^1.2.2", "react-transition-group": "^4.2.2", "redux": "^4.0.4", "styled-components": "^4.3.2" diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..79ad12038e660010b29e21a78d96766e76412843 GIT binary patch literal 103460 zcmeI530zOv`^Ue=5?QimPj-=gFYSw>ebY|bEGa_TvrDLuHHB;yMfM?ynTaehmh3Vn z8H^|fgP#9+Zk?~E?{8@GH^%7O>-Fw_Zr|meb3XTet9zd3IU-RtQ7sXERTH%r?Qb9w zxicLWR=VG{f^!k1BZ{Kg~e)AW3+dWV0 zn(g0j-RS1N9h;)kvRiL8{jP@2040N~H4MV)DXuZ-vdW;J_Wh8NIa>;Y|9q^l%PQU} zsin%(;Ea%Wn{$Q+2NWf4d+2}8w~Y*I&0l>vjQ=j?XqcgjkGn@ic1}+GLF4?)4HtJ7 ztsU{Ic}!Nq1>=~4sq+?R>v#Dn-qQc{w2MuY?(FwE@bZtsR=0c1jyWn4#b{R(%{LQ; zjcqC#)1s!RgNLSBJ*zko;N(AQTgVQ*2mnGJErc}YirT%ZSgC!X2R08#rY0-)gy1cQ;xAY zEDw(Pty3qj%2GJAA-*8Kh265(XBF;rD>jXBnh@RNL2;zf!*?}|V_Plr@7t$)@wsH{ zlQ(znd^0;cW6H~Q>b_4?KGrY!c0s1buFf;>ElIX4DLOvWIo$W;bkRMT2GbJX9*S2} z%2j-Px_L7wB{4|lod(J8s>7&7+mib1va*w1G9lttaLAJA6(01$m zf~_7-rmydwZlbMZGN@Djumx*P(__26{4OrFXw3PT&aHOXoymT)e9>u{KhhuH3|6|c zOYxOe%z@_*mU`Ro%&oTBqGWbrh|JT+smwP^cwJtc+62^4e z*TUMrSR-_Kw#PR8KazrS?;bUOQ)k6QyQ~PvNa(q4?x@@puh&PjTi?6TJMrerCPgpp z4Fihao^su7mw$Ovt_<2G2{i`&oZT#?1q@-Nw{-e}0n_2G9PDMRK$9rxiO-57Es} z-`4wN&bHW1XChjrD`;39&0mqN@aL0}O`4@^cB}j07L9AGF>m<0$Lc{2`Kd=jMn8F@ zzREEB-kTql*2ph9dZzX9eZ4K8*BWcv_pdJs?An!TJ@*vEy~!FB;~YEW&Gu}W`L(Jm z<@g80sjt46YtwVChuf1O>d^mY7t7ioYsRsD5kE9Pnp%__a>H+5_VP82i{GX=bjkbm zXa6->9oFA5d+n<}G<5vy!p2JTK72~;Zm?lg!aaCCFh;%T-ko5leD5XO@}FrpdexWf}5j$;%hL3SNA)n=7JTW=xOBj(_)J zq+H_Le47RN&*r3*)G6{$-yM2BGP}v7tGoOUXTKc!=FaZGdGKhk%nV)2kb6cGVMEdK z6K~Xa6d6sNtZsYmBL~@~@p{2bzv6c?kF3;Y?k;Q=6I!hP$Mfw$lTzB}C|tTxqOxX% zlgzHkFSh0+y|syap;AHqw{wLB&o(Y-b^f-q~w=;ZjX$W)0t+W8;agQN#2k7 zZ%b7Cf**S*6*{Fq=@OIMRV{c<_JPT_)|wCfupRv~6pkG~{_8uX&_7$p(&fS`!%g8Ky-n~U`0 z-o9OB6;in0xg;s}&GOZ87_n!LPK z+&yM+Ztf}nl5;;Ttra69`)A2k`2sh!Rp%eon6Kaa)R7ElnB(G8yiY0gRY}&1JI-~W z=ww#~%h%aUPX-sgiT~YU+f=>({oPV_?O8v2Cu&e%H7Fsw_qGM?Z2Y|@YMwi>G{4Jy z%aGq5KbL*|+Th)h&_Ssxm;9bcuq#aufv@!UN0K`HpA`qF0GO!hji1M zZc*A$SQumsb>bhyN4<{DHO>xtY;Jc{KJQrEJ-4&Pt9~AR=e4?$Nze7~Jp4n`5>1k0 zKYZd_1+_i)o5dX1QWU5B`_#CMjfzT<8LN$FBp&INqMl*7_~hF>h35m_UA6zy|HaH9 zMfYzUbo9OvH%{f(mvXxDM-z(n&;KdI+JDFQ#&LIYHoL`*di1hGtlHoX#q!R}ZVVmL zRc8M47ennP1|GhdI_6FFTSX<9$QeWk;?DfqZPBIq)gG}%1 zFO7?S{dh-g7e71e?9_s3&c6H3Oq}?lP`&?j&HGl9O>b{Vi8*j*OOZp8Ua!3^Vs97Q z$Mham@W;nSR{2ecX!^htQ+H-QyKp)}>CRyr^}^vrR}9zq`JHyFeJ*={K=8Yik|2vw zp&`F7_;sR8tjqaB2Ki2k-F@#|R^Pbla`CUfjZgOu2{0&8J@@)R7q=3<__zY~!9%(h zYiJba`rLBfapCp!b*{H>?|_h%jmPzq+iLh&KYHY8|F<1eAE(>oJ30<-XW_4u=sYsY zXo38%FH}TlHvQ$Vv0Z7o)sD=Zku@x4TXPGO#GrfA+fLmyFtjG%8|~44_f7) z+Uq3K@Z|LMHXr$L>G39AWb*h%$>!{JHb1CuD|+ZTx?O09W{z*WfM;{h1Ukp0w)K&X z3%Nb~_Kt$PD#Koch`hXtUKAO78SOiBD@rZ2%ev*SpO4bIzv?FsS2dO7$iiALUfS>b z-st|sD={ZkzIVNSz2Pc<3kOguE)Hn>Lv+XVeuWAxR=$34)y+9SPX397|B+jVoZ{L? zMZYN;bw#R5a7;9qaz0Nv-Paio3pc^%;9RJJ;J+8s)}&doA<# zO)m*LqE~qMPQzq&zQ%?W9=qEypzW!|{+<6&S$!erjmlA{0`t}vGn^0VDX1U#m3_?Y z@x38qb?(D|o@R&iKGvi6dbOk?jrKiqt4+FoDY5r1!<;$I`vqi#?Mr^4I#vHhz#0>y zhmY39u`A}dmi7I(Z7*N+9#(VBssL||&08-G6Ad?Uv`Egiy`=l%-I5gny=SKuMmk@5 z)T!5snckTf2G?>6nc-u2e~o4M8~G)tRa+EIYj-6hbb=<7^vx)!7E>7Fe!yYj@sG6d zw%mrTKmBIvZ)0++ko|p9y+@#BNO0us!AH{_bF>dn4o_`ysrbyi+2<~fA8vAZ@8*I- z7hA5EX%k$sDYalt_0C6+w*I;J=0>G-;|Mv|zj$%za>|+FxRQ6WaZXvwj2}(!l=(Q@JL&DyqnX|+ANym$OH;;Ndu#l;j?L<=S0o`*79jtek@9#d(Kfoje-W zeo!1(Gu^m-s$5~y;Nd3mJszlUbTy106JVD!Cvs_*E>>!4%U0B_`JM4W+x(Q)E7W%t z6yz5_pLb-H>7l}TOA}Q`FMem5-_@!0?ZTcNKm40#|KmrZ+tt!G+u0}QCaN}0EeXpW zH|3>WOjc{hL(ZDoH)DD?Sg4~tb)#mtrcrX20(DJtECO})3y#Jv%xU)gr?-bDDef#9 zbv@p~f6%=biAVC9Ip=2i!S8Kl`sMo#=MIS8;l6>cu}+HCnAzQ!4#OX?e^&9o+0w6Sq9%1k z0%bptZloh19RcYGNJl_A0@4wXj(~Ioq$4050qF=xM?g9P(h-o3z^9DBckD%CWSTYW zL}}2utu3YLz#g+Ht@KneeMjo5oU52*L3srRU<_o%7&sHyj;srRU<_o%7&sHyj;srRU<_o%7&sHyj;srRU< z_o%7&s3rZQ-lL}8qo&@YmVEt}wepjWCiS0u21xoR^-1Xm}L1Pi2wnEE5 zN<)=C7bpsjCa);H!lnRa=<0BwsBMk`ibi5AP}CFG0cBXiN}vo{9|e>lanV2-xOOR& zV`VrDBn*jJ0F~upE?#OEp7B3P#UNxU!ycp>Ghb>Omo09N<)plk0|w3 zly6WP4e6OgQ5e)qi}FdI*p+z^@gC0u>OE@eJ!OE@eJ!OE@e zJ!T=%Wxj?)}O}$4=y+=*GM@_v)O}$4=y+=*GM@_v)O}$4=y+=*GM@_v)T^TDs z>A6z>%bf#K|D~V*^c?u+u76qCRw6>ZK}r`XohLc~MRip;P;^r_0!44%HlXMp*aj4x zJqbY3NLmY&p>a!r(r@KFp!8iiA1J+I=0Z8x^Lr8ct&IT6;PsI}QD?tXD2B;#Krz}8 z2Nbnc3xU#w{Z61Xv(N-edvk4|bg?x6ih{KcP~OE@eJ!OE@eJ!OE@eJ?e7a3=5tJlz{=BKW{L6d+YKUxnc zt&P=y(!oL-C<->ZKxyTm50rWqYCusnA7w_V?C${TJ!OE@eJ!OE@eJ!OE@eJ!OE@eJ!OE@eJ!OE@e zJ!OE@eJ!+}{((Au6=YaJ3uiBsg+Dn2?P<*oY0mbpePM}z1CIZFo$7G<4 z+O!BL{iZkrMFIckAv82o1B$GLI#BA^YC$<1bf6AFmT5`@M?Ijl@h}5QpFlUDXv72o z#XKnrC@u#QfHE<24^XD(90$s{^i-e>oX6%92(6q(0j06w5TM8#3;{|D8*QL896JIi z&8@V~Q>?74YEdfsETG<_rrx8b-lL}8qo&@Yrrx8b-lL}8qo&@Yrrx72=RKYa#Cz1# zd(_l>)YN;_)O*y_d(_l>)YN;_)O*y_d(_l>)YN;_)O*y_d(_l>)YN;_)O*y_d(_l> z)KdSY*Z;@P0qON$wLkyIr*BK5_+R`UC?Pj9f#Q2E4Jf*crURw5g%(g6n5qG#(KutE zvT?iD#g}y+k?`;hf83%2kG z;(9OvDATeI0%cZiHc)&{9tO(rs5wAs=V1es`bMfHl*WcbFOYU({|>OE@eJ!OE@eJ!OE@eJ!OE@e zJ!OE@eJ!OE@eJ!u{ldh2QNFI^p6S#ipl=1KpAs79Vjm6 zP5{OA;z^)*TssF8mrL0|G5_IvplI&e43w^s!9b}u!2&3Fw z8MALaP-dPw1eAcBOrSU%+6k0?E9L{Gt)Cl|!*1mAwuCGl7yb3l6i zSMAS#$#cUhCafN%vz6{8N-qyfpy)@30mb~lR-iba%K(bcjnhC0xOV|4VGl1uIUZkx zFhbyi^FRs6&jHHR+h>5{e)%L&Y)@qXMJqWLDD6Tfu>VQlQ_oHpD9!BkfYNieCs1s5 zECY&9MiNl0Q{#ZrYXyr}By?Un6DW#acH1eIO9K2T^1)6(X*0nhl_KeHMf=oy)YN;_ z)O*y_d(_l>)YN;_)O*y_d(`E;$8&*rkD7XqntG3#dXJiVkD7XqntG3#dXJiVkD7Xq zntG3#dXJiVkD7XqntG3#dXJiVkD7XqntG3#dXKsyza<}+`v3mgm-;Wg{;S&cKP_Tf zUCJOI$Ga5Q)L5X*ypatQ@0+K9;&%B6P`qwu0A<4Ey+ASE83h!pgzzlN{5!`cQ)XN{ zm`IuaOXf35$ioXj3A}#^DBd^D0cFg|BS2AI!}1*n&D_j@Qr}z+D6J=&14VuLOrYq; z%>hd1h2B8vx^!wWW&E~f!zsS&7aLGIEc66Q8(+J0isa{eMf=oy)YN;_)O*y_d(_l> z)YN;_)O*y_d(_l>)YN;_)O*y_d(_l>)YN;_)O*y_d(_l>)YN;_)O*y_d(_l>)YN;_ z)O*y_d(_l>)YN;_)O*y_d(_l>)D`(H`MA`7>Ge;~0qON$wLbqPKMy>2uFIr^-N^z< z_>(I@3CTYNl!>`}fug!T6ew~wnm`c^?Fy8x`sy<%lI@e+pP7@|o-*O;p6!&tf^48H zcybjea~@s?O32+SKyf*e0+gOX6M#~W#jKU5e7t_A@fPPPsOE@eJ!OE@eJ!;AJRkTmNM@_v)O}$55(fd{OxYU2?^-s?M>GfZ=KK~^@AHvS;k)=#H zn9zw5aOQYRiouV^?@?r&EP+yk)if!G!9b`%sHM~IB}L8NN|_?rzRK>qU)(v8GBxkm zQ_A9J?C$|W`0qD?5|DQhC?4lCfnvCC6Ht1G&-jJn$nLRIDr&z$jDI-AEOpr{%B1kH zR+Ng~ud z)YN;_)O*y_d(_l>)YN;_lI@e+r{1Hc-lL}8qn3RA|94;Nzx4X2=YaJ3uUen~mHqsf z`s~?kiehvGP-=`)0ZJ`16`)ibF#ssdEwz#)YN;_)O*y_ zd(_l>)StUQ)O*y_d(_l>)YN;_)O*y_d(_l>)D^X#dXJiVkD7Xqx}x{{(vM61mtOz$ z9FSiBRqOLV9H2hM;r5M-l)5ffK#?(3fpS<32N^;&_CFz|r-zL><#T_&*`3(Zh+=eP zT{J~MZN*uN#`YzV6tzteij>N>PkT$)MvB{st=A}f_Uw_RRQCP<>E~1LQB&_xQ}0oi z^B&Iy;yr5WJ!d)OD>OE@eJ!OJbpwvT#`ntG3#dXKuY@BdFfU+TZ~ z`lsiB^!l&d7587i{v~@G3~y8@!*?gYq|{_JQ7LsS)qo=T|6zU8;dvCTK+o0`$>V?R z`x=qHu9UG^TYskF9zPBQgS3&+HN~d`NK&fr43Y2=*8bGPR z?h#X(T5G3KB#-yC@2@+b;XsK#d$d30Yj1yL+ef`eO}$4=y+=*GM@_v)O}$55+4lU~ z&!^s_rrx9ex7+u*k1zGV{D?~Zmwx`!bKqOQ{>z{JB9V2*p>32#9(F*f&GPFgb!;_R zjrjM}upAB)1&5K-D3ZsM?tfh)K)pvzy+=*GM@_v)O}$4=y+{3Z^;X&%$q|tHFF7Jr zYhUVr)$&`ix30T)%Tv_WMBSj&v^NBb%xFz0ha;=+PpECH@tD$atVvsn!ww>OE@eJ?ekAx6-3aj)2sE$q}hq`%?d_mfsci)@9dD9ZL5= z-x5j3^|4w&>F912NU7-f(#QYZ5uo0qrrx8b-lL}8qo&@Yrrx9ecY7y2 zdPO53^}nJK`nDgJ`u}bFTiM=?TC#W@Mb6C@C^B9qK&jzv1e6vNOn;(O_W080|GOhV zy+=*GM@_v)O}$4=y+{4;_C|X2%8r24|H_WoxAlCf|KFCMmF-Pm3yT31MX&K}jgOJZ&+Z*Z8 zDmwyF|0_FU-`4Y`{#VY=R;^meQif^RTT%>+C%aRWhuSMsDtEMh{@G0x6}wW}jvezS zrGAhLP~@T}1Ev1Fajz&HV`g@u{PR8dvPYoaqo&@Yrrx8b-lP7qt*!WI)O*y_d(_l> z)YN;_)O*y_d(;)T;J@_@ssGaJpPmEK>%VG!{_7k1nNX(trAJZLug|fk%$jq0Iz`7Y zGK|t^;Dmvc7M=Pwqtx%;MU~QOa35WY{19awO50(*^(YO8_f@B~@2}jA(x9i(DoR5$ z6QDE>9tV`>>jHq%EPBc_O810-mX!b2r+dY%rQV~a-lL}8qo&@Yrrx8b-lL}8qo&@Y zrrx8b-lL}8qo&@Yrrx8b-lL}8qo&@Yrrx8b-lL}8qpo;F|69+H`Y*ly={X?1{;SsK ze|Y%qu9U>YTRIf)fGBy2f{_B1-4klYpWydpuBD&2k5dyq_~r zTKT&GrA>eOE@eJ!OE@eJ!OE@eJ!OE@7{(fuwQvapbKRpMe z*MHUe{FnSZyKw^=P!=vcBS(>ZzU02;npwe={_zWeGV(+!P_#BK2g;yXfk1KmaW7ED zo;(PY(LbaCMR(_Fpmd%y0VoaTdIF`>#yLRgvm@M@BH6!heV=-dntG4AqW)9wQB&_x zQ}0nz?@?3lQB&_xQ}0nz?@?3lQB&_xQ}0nz?@?3lQCHO8Z|!lZ|I+K9o&(bB|69Al z|JnYm&O2>L@ytDPfug!S0w^7a8vtcsKLel)UKb7&qaTuhGU8A?P>c@70>vU_1yEGC zECNcWEwgq|{@Ky__TQa)kD7XqntG3#dXJiVkD7XqntG3#dXJiVkD7XqntG3#dXM_s zcj%w(q169>c6`6~cbEGAwLA1>+Xn#EC}CHRjG$O-j=DoJT`?CZ`o~rSWkBXipr{^? z2g=AFw*bZB_$HtXN{dXS^xqREOZl>+^X(svdXJiVkD7XqntG3#dXJiVkD7XqntG3# zdXJiVkD7Xq`rCKp%l1&}|Cb%_ukC13|GzdzzOMF!UCU@nG0%=wE615QS95~RrS0*Q zF}DtWPjSdiOQUFKtaPAQB_&m-d|jha^;$!{M_tZ)JQs-fsHyj;srRU<_o%7&sHyj; ztKNuzT|Jii|8@ECrCTHQ|4Wa!^mx({kdAWtn7Yi14>^mSWqT~jktp!cwF28 z{vzI?zq`*4z@I8?KViSJ9TpEQ9`OBul{Ta@`(+<^;`giMKF%XXy`%n3$TK4+N z?w4LT^n77`XJ7A!v!Z?B1B5)k()+>-%9<~1@c5P4r_cS;x*Kb&1|y77P1K%!XI~m3 zQIoo*2>a4+(h-o3fOG`@-Uxihp7xzgvu2%;2A$j5B25SOn2og3Q%OPETN|E2IyswP zLOPGLzKL`gYjGE8?_?^VU~hB_X=kH<1!-lfc@}A@KlCutT)p2?q^W8zTclP0E=>_p z|Nb?RHf`E8Midnl^%!ZUIp7)6P^0f7q`r#s4W!YKo=J$ppk7)?rJTkm zSJnQ@%C-_Q>J3u5h;*Ll0Ep_UZ~@)ajezLw+a{oUU>hJhdlCdRlGXw;G;XOtzm@X< z>AP~iK(Cm&Ai#ds3m;n>AuxD-Bp~YSd_fG8;{=R$!~vqVYN0?Eb}k^zEHnk$n`;Bo z#nwPT!CD6pc?%7Jx`u-R>7+f>AF1#>sN(%^ZKM^6s4fi##OBa;fssdc05XE@BWA~v z1*{Hj2E=I7QUT2fe?W!>PZSs!;0Z|I0C#}_VUqwEvS^yX@D-te=){K$7;TRM#B%3) z0sH;i0U4W-CSaDb0TAV&Ndoet^#EyYtR~RGLK_eT8(o1`4*G!9vrrRIH6LY$eCRx= z>iySV5_AIb$=(NuAH_7EoZ4K=d4}bP-Aat9t)uUO&E&Swh<c%0fsp)DfK1HY zE1nV}DzgQu=p&r|}l&5!G4a7a|k)t?h#F;{~Ox(?XP&PX(lZ*o3VJfBZw+ zs^0&_xhMJ~lg_1GLEO)#0OFFh{SGo9KH@bZ(jNxMcUpY}s%iBGL}BFct;mPQm;c=T za_S^x#=RfkAd8D`0J7xiU4e)vzXB3+{j7k==H-AWOtu$jZl;-u@b{D4x^7xzh^QsZ z21I}M6i-C*d6N58z5jeX!p`iGMJ60f=!68EIo=X6`0@BXM8?SykQ(e>i-5sEsKKbE z)9)psW^bj8@b~-B*8Sqnk;v4%V^5LA&)9ju2><=2KtSF_Ks?T83K;I&1W51j8NVQo z>~|I-`Thnm{^5vO>athJr0}p-h~)D=v|rWx|Dkc2`s~?kL@_!7kQ$>@1ZtV708(wl z0D729bOp{Yamd zh~b{7B4qlm_45$P=Y8saRquZ|Kz+pF_Kk~3T^B1rWK2~`u^0|AjB4z&0O{#rV~+gY z@wPj$r4eFuWL-3(pSI#GqOpBRB%-z{LJ|4U`)O|p+laWG*m@1wvuBSi@}cKf_UBc- z|9rfH;f)G1e0TCoq$ax`i`20!g?}E@HyxgbXa#zPL_f{j)*A*F?we@%8=-tab zkk5S_{(e=x|9l+03i2l*o#q7qQrlQnpq{k`AT`+U1f;38b{fK8_qn(0&Sy9v(PxkL zM?Uxaf9U!ayTFg4kM=_{B_^TwyO8vb@y(0 zL~Tvf4Wy>MAs{lNHA``1_xl;OZ8aVv9mkrqMZOjPt9t*rk1o4*>LA?%eM^uU_WFRx zjMEdSK2{5mj_ziG2!GwTvaRa<=RS^FvUnXL=Vl9tjF*W(4R0erT1+tg3E|KG*0xo> z|9t&+MX&LI)bX<`CCmZpFl1*~Jw-GYO>cpGYyN+&{pV{0^YwxGy5M|0@XzG~U%#8L z+s)VO=IeCx^||@F+@I^X6}?YY?>}E7lCKZR*M;Tl!SZ!r`TDPX-B-TeD_`f8ukXs& zb>-{1@^xJK`mKE3R=!>|h9KVKt^uMfu81?1z)*8$|~|M7MI z_>R`ve#apr5o@O5GM&-s?G z|HRjQ;_E%}b)NY8PJCS_zMd0b$BD1s#Mf=&>oxIpn)v!m{CHn_Th;qt(RlOv|9ssK zzTO94=Yy~B!PoWR>v`~XJox$@eBBPdUI$;NqoU*g+mBcE{`2u?d5l8ce_rpM*Lmml z-FaPiUe8^%td2jg-_Gl{^Lp*PPCI}9uYFt9`!DGauk$Ic?^{;am)G;jt#!(!MFF8Bn=3BN{ZQ$#TT& zu-tgH`hB`uHXPPxLL18wOBJ2X*LU%>-=XX`KC@SV*O}fSKDqto&iu9CeE)*}3j!YY znK$!EuQ@Xcm1p@rRSxrg+B0NIVUJ*+Cq4WpKI$=Z{DU4dJn#3II__2vpD{lxc{ycu za<$x{;Aj-n(o#Ew<-L118m8>mbf|J@bG3eRo2d1j)lj`p2+Mc(ksGEwj^(>sv0QgW zMxzR?V{Pa8{q_5H-O6+OyLyj#%k%nG<0AhjryukAhiqW$d6(t#4~~rz=kX6-xlo+L zKX`GFIDcQ!&GI?V-EXAQ>mJYBZ=%`%3D4PQ?LR^C^*_VI-H=uDK!vTN zsus5x^tP^qk%?=;gyqd!?oI%6mM?9ZydG4eWAtbHk0&lf-%9W+`jLIpJz^ zr;78z`v-W4bHNAA^AYEPYp!Lvo-7X>^S{klzcB|KbHAMq?-b{LtH;b2=Y6-t98?x# zi22^w2h91#T<<2MwP~(*cggl+j<4gc*t*iSw4bu{s8rw~YD9&B}6?F;}^% zgPu57xrLSXM9Dm5tAwZn9_ia|OwTz8lg}Rj&ogN-?qmv>ZjS=3=s*|{=nn12nm`+8 zHm}a!U*6LKT28iwHq)FzA;=vRSdMP{FqU7(bZft{(8AXdnoYEX7K}ENETKohI2btJ z8#I;&f<^KQaNHXYp6SUjo#l1oKxI| z)eGSH#Dg}n>$EJFxcjmYaUQX(yOB7DxPzCCIDgoZ^?~OO2eN!oo;Pf>BVL>{+}zF( z#QDNDS`T=xu)NEtO_F)Sm?P?)vu7sHkF{AhUz{7;-o-?m7ps}DOq>%t_UtinKCJtd z)8brMyEDhdd9VXEM2mA^>yBZ0%4PYlJ%YxGb6+QAB#ZN2C;o6iob%dk{tR)xYqv?G zcko@R{nF7O>g@FOfr?h0} z?HHD0>y?`Y-q*8P{5{Lr%sVGAfL+7BK_Q^0obR_McWcDqLefZ`l)P;j+~ z>owImPHoo2xW<0A&|-?sxsDzdagx_C%g?OE^HY_ajUMvcR3nxX#Pd=;FaIdcNewPw zd8n9^%5qUd9{w!OMV;{r>krFC#T?WzIX{SVQ2WHNTymC!in*taNbafDDpnJN<(^{B z>9_-%#W|-+(V^me(?J0qQ9ReQ<#fB;zsfUZ`GvK3ex-4A$WxwMIqhb)IInX2^`qjP zN{7r%;(SW?gX`DvT*~RU)6eoe%HRhq&z9v;V*aG(&p(RuC%cDwiE}5LI*$=j)Px z{^{QW3vwVZ|02w|!*YbLod(DBT`+3fYEWf)$8A~uaSK*sps6#f_2O&KewVox51#M! zKzY8`54|F$Z11z&ZwbrwRPf!pyr18e<$B$gdO_EDo>#m2kNserG3NKpIJGyA=QhrL zcwU^>X!-qGaZY3H5d&yGqY6JCSq@`Ep2s-tZssMP!#MBpHF5r;535aHd;LUn;{vFT1TU>ZgoO|byu}hqH*V@H2@2_(1Se{*Ho@=MMCV211 z^6XgqTlt>fr|Wet&Br}^=Q8(GI~KT}Ob$}pz2!;!uxT%ASm;A_y#b8=jDFBie^5>{ zgQ31GmrS0v9W`rvYwW|+I~fT9`6thY{&w!wl4rLeqVNtZdXfi0x6Z?)GwEReeG&{@ z#_Chd_Ic3J+v#VPFII~o{`=p2ZRkM81pUNCZw)rii!+`xO;hq%bf4vh)#SNxQ;zLY z;CXRfA{Rb;pA$DooDU~AV(_w$<-%e9oA<55aXkMmqVRq0+mx%>;=H%v8NkF3)wdOpXe#Or9Hm56o*)-MurD=XBLHd7sZ!$7tB)igLMF?p6z)zcuOV_YZmQ zmN}~@!}GS(6PIm~%-IrhwRSC;Pja=id7CP;omNyd!r(;4YSq1|zty%G0Wy|aAcMIF z#zUc&(a?w32bPD_ozd*C-j}t#9`?cQ;%;Bp3u)_J&+j>DnYr!$sP8xBYNxHs8oWJf zk$PgZJ!^kY`g)yBA&zEymTj~-y!O8TuJ{oQb~mN+Uzk_qoOjEI=Ty~Ue`5(bRV*2)`hJzsEGMQN&k0ft zo%V+31J!1Iz|1o7V2<`8cwyegeV zPV-2H{oVKb($5>6ey|V2xl4%e@69LJ{~^QvSFO!*VPqJ!Y&D>vozAXC_Bzfi2ZC2x z{?bkS=guRRBWwZDVv!a06}W(S>>9b*c$UD^M*o;0S+n-{#yE3~F=y>J#2E9x(_YEvuvqe@7*CF|#z$jpbi>}=yJKuL z#znUr+*=ppqA?~~UZsyV#zbR0bYtZnV=x{XW1$;)*aF5v3vtjZXS~8V=&~5-`v0Hy zO1{?7d5wyn{{f5n3$iAxSb2QtqQzOmSo>8R$8I#Wu!|Wz$|7P+Q!Deit=(*++xt4l zwVUd=sm)aTO)aO|ZE7*WDxt+h>y0hEM#s1Haa_^V*(6oYT<>&UYh!3IVKg)f83ziR zW<%T9peLR4R z>}!lo#<=9RtWFrlC1Xr-%bDz6tG^3iJaY3Xj)3vV7>iujYcyaiGR7fyNtm0Damb(2 zzQ6Mt7Go-dai(Q4wzc_dSZu92#@AwOt@`d*z}Q-ht5uC&02o(`F}2R@ei_EpVmz(M zp)G*%v=~dk*7h3M2iE@Dj2is?z4ol{$8a5aF#du5 zfw<>7``1H%@5wO0)e{EUxq#uWcrZK6>d5}I8%#6U+{U4eV6cbXTUiqdeOCoR=NNzJ zlrZ~Bm(6qLu=eA0^cVeNv7MbUz7u0xP183%z}QxdYwbD205Gll0b^Oq;#jS}Xu~BA#^(5|&Ayi28DldsF4Jt?qC$+z#F$LA%vFFf znHZ0$^TQ^viLsD6M_2X1Sjf+KXz5!?N8q!MKxzCCk12Y; zEx{v&ib~_U*k6n+42cCSi9~t0l_}e@=ni3@Z`nh_{ztaL`>@9)w{J!4*M55uiw~0v ztH$2stw<(JG)5#U5y|A?7M{vHt(J+~YJB^)I_|$>;ZUqS;%y1rXYFKhQtG9#QX+-& z+hb+>e4AIcpC^OQs}AM2BGy~{Ip4Bhj>7ZjOKjPAsSC#mlh|U|8GAkZ_Lp0X(`wJq zV2Id^a@qPBJhq0% z)$nyP`1%+;mWHo~fooy#H8A+v7kteNzSad_Ngz}IfzYc}w;8u%Iwd~F84 zCIha=z}I2GH5mBX3w+H5zSe?5*%}LcZ3W4p;A;rr`T>0109-SGuNA=82;gf2@HGMW zS^#_v0ABl_*Zk+T{&|gmUfW-?ra!M?kM-+$-FjZHp4X|D73$Hj`t+nGJ=UY=b?ABh zd0uy(*PG`t8(3qW*Ouos<@q@98tPa-o!3q0_0oBrbRN&ZV;OiobY2IY*FWd63p{Rt z*Etu*E0onWr*R4xqrl@6)L89qUbCCm>gF}Nd2Mc9lbhG#<~6vn{x+|>&FgLRI@`Ry zHjg!sj5FZ#^L%ce&&%^V(R@B0YeVyz(7YBjuK~?#Kl7T;yw)?X@yu&G^P0|l{Bg_Y z*ZJH!uUE|L6!ZGT;<@y)dc=GV9cvKt+QYo&Ft0VtYYg++!n~$1Jw9q)I~VKb@_MG2x^TT{@n9mFIIbmL(l^>sP`5Z8x z|K)X8dA(K1IbS~4`=NPWJ~zteMa6TXtR5h*4a$$rx4Z@@ul>nue)3wMw8kf&>-;g)~V$6DL*uq$>%Tm+$Ep4q;rqvtaZq1 z8&;;KA)k}u^KpDGuCnuRe7@~d=Gu6TIIIuH=b|b*55?!3_&HPAEuT-~b;)==vdX^C zr#_$0r-9iI!}^8jCZ4uIdw7i!X!)uQ>-8Z>yH zo8RO9yZ5*Gy=;CTo8QC!l(EG7)chVbzdz0IP4oNG{GK$wAI^82v=-+Qq99xHD@zqiWotMYrQ{C+CGm&)&>e(rmy z{N5tJugLEy^81PWULwDb$lLunx9p$FYW%(+zbDA=2l9J?{6664Jg($>;e9cFPmJFW ziSW49_8)fgg99@EIvTB6!dWx|HMs(!2mRL|=R z)t*j)Ft`WDpaAkh0rJ#YUK!&Rq5yIXyq(dI-MU!KcqIyDS#hE4wgrg4@ID2*Eg{^i z5Z|X@H$(oBb%$9F4(8rq&JE_;V2%yu)(AN@m`lTQXfSsMb7nAC26JR+ZVcvNVD1Ix zTwtyR=2&2E1?E&>E(PXLVD1FwOkl1A=15>}1kZ`U7<`Pq$C!JJwZ|BHjI9@9>RHS) z#?WKzJjTpptUSiZV{AOe#Iqq1VGOnqd(C35G1eMmtTDbCW2!Nh8e^z2b{b=*F;*I5 zq%k%cW1{i(7=w$kw-|Gav9=gvi?OvBQ;V^*7(@8sl6G&avR!3eKtg({m|!uQRM#>H6eh gtYMr?rk~DbycUUGg|hp*3{`dZLxkx(2EO+H0GBNKqW}N^ literal 0 HcmV?d00001 diff --git a/public/img/docker.png b/public/img/docker.png new file mode 100644 index 0000000000000000000000000000000000000000..8d8b093b34b17bac7842abe16bac431f2eccdf92 GIT binary patch literal 2049 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 literal 0 HcmV?d00001 diff --git a/public/img/jest.png b/public/img/jest.png new file mode 100644 index 0000000000000000000000000000000000000000..43ac87e7c288deba7ca666cc16772e9f71859357 GIT binary patch literal 3401 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 e8E(l%t_T9hLRjvjJFoHo-yF8EH-BVyBI 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 r{53j3A9-(A3QM literal 0 HcmV?d00001 diff --git a/public/img/snake.png b/public/img/snake.png new file mode 100644 index 0000000000000000000000000000000000000000..f45ec092210f51f979b011e4b479f804401c2ffa GIT binary patch literal 3120 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 T{HA{YPgY;&l6JA?^`QR(A7!$d literal 0 HcmV?d00001 diff --git a/public/img/webpack.png b/public/img/webpack.png new file mode 100644 index 0000000000000000000000000000000000000000..cf3b9c0ac5bf7fadfd61f29908d773c462f4a77d GIT binary patch literal 5516 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 literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html index 9776439..a58d43c 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,7 @@ - Redux Demo + Crafity Snake - A React/Redux Demo + + + + + + +
diff --git a/src/components/Checkbox.js b/src/components/Checkbox.js new file mode 100644 index 0000000..ba3b4b9 --- /dev/null +++ b/src/components/Checkbox.js @@ -0,0 +1,49 @@ +import styled from "styled-components"; + +export const Checkbox = styled.input.attrs({ type: "checkbox" })` + appearance: none; + + border-width: ${({ theme }) => theme.checkbox.borderWidth}; + border-style: solid; + border-radius: ${({ theme }) => theme.checkbox.borderRadius}; + border-color: ${({ theme }) => theme.checkbox.borderColor}; + + height: 16px; + width: 16px; + + position: relative; + + &:checked { + &:before { + position: absolute; + content: ""; + display: block; + top: 0; + transform: rotate(-45deg); + border-bottom: 1px solid black; + border-left: 1px solid black; + border-width: 3px; + border-color: ${({ theme }) => theme.checkbox.checked.background}; + width: 10px; + height: 5px; + } + } + + &:focus, + &:hover { + outline: ${({ disabled }) => (!disabled ? "1px solid silver" : "none")}; + } + + &:hover { + background-color: ${({ theme }) => theme.button.hover.background}; + } + + &:active, + &.active { + border-color: ${({ theme }) => theme.button.active.borderColor}; + color: ${({ theme }) => theme.button.active.color}; + background-color: ${({ theme }) => theme.button.active.background}; + } +`; + +export default Checkbox; diff --git a/src/components/Code.js b/src/components/Code.js index 7127ebb..62a199f 100644 --- a/src/components/Code.js +++ b/src/components/Code.js @@ -2,10 +2,12 @@ import styled from "styled-components"; export const Code = styled.code` display: inline-block; - color: white; - background-color: gray; - padding: 0 0.2rem; - line-height: 1.4rem; + font-weight: bold; + background-color: ${({ theme }) => theme.button.background}; + color: ${({ theme }) => theme.button.color}; + padding: 0 0.3rem; + line-height: 1.5rem; + border-radius: 0.125rem; `; export default Code; diff --git a/src/components/ControlPanel.js b/src/components/ControlPanel.js index f7fbcee..5c179f5 100644 --- a/src/components/ControlPanel.js +++ b/src/components/ControlPanel.js @@ -1,6 +1,8 @@ import React from "react"; import styled from "styled-components"; import { IconButton, ToggleIconButton } from "../components/Button.js"; +import Link from "../components/Link.js"; +import Checkbox from "../components/Checkbox.js"; const buttonSize = 2.5; @@ -33,9 +35,26 @@ export const ControlPanel = ({ zoom, setFps, zoomIn, - zoomOut + zoomOut, + allowWrapping, + changeAllowWrapping }) => ( + + + How to play? + + + + +
+ changeAllowWrapping(checked)} + /> +
+
Zoom: {zoom} diff --git a/src/components/Highscore.js b/src/components/Highscore.js index d1fc7ab..38d05ec 100644 --- a/src/components/Highscore.js +++ b/src/components/Highscore.js @@ -48,7 +48,6 @@ const HighscoreEntry = styled.li` `; const Highscore = ({ highscores = [] }) => { - console.log("highscores", highscores); return ( Highscore diff --git a/src/components/Link.js b/src/components/Link.js index ada3712..c6df2d9 100644 --- a/src/components/Link.js +++ b/src/components/Link.js @@ -1,5 +1,6 @@ import styled from "styled-components"; import { NavLink } from "react-router-dom"; +import { HashLink } from "react-router-hash-link"; const SharedStyle = () => ` color: gray; @@ -19,6 +20,13 @@ const StyledNavLink = styled(NavLink)` } `; +const StyledHashLink = styled(HashLink)` + ${SharedStyle} + &.active { + color: black; + } +`; + const Link = (props, refs) => { const updatedProps = { ...props }; @@ -26,7 +34,11 @@ const Link = (props, refs) => { updatedProps.onClick = e => e.preventDefault() || props.onClick(); } - return props.to ? StyledNavLink.render(updatedProps) : StyledLink.render(updatedProps); + return props.to + ? props.to.match("#") + ? StyledHashLink.render(updatedProps) + : StyledNavLink.render(updatedProps) + : StyledLink.render(updatedProps); }; export default Link; diff --git a/src/components/PageSection.js b/src/components/PageSection.js index a2c4777..6920c0d 100644 --- a/src/components/PageSection.js +++ b/src/components/PageSection.js @@ -6,8 +6,11 @@ export const Content = styled.div` content === PageSection.content.center && css` max-width: 60rem; + min-height: 31.25rem; + display: flex; + flex-direction: column; + justify-content: center; margin: 0 auto; - // padding: 0.5em; `} `; @@ -16,10 +19,12 @@ const contentPosition = { stretch: "stretch" }; -export const PageSection = ({ content, children, className } = { content: contentPosition.stretch }) => { +export const PageSection = ({ content, children, className, center, stretch } = {}) => { return (
- {children} + +
{children}
+
); }; diff --git a/src/components/Stage.js b/src/components/Stage.js index ef57f56..462aeb2 100644 --- a/src/components/Stage.js +++ b/src/components/Stage.js @@ -22,12 +22,12 @@ const Cell = styled.div.attrs(({ theme, zoom }) => ({ box-shadow: ${({ theme }) => theme.stage.cell.boxShadow}; `; -export const Stage = ({ data, children, zoom = 1 }) => ( +export const Stage = ({ grid, children, zoom = 1 }) => (
- {data.map((r, y) => ( - + {grid.map((r, y) => ( + {r.map((c, x) => ( - + {children(c, zoom)} ))} diff --git a/src/components/ThemeSelector.js b/src/components/ThemeSelector.js index c0a31cd..b5ba5a9 100644 --- a/src/components/ThemeSelector.js +++ b/src/components/ThemeSelector.js @@ -38,6 +38,7 @@ const ThemeSelector = ({ theme, changeTheme, border }) => { changeTheme(e.target.value)} value={theme}> + diff --git a/src/components/Title.js b/src/components/Title.js index 080b804..d939cbb 100644 --- a/src/components/Title.js +++ b/src/components/Title.js @@ -1,10 +1,19 @@ import styled, { css } from "styled-components"; -const Title = styled.h1` +const Title = styled.h1.attrs(({ id }) => ({ + id +}))` margin: 0; font-size: 1.5rem; line-height: 4rem; + ${props => + props.smaller && + css` + font-size: 1.2rem; + line-height: 3rem; + `} + ${props => props.large && css` diff --git a/src/containers/About.js b/src/containers/About.js index 67576cf..1d64a85 100644 --- a/src/containers/About.js +++ b/src/containers/About.js @@ -3,6 +3,8 @@ import styled, { css } from "styled-components"; import Title from "../components/Title.js"; import PageSection from "../components/PageSection.js"; import Code from "../components/Code.js"; +import Button from "../components/Button.js"; +import Link from "../components/Link.js"; const MainPageSection = styled(PageSection)` background-color: ${({ theme }) => theme.colors.pageSectionMainColor}; @@ -54,9 +56,32 @@ const Layout = styled.div` `} `; +const StyledAboutPage = styled.div` + font-size: 1.5rem; +`; + +const KeyMapTable = styled.table` + margin: 3rem 0; + thead td { + font-size: 0.7em; + color: ${({ theme }) => theme.colors.colorAlternate}; + } + + td:nth-child(1), + td:nth-child(2) { + padding: 0 1rem; + text-align: center; + } + td:nth-child(3) { + padding: 0 1rem; + line-height: 2.3rem; + font-size: 0.8em; + } +`; + export const About = () => ( - - + + Crafity Snake @@ -65,58 +90,204 @@ export const About = () => ( 🐍 - + Introduction

- This experiment has been created to test out React, Redux and Styled Components and see if it is possible to - create a simple web based game like snake. The source code for this game is open source and free to download and - change. + This game has been created as a Crafity experiment to test out React, Redux and Styled Components and see + if it is possible to create a simple web based game like snake. The{" "} + + source code + {" "} + for this game is open source and free to download and change.

- A couple of noticable features in this game are resumable game play using local storage. The page can be + A couple of noticeable features in this game are resumable game play using local storage. The page can be reloaded or reopened any time and the game should continue from where it was left.

- - How to play + + How to play

- You control the snake using the arrow keys or the hjkl keys. Everytime the snake eats an apple the - tail of the snake grows longer. When the snake touches his own body the game is over. + In this game you play a snake and the goal is to eat as many apples as possible. Every time your snake eats an + apple its tail grows a little bit longer and you earn a point. If your snake touches its own tail or one of the + four walls* it will die.

- Other keys to control the game are r to (re)start the game. The s key to stop the game - and p to pause the game. + * Only when allow wrapping is turned off +

+ + + + Key + Alternative + + + + + + + r + + + (Re)start the game + + + + s + + + Stop the game + + + + p + + + Pause the game + + + + h + + + + + Left + + + + j + + + + + Down + + + + k + + + + + Up + + + + l + + + + + Right + + + + w + + + + + Allow Wrapping + + + + + + + + = + + Increase FPS + + + + - + + + Decrease FPS + + + + 0...9 + + + Zoom level + + + +

+

- + Technology

Snake has been developed with the folowing technologies.

-
    -
  • React + React Router
  • -
  • Redux
  • -
  • Styled Components
  • -
  • Webpack + Babel
  • -
  • Jest
  • -
  • ESLint
  • -
  • pre-commit
  • -
  • Docker
  • -
+ +
    +
  • React
  • +
  • React Router
  • +
  • Redux
  • +
  • Styled Components
  • +
  • Webpack
  • +
  • Babel
  • +
+
    +
  • Font Awesome
  • +
  • Jest
  • +
  • ESLint
  • +
  • pre-commit
  • +
  • Docker
  • +
+
+ + React Logo + Redux Logo + Styled Components Logo + Font Awesome Logo + Webpack Logo + Jest Logo + Docker Logo + + State Management +

+ All the game and UI state is managed with Redux. This means all the state is stored in a central state store. + Every second the state is serialized and stored in the browser"s localStorage. This makes the game fully + resumable. +

+

Change is changed using Redux reducers and the orchistration of actions is handled by Redux Middleware.

+ Styling +

+ All the styling and theming is handled by the module styled-components. Styled-components let"s you style + your react components directly from your Javascript code. +

- - Source Code -

The source code is hosted on Crafity's git repositories at the following location:

+ + Source Code +

The source code is hosted on Crafity's Gitea environment at the following location:

- Crafity Snake -

-

After donwloading the source code run the following commands:

-

- npm install + + Download here +

- npm run dev + The source code is MIT licensed. Feel free to make modifications and share. Suggestions and pull requests are + welcome. Use Crafity"s git repository to file issues and pull requests.

-
+ + Running Snake +

After donwloading the source code run the following commands:

+ +

$ npm install

+

$ npm run dev

+
+
+ ); export default About; diff --git a/src/containers/Empty.js b/src/containers/Empty.js new file mode 100644 index 0000000..45c51da --- /dev/null +++ b/src/containers/Empty.js @@ -0,0 +1,7 @@ +import React from "react"; + +export const Empty = () => { + return

lskdfj

; +}; + +export default Empty; diff --git a/src/containers/Snake.js b/src/containers/Snake.js index f81918b..7749a60 100644 --- a/src/containers/Snake.js +++ b/src/containers/Snake.js @@ -12,6 +12,7 @@ import { pauseSnake, updateFrameSnake, keyPressedSnake, + changeAllowWrapping, zoomIn, zoomOut } from "../redux/snake.js"; @@ -68,7 +69,9 @@ const Snake = ({ lastGameId, zoom, zoomIn, - zoomOut + zoomOut, + allowWrapping, + changeAllowWrapping }) => { const onSubmitHighscore = result => { if (result) { @@ -82,10 +85,10 @@ const Snake = ({ - + {cell => - (cell.type === "apple" && ) || - (cell.type === "snake" && ) + (cell.type === "apple" && ) || + (cell.type === "snake" && ) } {!started && ( @@ -101,26 +104,30 @@ const Snake = ({ )} {died && lastGameId !== gameId && hasHighscore(score) && ( - + )} - + @@ -148,7 +155,8 @@ const mapActionsToProps = { zoomOut, changeTheme, registerHighscore, - skipHighscore + skipHighscore, + changeAllowWrapping }; export default connect( diff --git a/src/enums/keys.js b/src/enums/keys.js index 44cb924..2f37b58 100644 --- a/src/enums/keys.js +++ b/src/enums/keys.js @@ -16,6 +16,7 @@ export const keys = { p: "p", s: "s", r: "r", + w: "w", 1: "1", 2: "2", diff --git a/src/index.js b/src/index.js index d57c564..db3fcdf 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import React from "react"; import ReactDOM from "react-dom"; -import { Provider } from "react-redux"; +import { Provider, ReactReduxContext } from "react-redux"; import { ConnectedRouter } from "connected-react-router"; import { createStore } from "./redux/store"; @@ -13,8 +13,8 @@ const store = createStore(); window.store = store; ReactDOM.render( - - + + , diff --git a/src/redux/Module.js b/src/redux/Module.js index e94b916..4dd1c8c 100644 --- a/src/redux/Module.js +++ b/src/redux/Module.js @@ -214,7 +214,6 @@ export class Module { select(...items) { return (items || []).reduce((selection, item) => { - console.log("item", item); return selection; }, {}); } diff --git a/src/redux/snake.js b/src/redux/snake.js index c299a59..f1d970d 100644 --- a/src/redux/snake.js +++ b/src/redux/snake.js @@ -10,6 +10,7 @@ const DEFAUL_ZOOM_STEP = 0.1; const initialState = { grid: createGrid(DEFAULT_GRID_SIZE), zoom: 1.5, + allowWrapping: false, size: DEFAULT_GRID_SIZE, paused: false, gameId: 0, @@ -64,9 +65,12 @@ const updateGrid = ({ grid, apple, snake }) => { ); }; -const moveSnakePart = ([x, y], vX, vY, size) => { +const moveSnakePart = ([x, y], vX, vY, size, allowWrapping) => { let newX = x + vX; let newY = y + vY; + if (!allowWrapping) { + return [newX, newY]; + } if (newX > size - 1) { newX = 0; } @@ -82,6 +86,22 @@ const moveSnakePart = ([x, y], vX, vY, size) => { return [newX, newY]; }; +const isOutOfBound = ([x, y], size) => { + if (x > size - 1) { + return true; + } + if (x < 0) { + return true; + } + if (y > size - 1) { + return true; + } + if (y < 0) { + return true; + } + return false; +}; + export const module = new Module(MODULE_NAME, initialState); /* === Actions ================================================================================== */ @@ -97,6 +117,11 @@ export const [ZOOM_OUT, zoomOut] = module.action("ZOOM_OUT", step => ({ step })) export const [UPDATE_FRAME_SNAKE, updateFrameSnake] = module.action("UPDATE_FRAME_SNAKE"); export const [CHANGE_DIRECTION, changeDirection] = module.action("CHANGE_DIRECTION", direction => ({ direction })); export const [KEY_PRESSED_SNAKE, keyPressedSnake] = module.action("KEY_PRESSED_SNAKE", key => ({ key })); +export const [CHANGE_ALLOW_WRAPPING, changeAllowWrapping] = module.action("CHANGE_ALLOW_WRAPPING", allowWrapping => ({ + allowWrapping +})); +export const [TOGGLE_ALLOW_WRAPPING, toggleAllowWrapping] = module.action("TOGGLE_ALLOW_WRAPPING"); + export const [UPDATE_STATE, updateState] = module.action("UPDATE_STATE", (newState, fullUpdate = false) => ({ newState, fullUpdate @@ -117,11 +142,12 @@ module.reducer(ZOOM_OUT, (state, { step = DEFAUL_ZOOM_STEP }) => module.reducer(RESET_SNAKE, (state, options) => { const grid = createGrid(state.size); const zoom = state.zoom; + const allowWrapping = state.allowWrapping; const gameId = (state.gameId || 0) + 1; const apple = options.started ? randomPosition(state.size) : initialState.apple; const snake = options.started ? [[0, 0]] : initialState.snake; updateGrid({ grid, apple, snake }); - return { ...state, ...initialState, grid, zoom, snake, apple, gameId, ...options }; + return { ...state, ...initialState, grid, allowWrapping, zoom, snake, apple, gameId, ...options }; }); module.reducer(CHANGE_DIRECTION, (state, { direction }) => { @@ -145,31 +171,39 @@ module.reducer(CHANGE_DIRECTION, (state, { direction }) => { }); module.reducer(UPDATE_FRAME_SNAKE, state => { - const { snake, vX, vY, next, size } = state; + const { snake, vX, vY, next, size, allowWrapping } = state; let { apple, started, died, score } = state; const grid = createGrid(size); const [vXNext, vYNext] = next.length ? next[0] : [vX, vY]; - const nextPosition = moveSnakePart(snake[snake.length - 1], vXNext, vYNext, size); - const newSnake = [...snake, nextPosition]; + const nextPosition = moveSnakePart(snake[snake.length - 1], vXNext, vYNext, size, allowWrapping); + let newSnake; - /* If the snake did not eat an apple then remove the last part of its tail */ - if (!comparePositions(nextPosition, apple)) { - newSnake.shift(); - } - - /* Check if the snake hits itself */ - if (newSnake.slice(0, -1).filter(snakePart => comparePositions(snakePart, nextPosition)).length) { + if (!allowWrapping && isOutOfBound(nextPosition, size)) { + newSnake = snake; started = false; died = true; - } + } else { + newSnake = [...snake, nextPosition]; - if (comparePositions(nextPosition, apple)) { - apple = randomPosition(size); - // eslint-disable-next-line no-loop-func - while (newSnake.filter(snakePart => comparePositions(snakePart, apple)).length) { + /* If the snake did not eat an apple then remove the last part of its tail */ + if (!comparePositions(nextPosition, apple)) { + newSnake.shift(); + } + + /* Check if the snake hits itself */ + if (newSnake.slice(0, -1).filter(snakePart => comparePositions(snakePart, nextPosition)).length) { + started = false; + died = true; + } + + if (comparePositions(nextPosition, apple)) { apple = randomPosition(size); + // eslint-disable-next-line no-loop-func + while (newSnake.filter(snakePart => comparePositions(snakePart, apple)).length) { + apple = randomPosition(size); + } } } updateGrid({ grid, apple, snake: newSnake }); @@ -188,6 +222,8 @@ module.reducer(UPDATE_FRAME_SNAKE, state => { score }; }); +module.reducer(CHANGE_ALLOW_WRAPPING, (state, { allowWrapping }) => ({ ...state, allowWrapping })); +module.reducer(TOGGLE_ALLOW_WRAPPING, state => ({ ...state, allowWrapping: !state.allowWrapping })); /* === Middleware =============================================================================== */ @@ -259,6 +295,9 @@ module.middleware(KEY_PRESSED_SNAKE, (dispatch, _, { key, altKey, ctrlKey, metaK if (key === keys.r) { dispatch(startSnake()); } + if (key === keys.w) { + dispatch(toggleAllowWrapping()); + } /* Map the number keys to a zoom level */ const keyAsInt = parseInt(key, 10); diff --git a/src/theming/theme.js b/src/theming/theme.js index f323323..c3074a2 100644 --- a/src/theming/theme.js +++ b/src/theming/theme.js @@ -2,11 +2,13 @@ import defaultTheme from "./themes/default.js"; import lightTheme from "./themes/light.js"; import darkTheme from "./themes/dark.js"; import darkOceanTheme from "./themes/darkOcean.js"; +import forestNightTheme from "./themes/forestNight.js"; const themes = { default: defaultTheme, light: lightTheme, dark: darkTheme, + forestNight: forestNightTheme, darkOcean: darkOceanTheme }; diff --git a/src/theming/themes/dark.js b/src/theming/themes/dark.js index 8ec5499..6010945 100644 --- a/src/theming/themes/dark.js +++ b/src/theming/themes/dark.js @@ -18,7 +18,10 @@ export const colors = { spinnerHighlight: "#db7093", cardFoldHighlight: "#ad5a75", cardFoldShadow: "#bdb19a", - selectColor: "#db7093" + selectColor: "#db7093", + pageSectionMainColor: "#333", + pageSectionStandardColor: "#444", + pageSectionAlternateColor: "#555" }; const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min); diff --git a/src/theming/themes/darkOcean.js b/src/theming/themes/darkOcean.js index e0147f0..4a3517c 100644 --- a/src/theming/themes/darkOcean.js +++ b/src/theming/themes/darkOcean.js @@ -3,11 +3,11 @@ import { renderTheme } from "./default.js"; export const colors = { background: "#333", - backgroundActive: "#828282", + backgroundActive: "transparent", backgroundInactive: "#4d4d4d", backgroundAlternate: "#555", color: "#7094db", - colorActive: "#ecb1c5", + colorActive: "#7094db", colorInactive: "#7d98d4", colorAlternate: "#a1c0fc", shadowColor: "#222", @@ -18,7 +18,10 @@ export const colors = { spinnerHighlight: "#db7093", cardFoldHighlight: "#091225", cardFoldShadow: "#bdb19a", - selectColor: "#7094db" + selectColor: "#7094db", + pageSectionMainColor: "#333", + pageSectionStandardColor: "#444", + pageSectionAlternateColor: "#555" }; const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min); diff --git a/src/theming/themes/default.js b/src/theming/themes/default.js index f5b2a6c..e5362fb 100644 --- a/src/theming/themes/default.js +++ b/src/theming/themes/default.js @@ -64,6 +64,14 @@ export const renderTheme = themeColors => { } } }, + checkbox: { + borderWidth: `${2 / 16}rem`, + borderRadius: `${3 / 16}rem`, + borderColor: colors.color, + checked: { + background: colors.colorAlternate + } + }, button: { borderColor: colors.borderColor, background: colors.color, diff --git a/src/theming/themes/forestNight.js b/src/theming/themes/forestNight.js new file mode 100644 index 0000000..11fbe94 --- /dev/null +++ b/src/theming/themes/forestNight.js @@ -0,0 +1,52 @@ +import merge from "deepmerge"; +import { renderTheme } from "./default.js"; + +export const colors = { + background: "#333", + backgroundActive: "#828282", + backgroundInactive: "#4d4d4d", + backgroundAlternate: "#555", + // color: "#84db70", + color: "#74EA4D", + colorActive: "#ecb1c5", + colorInactive: "#addda2", + colorAlternate: "#55af41", + shadowColor: "#222", + borderColor: "#222", + borderColorActive: "silver", + borderColorInactive: "#addda2", + spinnerShadow: "#444", + spinnerHighlight: "#db7093", + cardFoldHighlight: "#091225", + cardFoldShadow: "#bdb19a", + selectColor: "#addda2", + pageSectionMainColor: "#333", + pageSectionStandardColor: "#444", + pageSectionAlternateColor: "#555" +}; + +const mapRange = (l, i, min, max) => Math.round((max - min) * ((l - i) / l) + min); + +export const darkTheme = merge(renderTheme(colors), { + snakePart: { + boxShadow: "1px 1px 2px #888 inset, -1px -1px 2px #222 inset", + getColor(length, index, died) { + const hue = 109; + if (!died) { + const saturation = mapRange(length, index, 20, 65); + const brightness = 61; + return `hsl(${hue}, ${saturation}%, ${brightness}%)`; + } + const saturation = 0; + const brightness = mapRange(length, index, 70, 0); + return `hsl(${hue}, ${saturation}%, ${brightness}%)`; + } + }, + stage: { + cell: { + boxShadow: "1px 1px 3px #191919 inset, -1px -1px 1px #777777 inset" + } + } +}); + +export default darkTheme; diff --git a/webpack.config.js b/webpack.config.js index d4449ab..bc3c9c3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -17,6 +17,15 @@ module.exports = { port: 9000, historyApiFallback: true }, + externals: { + react: "React", + React: "React", + redux: "Redux", + "react-dom": "ReactDOM", + "react-redux": "ReactRedux", + "react-router": "ReactRouter" + // "react-router-dom": "ReactRouterDOM" + }, plugins: [new webpack.HotModuleReplacementPlugin()], module: { rules: [ diff --git a/webpack.production.config.js b/webpack.production.config.js index b7fc58e..dbb2f01 100644 --- a/webpack.production.config.js +++ b/webpack.production.config.js @@ -1,4 +1,3 @@ -const webpack = require("webpack"); const path = require("path"); module.exports = { @@ -6,10 +5,15 @@ module.exports = { mode: "production", entry: path.resolve(__dirname, "src/index.js"), output: { path: path.resolve(__dirname, "public") }, - plugins: [new webpack.HotModuleReplacementPlugin()], - devServer: { - hot: false, - inline: false + externals: { + react: "React", + React: "React", + redux: "Redux", + "react-dom": "ReactDOM", + "react-redux": "ReactRedux", + "react-router": "ReactRouter", + "styled-components": "styled" + // "react-router-dom": "ReactRouterDOM" }, module: { rules: [