├── .babelrc ├── docs ├── music.mp3 ├── css-1.0.1.css.map ├── index.html ├── loader.css └── css-1.0.1.css ├── src ├── resource │ ├── image │ │ ├── icon.png │ │ └── share.png │ ├── music │ │ ├── music.mp3 │ │ └── music.wav │ └── css │ │ └── loader.css ├── components │ ├── next │ │ ├── index.less │ │ └── index.js │ ├── keyboard │ │ ├── index.less │ │ ├── button │ │ │ ├── index.js │ │ │ └── index.less │ │ └── index.js │ ├── matrix │ │ ├── index.less │ │ └── index.js │ ├── music │ │ ├── index.less │ │ └── index.js │ ├── pause │ │ ├── index.less │ │ └── index.js │ ├── number │ │ ├── index.less │ │ └── index.js │ ├── logo │ │ ├── index.less │ │ └── index.js │ ├── decorate │ │ ├── index.less │ │ └── index.js │ ├── point │ │ └── index.js │ └── guide │ │ ├── index.less │ │ └── index.js ├── store │ └── index.js ├── reducers │ ├── keyboard │ │ ├── down.js │ │ ├── drop.js │ │ ├── left.js │ │ ├── music.js │ │ ├── pause.js │ │ ├── reset.js │ │ ├── right.js │ │ ├── rotate.js │ │ └── index.js │ ├── focus │ │ └── index.js │ ├── reset │ │ └── index.js │ ├── drop │ │ └── index.js │ ├── lock │ │ └── index.js │ ├── pause │ │ └── index.js │ ├── next │ │ └── index.js │ ├── matrix │ │ └── index.js │ ├── clearLines │ │ └── index.js │ ├── speedRun │ │ └── index.js │ ├── speedStart │ │ └── index.js │ ├── startLines │ │ └── index.js │ ├── max │ │ └── index.js │ ├── points │ │ └── index.js │ ├── music │ │ └── index.js │ ├── cur │ │ └── index.js │ └── index.js ├── control │ ├── todo │ │ ├── index.js │ │ ├── s.js │ │ ├── p.js │ │ ├── r.js │ │ ├── rotate.js │ │ ├── left.js │ │ ├── right.js │ │ ├── space.js │ │ └── down.js │ ├── index.js │ └── states.js ├── index.js ├── unit │ ├── reducerType.js │ ├── event.js │ ├── music.js │ ├── const.js │ ├── index.js │ └── block.js ├── actions │ ├── keyboard.js │ └── index.js └── containers │ ├── loader.less │ ├── index.less │ └── index.js ├── webpack.config.js ├── webpack.production.config.js ├── .eslintrc.js ├── server ├── index.tmpl.html └── index.html ├── .gitignore ├── package.json ├── i18n.json ├── w.config.js ├── README.md └── README-EN.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chvin/react-tetris/HEAD/docs/music.mp3 -------------------------------------------------------------------------------- /src/resource/image/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chvin/react-tetris/HEAD/src/resource/image/icon.png -------------------------------------------------------------------------------- /docs/css-1.0.1.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"css-1.0.1.css","sourceRoot":""} -------------------------------------------------------------------------------- /src/resource/image/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chvin/react-tetris/HEAD/src/resource/image/share.png -------------------------------------------------------------------------------- /src/resource/music/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chvin/react-tetris/HEAD/src/resource/music/music.mp3 -------------------------------------------------------------------------------- /src/resource/music/music.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chvin/react-tetris/HEAD/src/resource/music/music.wav -------------------------------------------------------------------------------- /src/components/next/index.less: -------------------------------------------------------------------------------- 1 | .next{ 2 | div{ 3 | height: 22px; 4 | width: 88px; 5 | float: right; 6 | } 7 | } -------------------------------------------------------------------------------- /src/components/keyboard/index.less: -------------------------------------------------------------------------------- 1 | .keyboard{ 2 | width: 580px; 3 | height: 330px; 4 | margin: 20px auto 0; 5 | position:relative; 6 | } -------------------------------------------------------------------------------- /src/components/matrix/index.less: -------------------------------------------------------------------------------- 1 | .matrix{ 2 | border:2px solid #000; 3 | padding:3px 1px 1px 3px; 4 | width:228px; 5 | p{ 6 | width:220px; 7 | height:22px; 8 | } 9 | } -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux'; 2 | import rootReducer from '../reducers'; 3 | 4 | const store = createStore(rootReducer, window.devToolsExtension && window.devToolsExtension()); 5 | 6 | export default store; 7 | -------------------------------------------------------------------------------- /src/components/music/index.less: -------------------------------------------------------------------------------- 1 | .music{ 2 | width: 25px; 3 | height: 21px; 4 | background-position: -175px -75px; 5 | position: absolute; 6 | top: 2px; 7 | left: -12px; 8 | &.c{ 9 | background-position: -150px -75px; 10 | } 11 | } -------------------------------------------------------------------------------- /src/components/pause/index.less: -------------------------------------------------------------------------------- 1 | .pause { 2 | width: 20px; 3 | height: 18px; 4 | background-position: -100px -75px; 5 | position: absolute; 6 | top: 3px; 7 | left: 18px; 8 | &.c { 9 | background-position: -75px -75px; 10 | } 11 | } -------------------------------------------------------------------------------- /src/reducers/keyboard/down.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | 3 | const initState = false; 4 | 5 | const reducer = (state = initState, action) => { 6 | switch (action.type) { 7 | case reducerType.KEY_DOWN: 8 | return action.data; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default reducer; 15 | -------------------------------------------------------------------------------- /src/reducers/keyboard/drop.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | 3 | const initState = false; 4 | 5 | const reducer = (state = initState, action) => { 6 | switch (action.type) { 7 | case reducerType.KEY_DROP: 8 | return action.data; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default reducer; 15 | -------------------------------------------------------------------------------- /src/reducers/keyboard/left.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | 3 | const initState = false; 4 | 5 | const reducer = (state = initState, action) => { 6 | switch (action.type) { 7 | case reducerType.KEY_LEFT: 8 | return action.data; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default reducer; 15 | -------------------------------------------------------------------------------- /src/reducers/keyboard/music.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | 3 | const initState = false; 4 | 5 | const reducer = (state = initState, action) => { 6 | switch (action.type) { 7 | case reducerType.KEY_MUSIC: 8 | return action.data; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default reducer; 15 | -------------------------------------------------------------------------------- /src/reducers/keyboard/pause.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | 3 | const initState = false; 4 | 5 | const reducer = (state = initState, action) => { 6 | switch (action.type) { 7 | case reducerType.KEY_PAUSE: 8 | return action.data; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default reducer; 15 | -------------------------------------------------------------------------------- /src/reducers/keyboard/reset.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | 3 | const initState = false; 4 | 5 | const reducer = (state = initState, action) => { 6 | switch (action.type) { 7 | case reducerType.KEY_RESET: 8 | return action.data; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default reducer; 15 | -------------------------------------------------------------------------------- /src/reducers/keyboard/right.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | 3 | const initState = false; 4 | 5 | const reducer = (state = initState, action) => { 6 | switch (action.type) { 7 | case reducerType.KEY_RIGHT: 8 | return action.data; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default reducer; 15 | -------------------------------------------------------------------------------- /src/reducers/keyboard/rotate.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | 3 | const initState = false; 4 | 5 | const reducer = (state = initState, action) => { 6 | switch (action.type) { 7 | case reducerType.KEY_ROTATE: 8 | return action.data; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default reducer; 15 | -------------------------------------------------------------------------------- /src/control/todo/index.js: -------------------------------------------------------------------------------- 1 | import left from './left'; 2 | import right from './right'; 3 | import down from './down'; 4 | import rotate from './rotate'; 5 | import space from './space'; 6 | import s from './s'; 7 | import r from './r'; 8 | import p from './p'; 9 | 10 | export default { 11 | left, 12 | down, 13 | rotate, 14 | right, 15 | space, 16 | r, 17 | p, 18 | s, 19 | }; 20 | -------------------------------------------------------------------------------- /src/reducers/focus/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { isFocus } from '../../unit/'; 3 | 4 | const initState = isFocus(); 5 | const focus = (state = initState, action) => { 6 | switch (action.type) { 7 | case reducerType.FOCUS: 8 | return action.data; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default focus; 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var config = require('./w.config'); 2 | 3 | // dev环境配置 4 | module.exports = { 5 | devtool: config.devtool, 6 | entry: config.entry, 7 | output: { 8 | path: __dirname + '/server', 9 | filename: 'app.js', 10 | }, 11 | eslint: config.eslint, 12 | module: { 13 | loaders: config.loaders 14 | }, 15 | plugins: config.devPlugins, 16 | devServer: config.devServer, 17 | postcss: config.postcss 18 | }; 19 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | var config = require('./w.config'); 2 | 3 | // production环境配置 4 | module.exports = { 5 | devtool: config.devtool, 6 | entry: config.entry, 7 | output: { 8 | path: __dirname + '/docs', 9 | filename: 'app-' + config.version+'.js', 10 | }, 11 | eslint: config.eslint, 12 | module: { 13 | loaders: config.loaders 14 | }, 15 | plugins: config.productionPlugins, 16 | postcss: config.postcss 17 | }; 18 | -------------------------------------------------------------------------------- /src/reducers/reset/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { lastRecord } from '../../unit/const'; 3 | 4 | const initState = lastRecord && lastRecord.reset ? !!lastRecord.reset : false; 5 | const reset = (state = initState, action) => { 6 | switch (action.type) { 7 | case reducerType.RESET: 8 | return action.data; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default reset; 15 | -------------------------------------------------------------------------------- /src/reducers/drop/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { lastRecord } from '../../unit/const'; 3 | 4 | const initState = lastRecord && lastRecord.drop !== undefined ? !!lastRecord.drop : false; 5 | 6 | const drop = (state = initState, action) => { 7 | switch (action.type) { 8 | case reducerType.DROP: 9 | return action.data; 10 | default: 11 | return state; 12 | } 13 | }; 14 | 15 | export default drop; 16 | -------------------------------------------------------------------------------- /src/reducers/lock/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { lastRecord } from '../../unit/const'; 3 | 4 | const initState = lastRecord && lastRecord.lock !== undefined ? !!lastRecord.lock : false; 5 | 6 | const lock = (state = initState, action) => { 7 | switch (action.type) { 8 | case reducerType.LOCK: 9 | return action.data; 10 | default: 11 | return state; 12 | } 13 | }; 14 | 15 | export default lock; 16 | -------------------------------------------------------------------------------- /src/reducers/pause/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { lastRecord } from '../../unit/const'; 3 | 4 | const initState = lastRecord && lastRecord.pause !== undefined ? !!lastRecord.pause : false; 5 | const pause = (state = initState, action) => { 6 | switch (action.type) { 7 | case reducerType.PAUSE: 8 | return action.data; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default pause; 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "installedESLint": true, 4 | "plugins": [ 5 | "react" 6 | ], 7 | "rules": { 8 | "react/jsx-filename-extension": [2, { extensions: ['.js','.jsx'] }], 9 | "func-names": [0], 10 | "new-cap": [2, { newIsCap: true ,capIsNew: true, capIsNewExceptions: ['List', 'Map']}], 11 | "linebreak-style": [0] 12 | }, 13 | "env": { 14 | "browser": true 15 | } 16 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import store from './store'; 5 | import App from './containers/'; 6 | import './unit/const'; 7 | import './control'; 8 | import { subscribeRecord } from './unit'; 9 | 10 | subscribeRecord(store); // 将更新的状态记录到localStorage 11 | 12 | render( 13 | 14 | 15 | 16 | , document.getElementById('root') 17 | ); 18 | 19 | -------------------------------------------------------------------------------- /src/reducers/next/index.js: -------------------------------------------------------------------------------- 1 | import { getNextType } from '../../unit'; 2 | import * as reducerType from '../../unit/reducerType'; 3 | import { lastRecord, blockType } from '../../unit/const'; 4 | 5 | const initState = lastRecord && blockType.indexOf(lastRecord.next) !== -1 ? 6 | lastRecord.next : getNextType(); 7 | const parse = (state = initState, action) => { 8 | switch (action.type) { 9 | case reducerType.NEXT_BLOCK: 10 | return action.data; 11 | default: 12 | return state; 13 | } 14 | }; 15 | 16 | export default parse; 17 | -------------------------------------------------------------------------------- /src/reducers/keyboard/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux-immutable'; 2 | import drop from './drop'; 3 | import down from './down'; 4 | import left from './left'; 5 | import right from './right'; 6 | import rotate from './rotate'; 7 | import reset from './reset'; 8 | import music from './music'; 9 | import pause from './pause'; 10 | 11 | const keyboardReducer = combineReducers({ 12 | drop, 13 | down, 14 | left, 15 | right, 16 | rotate, 17 | reset, 18 | music, 19 | pause, 20 | }); 21 | 22 | export default keyboardReducer; 23 | -------------------------------------------------------------------------------- /src/reducers/matrix/index.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | import * as reducerType from '../../unit/reducerType'; 3 | import { blankMatrix, lastRecord } from '../../unit/const'; 4 | 5 | const initState = lastRecord && Array.isArray(lastRecord.matrix) ? 6 | List(lastRecord.matrix.map(e => List(e))) : blankMatrix; 7 | 8 | const matrix = (state = initState, action) => { 9 | switch (action.type) { 10 | case reducerType.MATRIX: 11 | return action.data; 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default matrix; 18 | -------------------------------------------------------------------------------- /src/reducers/clearLines/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { lastRecord } from '../../unit/const'; 3 | 4 | let initState = lastRecord && !isNaN(parseInt(lastRecord.clearLines, 10)) ? 5 | parseInt(lastRecord.clearLines, 10) : 0; 6 | if (initState < 0) { 7 | initState = 0; 8 | } 9 | 10 | const clearLines = (state = initState, action) => { 11 | switch (action.type) { 12 | case reducerType.CLEAR_LINES: 13 | return action.data; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default clearLines; 20 | -------------------------------------------------------------------------------- /src/reducers/speedRun/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { lastRecord } from '../../unit/const'; 3 | 4 | let initState = lastRecord && !isNaN(parseInt(lastRecord.speedRun, 10)) ? 5 | parseInt(lastRecord.speedRun, 10) : 1; 6 | if (initState < 1 || initState > 6) { 7 | initState = 1; 8 | } 9 | 10 | const speedRun = (state = initState, action) => { 11 | switch (action.type) { 12 | case reducerType.SPEED_RUN: 13 | return action.data; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default speedRun; 20 | -------------------------------------------------------------------------------- /src/reducers/speedStart/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { lastRecord } from '../../unit/const'; 3 | 4 | let initState = lastRecord && !isNaN(parseInt(lastRecord.speedStart, 10)) ? 5 | parseInt(lastRecord.speedStart, 10) : 1; 6 | if (initState < 1 || initState > 6) { 7 | initState = 1; 8 | } 9 | 10 | const speedStart = (state = initState, action) => { 11 | switch (action.type) { 12 | case reducerType.SPEED_START: 13 | return action.data; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default speedStart; 20 | -------------------------------------------------------------------------------- /src/reducers/startLines/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { lastRecord } from '../../unit/const'; 3 | 4 | let initState = lastRecord && !isNaN(parseInt(lastRecord.startLines, 10)) ? 5 | parseInt(lastRecord.startLines, 10) : 0; 6 | if (initState < 0 || initState > 10) { 7 | initState = 0; 8 | } 9 | 10 | const startLines = (state = initState, action) => { 11 | switch (action.type) { 12 | case reducerType.START_LINES: 13 | return action.data; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default startLines; 20 | -------------------------------------------------------------------------------- /src/reducers/max/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { lastRecord, maxPoint } from '../../unit/const'; 3 | 4 | let initState = lastRecord && !isNaN(parseInt(lastRecord.max, 10)) ? 5 | parseInt(lastRecord.max, 10) : 0; 6 | 7 | if (initState < 0) { 8 | initState = 0; 9 | } else if (initState > maxPoint) { 10 | initState = maxPoint; 11 | } 12 | 13 | const parse = (state = initState, action) => { 14 | switch (action.type) { 15 | case reducerType.MAX: 16 | return action.data > 999999 ? 999999 : action.data; // 最大分数 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export default parse; 23 | -------------------------------------------------------------------------------- /src/components/music/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cn from 'classnames'; 3 | import propTypes from 'prop-types'; 4 | 5 | import style from './index.less'; 6 | 7 | export default class Music extends React.Component { 8 | shouldComponentUpdate({ data }) { 9 | return data !== this.props.data; 10 | } 11 | render() { 12 | return ( 13 |
22 | ); 23 | } 24 | } 25 | 26 | Music.propTypes = { 27 | data: propTypes.bool.isRequired, 28 | }; 29 | -------------------------------------------------------------------------------- /src/reducers/points/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { lastRecord, maxPoint } from '../../unit/const'; 3 | 4 | let initState = lastRecord && !isNaN(parseInt(lastRecord.points, 10)) ? 5 | parseInt(lastRecord.points, 10) : 0; 6 | 7 | if (initState < 0) { 8 | initState = 0; 9 | } else if (initState > maxPoint) { 10 | initState = maxPoint; 11 | } 12 | 13 | const points = (state = initState, action) => { 14 | switch (action.type) { 15 | case reducerType.POINTS: 16 | return action.data > maxPoint ? maxPoint : action.data; // 最大分数 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export default points; 23 | -------------------------------------------------------------------------------- /src/reducers/music/index.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../../unit/reducerType'; 2 | import { lastRecord } from '../../unit/const'; 3 | import { hasWebAudioAPI } from '../../unit/music'; 4 | 5 | let initState = lastRecord && lastRecord.music !== undefined ? !!lastRecord.music : true; 6 | if (!hasWebAudioAPI.data) { 7 | initState = false; 8 | } 9 | const music = (state = initState, action) => { 10 | switch (action.type) { 11 | case reducerType.MUSIC: 12 | if (!hasWebAudioAPI.data) { // 若浏览器不支持 WebAudioApi, 将无法播放音效 13 | return false; 14 | } 15 | return action.data; 16 | default: 17 | return state; 18 | } 19 | }; 20 | 21 | export default music; 22 | -------------------------------------------------------------------------------- /src/control/todo/s.js: -------------------------------------------------------------------------------- 1 | import event from '../../unit/event'; 2 | import actions from '../../actions'; 3 | 4 | const down = (store) => { 5 | store.dispatch(actions.keyboard.music(true)); 6 | if (store.getState().get('lock')) { 7 | return; 8 | } 9 | event.down({ 10 | key: 's', 11 | once: true, 12 | callback: () => { 13 | if (store.getState().get('lock')) { 14 | return; 15 | } 16 | store.dispatch(actions.music(!store.getState().get('music'))); 17 | }, 18 | }); 19 | }; 20 | 21 | const up = (store) => { 22 | store.dispatch(actions.keyboard.music(false)); 23 | event.up({ 24 | key: 's', 25 | }); 26 | }; 27 | 28 | 29 | export default { 30 | down, 31 | up, 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/number/index.less: -------------------------------------------------------------------------------- 1 | .number{ 2 | height:24px; 3 | font-size:14px; 4 | float:right; 5 | span{ 6 | float:left; 7 | width:14px; 8 | height:24px; 9 | } 10 | .s_0{background-position:-75px -25px;} 11 | .s_1{background-position:-89px -25px;} 12 | .s_2{background-position:-103px -25px;} 13 | .s_3{background-position:-117px -25px;} 14 | .s_4{background-position:-131px -25px;} 15 | .s_5{background-position:-145px -25px;} 16 | .s_6{background-position:-159px -25px;} 17 | .s_7{background-position:-173px -25px;} 18 | .s_8{background-position:-187px -25px;} 19 | .s_9{background-position:-201px -25px;} 20 | .s_n{background-position:-215px -25px;} 21 | .s_d{background-position:-243px -25px;} 22 | .s_d_c{background-position:-229px -25px;} 23 | } -------------------------------------------------------------------------------- /src/reducers/cur/index.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | import * as reducerType from '../../unit/reducerType'; 3 | import { lastRecord } from '../../unit/const'; 4 | import Block from '../../unit/block'; 5 | 6 | const initState = (() => { 7 | if (!lastRecord || !lastRecord.cur) { // 无记录 或 有记录 但方块为空, 返回 null 8 | return null; 9 | } 10 | const cur = lastRecord.cur; 11 | const option = { 12 | type: cur.type, 13 | rotateIndex: cur.rotateIndex, 14 | shape: List(cur.shape.map(e => List(e))), 15 | xy: cur.xy, 16 | }; 17 | return new Block(option); 18 | })(); 19 | 20 | const cur = (state = initState, action) => { 21 | switch (action.type) { 22 | case reducerType.MOVE_BLOCK: 23 | return action.data; 24 | default: 25 | return state; 26 | } 27 | }; 28 | 29 | export default cur; 30 | -------------------------------------------------------------------------------- /src/control/todo/p.js: -------------------------------------------------------------------------------- 1 | import event from '../../unit/event'; 2 | import states from '../states'; 3 | import actions from '../../actions'; 4 | 5 | const down = (store) => { 6 | store.dispatch(actions.keyboard.pause(true)); 7 | event.down({ 8 | key: 'p', 9 | once: true, 10 | callback: () => { 11 | const state = store.getState(); 12 | if (state.get('lock')) { 13 | return; 14 | } 15 | const cur = state.get('cur'); 16 | const isPause = state.get('pause'); 17 | if (cur !== null) { // 暂停 18 | states.pause(!isPause); 19 | } else { // 新的开始 20 | states.start(); 21 | } 22 | }, 23 | }); 24 | }; 25 | 26 | const up = (store) => { 27 | store.dispatch(actions.keyboard.pause(false)); 28 | event.up({ 29 | key: 'p', 30 | }); 31 | }; 32 | 33 | 34 | export default { 35 | down, 36 | up, 37 | }; 38 | -------------------------------------------------------------------------------- /src/unit/reducerType.js: -------------------------------------------------------------------------------- 1 | export const PAUSE = 'PAUSE'; 2 | export const MUSIC = 'MUSIC'; 3 | export const MATRIX = 'MATRIX'; 4 | export const NEXT_BLOCK = 'NEXT_BLOCK'; 5 | export const MOVE_BLOCK = 'MOVE_BLOCK'; 6 | export const START_LINES = 'START_LINES'; 7 | export const MAX = 'MAX'; 8 | export const POINTS = 'POINTS'; 9 | export const SPEED_START = 'SPEED_START'; 10 | export const SPEED_RUN = 'SPEED_RUN'; 11 | export const LOCK = 'LOCK'; 12 | export const CLEAR_LINES = 'CLEAR_LINES'; 13 | export const RESET = 'RESET'; 14 | export const DROP = 'DROP'; 15 | export const KEY_DROP = 'KEY_DROP'; 16 | export const KEY_DOWN = 'KEY_DOWN'; 17 | export const KEY_LEFT = 'KEY_LEFT'; 18 | export const KEY_RIGHT = 'KEY_RIGHT'; 19 | export const KEY_ROTATE = 'KEY_ROTATE'; 20 | export const KEY_RESET = 'KEY_RESET'; 21 | export const KEY_MUSIC = 'KEY_MUSIC'; 22 | export const KEY_PAUSE = 'KEY_PAUSE'; 23 | export const FOCUS = 'FOCUS'; 24 | -------------------------------------------------------------------------------- /src/control/todo/r.js: -------------------------------------------------------------------------------- 1 | import event from '../../unit/event'; 2 | import states from '../states'; 3 | import actions from '../../actions'; 4 | 5 | const down = (store) => { 6 | store.dispatch(actions.keyboard.reset(true)); 7 | if (store.getState().get('lock')) { 8 | return; 9 | } 10 | if (store.getState().get('cur') !== null) { 11 | event.down({ 12 | key: 'r', 13 | once: true, 14 | callback: () => { 15 | states.overStart(); 16 | }, 17 | }); 18 | } else { 19 | event.down({ 20 | key: 'r', 21 | once: true, 22 | callback: () => { 23 | if (store.getState().get('lock')) { 24 | return; 25 | } 26 | states.start(); 27 | }, 28 | }); 29 | } 30 | }; 31 | 32 | const up = (store) => { 33 | store.dispatch(actions.keyboard.reset(false)); 34 | event.up({ 35 | key: 'r', 36 | }); 37 | }; 38 | 39 | export default { 40 | down, 41 | up, 42 | }; 43 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux-immutable'; 2 | import pause from './pause'; 3 | import music from './music'; 4 | import matrix from './matrix'; 5 | import next from './next'; 6 | import cur from './cur'; 7 | import startLines from './startLines'; 8 | import max from './max'; 9 | import points from './points'; 10 | import speedStart from './speedStart'; 11 | import speedRun from './speedRun'; 12 | import lock from './lock'; 13 | import clearLines from './clearLines'; 14 | import reset from './reset'; 15 | import drop from './drop'; 16 | import keyboard from './keyboard'; 17 | import focus from './focus'; 18 | 19 | 20 | const rootReducer = combineReducers({ 21 | pause, 22 | music, 23 | matrix, 24 | next, 25 | cur, 26 | startLines, 27 | max, 28 | points, 29 | speedStart, 30 | speedRun, 31 | lock, 32 | clearLines, 33 | reset, 34 | drop, 35 | keyboard, 36 | focus, 37 | }); 38 | 39 | export default rootReducer; 40 | -------------------------------------------------------------------------------- /server/index.tmpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 俄罗斯方块 11 | 12 | 13 | 14 |
15 |
16 |
加载中...
17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | .DS_Store 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directories 28 | node_modules 29 | jspm_packages 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # Created by .ignore support plugin (hsz.mobi) 38 | .idea 39 | .idea/workspace.xml 40 | .idea/encodings.xml 41 | .idea/jsLibraryMappings.xml 42 | .idea/misc.xml 43 | .idea/modules.xml 44 | .idea/react-tetris.iml 45 | .idea/vcs.xml 46 | .idea/watcherTasks.xml -------------------------------------------------------------------------------- /src/control/index.js: -------------------------------------------------------------------------------- 1 | import store from '../store'; 2 | import todo from './todo'; 3 | 4 | const keyboard = { 5 | 37: 'left', 6 | 38: 'rotate', 7 | 39: 'right', 8 | 40: 'down', 9 | 32: 'space', 10 | 83: 's', 11 | 82: 'r', 12 | 80: 'p', 13 | }; 14 | 15 | let keydownActive; 16 | 17 | const boardKeys = Object.keys(keyboard).map(e => parseInt(e, 10)); 18 | 19 | const keyDown = (e) => { 20 | if (e.metaKey === true || boardKeys.indexOf(e.keyCode) === -1) { 21 | return; 22 | } 23 | const type = keyboard[e.keyCode]; 24 | if (type === keydownActive) { 25 | return; 26 | } 27 | keydownActive = type; 28 | todo[type].down(store); 29 | }; 30 | 31 | const keyUp = (e) => { 32 | if (e.metaKey === true || boardKeys.indexOf(e.keyCode) === -1) { 33 | return; 34 | } 35 | const type = keyboard[e.keyCode]; 36 | if (type === keydownActive) { 37 | keydownActive = ''; 38 | } 39 | todo[type].up(store); 40 | }; 41 | 42 | document.addEventListener('keydown', keyDown, true); 43 | document.addEventListener('keyup', keyUp, true); 44 | 45 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 俄罗斯方块 11 | 12 | 13 | 14 |
15 |
16 |
加载中...
17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 俄罗斯方块 11 | 12 | 13 | 14 | 15 |
16 |
17 |
加载中...
18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/logo/index.less: -------------------------------------------------------------------------------- 1 | .logo { 2 | width: 224px; 3 | height: 200px; 4 | position: absolute; 5 | top: 100px; 6 | left: 12px; 7 | text-align: center; 8 | overflow: hidden; 9 | p { 10 | position: absolute; 11 | width: 100%; 12 | line-height: 1.4; 13 | top: 100px; 14 | left: 0; 15 | font-family: initial; 16 | letter-spacing: 6px; 17 | text-shadow: 1px 1px 1px rgba(255, 255, 255,.35); 18 | } 19 | .dragon { 20 | width: 80px; 21 | height: 86px; 22 | margin: 0 auto; 23 | background-position: 0 -100px; 24 | &.r1,&.l1 { 25 | background-position: 0 -100px; 26 | } 27 | &.r2,&.l2 { 28 | background-position: -100px -100px; 29 | } 30 | &.r3,&.l3 { 31 | background-position: -200px -100px; 32 | } 33 | &.r4,&.l4 { 34 | background-position: -300px -100px; 35 | } 36 | &.l1,&.l2,&.l3,&.l4{ 37 | transform: scale(-1, 1); 38 | -webkit-transform: scale(-1, 1); 39 | -ms-transform: scale(-1, 1); 40 | -moz-transform: scale(-1, 1); 41 | -o-transform: scale(-1, 1); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/components/decorate/index.less: -------------------------------------------------------------------------------- 1 | .decorate{ 2 | h1{ 3 | position: absolute; 4 | width: 100%; 5 | text-align: center; 6 | font-weight: normal; 7 | top: -12px; 8 | left: 0; 9 | margin: 0; 10 | padding: 0; 11 | font-size: 30px; 12 | } 13 | .topBorder{ 14 | position:absolute; 15 | height:10px; 16 | width:100%; 17 | position:absolute; 18 | top:0px; 19 | left:0px; 20 | overflow:hidden; 21 | span{ 22 | display:block; 23 | width:10px; 24 | height:10px; 25 | overflow:hidden; 26 | background:#000; 27 | &.mr{ 28 | margin-right:10px; 29 | } 30 | &.ml{ 31 | margin-left:10px; 32 | } 33 | } 34 | } 35 | .view{ 36 | position: absolute; 37 | right: -70px; 38 | top: 20px; 39 | width: 44px; 40 | em { 41 | display: block; 42 | width: 22px; 43 | height: 22px; 44 | overflow: hidden; 45 | float: left; 46 | } 47 | p { 48 | height: 22px; 49 | clear: both; 50 | } 51 | &.l{ 52 | right: auto; 53 | left: -70px; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/actions/keyboard.js: -------------------------------------------------------------------------------- 1 | import * as reducerType from '../unit/reducerType'; 2 | 3 | function drop(data) { 4 | return { 5 | type: reducerType.KEY_DROP, 6 | data, 7 | }; 8 | } 9 | 10 | function down(data) { 11 | return { 12 | type: reducerType.KEY_DOWN, 13 | data, 14 | }; 15 | } 16 | 17 | function left(data) { 18 | return { 19 | type: reducerType.KEY_LEFT, 20 | data, 21 | }; 22 | } 23 | 24 | function right(data) { 25 | return { 26 | type: reducerType.KEY_RIGHT, 27 | data, 28 | }; 29 | } 30 | 31 | function rotate(data) { 32 | return { 33 | type: reducerType.KEY_ROTATE, 34 | data, 35 | }; 36 | } 37 | 38 | function reset(data) { 39 | return { 40 | type: reducerType.KEY_RESET, 41 | data, 42 | }; 43 | } 44 | 45 | function music(data) { 46 | return { 47 | type: reducerType.KEY_MUSIC, 48 | data, 49 | }; 50 | } 51 | 52 | function pause(data) { 53 | return { 54 | type: reducerType.KEY_PAUSE, 55 | data, 56 | }; 57 | } 58 | 59 | export default { 60 | drop, 61 | down, 62 | left, 63 | right, 64 | rotate, 65 | reset, 66 | music, 67 | pause, 68 | }; 69 | -------------------------------------------------------------------------------- /src/unit/event.js: -------------------------------------------------------------------------------- 1 | const eventName = {}; 2 | 3 | const down = (o) => { // 键盘、手指按下 4 | const keys = Object.keys(eventName); 5 | keys.forEach(i => { 6 | clearTimeout(eventName[i]); 7 | eventName[i] = null; 8 | }); 9 | if (!o.callback) { 10 | return; 11 | } 12 | const clear = () => { 13 | clearTimeout(eventName[o.key]); 14 | }; 15 | o.callback(clear); 16 | if (o.once === true) { 17 | return; 18 | } 19 | let begin = o.begin || 100; 20 | const interval = o.interval || 50; 21 | const loop = () => { 22 | eventName[o.key] = setTimeout(() => { 23 | begin = null; 24 | loop(); 25 | o.callback(clear); 26 | }, begin || interval); 27 | }; 28 | loop(); 29 | }; 30 | 31 | const up = (o) => { // 键盘、手指松开 32 | clearTimeout(eventName[o.key]); 33 | eventName[o.key] = null; 34 | if (!o.callback) { 35 | return; 36 | } 37 | o.callback(); 38 | }; 39 | 40 | const clearAll = () => { 41 | const keys = Object.keys(eventName); 42 | keys.forEach(i => { 43 | clearTimeout(eventName[i]); 44 | eventName[i] = null; 45 | }); 46 | }; 47 | 48 | export default { 49 | down, 50 | up, 51 | clearAll, 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/keyboard/button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cn from 'classnames'; 3 | import propTypes from 'prop-types'; 4 | 5 | import style from './index.less'; 6 | import { transform } from '../../../unit/const'; 7 | 8 | export default class Button extends React.Component { 9 | shouldComponentUpdate(nextProps) { 10 | return nextProps.active !== this.props.active; 11 | } 12 | render() { 13 | const { 14 | active, color, size, top, left, label, position, arrow, 15 | } = this.props; 16 | return ( 17 |
21 | { this.dom = c; }} 24 | /> 25 | { size === 's1' && } 30 | {label} 31 |
32 | ); 33 | } 34 | } 35 | 36 | Button.propTypes = { 37 | color: propTypes.string.isRequired, 38 | size: propTypes.string.isRequired, 39 | top: propTypes.number.isRequired, 40 | left: propTypes.number.isRequired, 41 | label: propTypes.string.isRequired, 42 | position: propTypes.bool, 43 | arrow: propTypes.string, 44 | active: propTypes.bool.isRequired, 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /src/containers/loader.less: -------------------------------------------------------------------------------- 1 | 2 | .load{ 3 | 4 | @-webkit-keyframes loads{ 5 | 0%,80%,100%{ 6 | box-shadow:0 0 #efcc19; 7 | height:4em} 8 | 40%{ 9 | box-shadow:0 -2em #efcc19; 10 | height:5em 11 | } 12 | } 13 | 14 | @keyframes loads{ 15 | 0%,80%,100%{ 16 | box-shadow:0 0 #efcc19; 17 | height:4em 18 | } 19 | 40%{ 20 | box-shadow:0 -2em #efcc19; 21 | height:5em 22 | } 23 | } 24 | 25 | width:240px; 26 | height:240px; 27 | float:left; 28 | position:relative; 29 | color:#fff; 30 | text-align:center; 31 | position:absolute; 32 | top:50%; 33 | left:50%; 34 | margin:-120px 0 0 -120px; 35 | p{ 36 | position:absolute; 37 | bottom:0; 38 | left:-25%; 39 | width:150%; 40 | white-space:nowrap; 41 | display:none; 42 | } 43 | .loader{ 44 | &,&:before,&:after{ 45 | background:#efcc19; 46 | -webkit-animation:loads 1s infinite ease-in-out; 47 | animation:loads 1s infinite ease-in-out; 48 | width:1em; 49 | height:4em 50 | } 51 | &:before,&:after{ 52 | position:absolute;top:0;content:'' 53 | } 54 | &:before{ 55 | left:-1.5em; 56 | -webkit-animation-delay:-0.32s; 57 | animation-delay:-0.32s 58 | } 59 | text-indent:-9999em; 60 | margin:8em auto; 61 | position:relative; 62 | font-size:11px; 63 | -webkit-animation-delay:-0.16s; 64 | animation-delay:-0.16s; 65 | &:after{ 66 | left:1.5em 67 | } 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/components/pause/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cn from 'classnames'; 3 | import propTypes from 'prop-types'; 4 | 5 | import style from './index.less'; 6 | 7 | export default class Pause extends React.Component { 8 | constructor() { 9 | super(); 10 | this.state = { // 控制显示状态 11 | showPause: false, 12 | }; 13 | } 14 | componentDidMount() { 15 | this.setShake(this.props.data); 16 | } 17 | componentWillReceiveProps({ data }) { 18 | this.setShake(data); 19 | } 20 | shouldComponentUpdate({ data }) { 21 | if (data) { // 如果暂停了, 不会有太多的dispatch, 考虑到闪烁效果, 直接返回true 22 | return true; 23 | } 24 | return data !== this.props.data; 25 | } 26 | setShake(bool) { // 根据props显示闪烁或停止闪烁 27 | if (bool && !Pause.timeout) { 28 | Pause.timeout = setInterval(() => { 29 | this.setState({ 30 | showPause: !this.state.showPause, 31 | }); 32 | }, 250); 33 | } 34 | if (!bool && Pause.timeout) { 35 | clearInterval(Pause.timeout); 36 | this.setState({ 37 | showPause: false, 38 | }); 39 | Pause.timeout = null; 40 | } 41 | } 42 | render() { 43 | return ( 44 |
53 | ); 54 | } 55 | } 56 | 57 | Pause.statics = { 58 | timeout: null, 59 | }; 60 | 61 | Pause.propTypes = { 62 | data: propTypes.bool.isRequired, 63 | }; 64 | 65 | Pause.defaultProps = { 66 | data: false, 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/next/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import propTypes from 'prop-types'; 3 | 4 | import style from './index.less'; 5 | import { blockShape } from '../../unit/const'; 6 | 7 | const xy = { // 方块在下一个中的坐标 8 | I: [1, 0], 9 | L: [0, 0], 10 | J: [0, 0], 11 | Z: [0, 0], 12 | S: [0, 0], 13 | O: [0, 1], 14 | T: [0, 0], 15 | }; 16 | 17 | const empty = [ 18 | [0, 0, 0, 0], 19 | [0, 0, 0, 0], 20 | ]; 21 | 22 | export default class Next extends React.Component { 23 | constructor() { 24 | super(); 25 | this.state = { 26 | block: empty, 27 | }; 28 | } 29 | componentWillMount() { 30 | this.build(this.props.data); 31 | } 32 | componentWillReceiveProps(nextProps) { 33 | this.build(nextProps.data); 34 | } 35 | shouldComponentUpdate(nextProps) { 36 | return nextProps.data !== this.props.data; 37 | } 38 | build(type) { 39 | const shape = blockShape[type]; 40 | const block = empty.map(e => ([...e])); 41 | shape.forEach((m, k1) => { 42 | m.forEach((n, k2) => { 43 | if (n) { 44 | block[k1 + xy[type][0]][k2 + xy[type][1]] = 1; 45 | } 46 | }); 47 | }); 48 | this.setState({ block }); 49 | } 50 | render() { 51 | return ( 52 |
53 | { 54 | this.state.block.map((arr, k1) => ( 55 |
56 | { 57 | arr.map((e, k2) => ( 58 | 59 | )) 60 | } 61 |
62 | )) 63 | } 64 |
65 | ); 66 | } 67 | } 68 | 69 | Next.propTypes = { 70 | data: propTypes.string, 71 | }; 72 | -------------------------------------------------------------------------------- /docs/loader.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background: #009688; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | .load{ 7 | width:240px; 8 | height:240px; 9 | position:absolute; 10 | top:50%; 11 | left:50%; 12 | margin:-120px 0 0 -120px; 13 | color:#efcc19; 14 | -webkit-animation:fadeIn 2s infinite ease-in-out; 15 | animation:fadeIn 2s infinite ease-in-out; 16 | -webkit-animation-delay:2s; 17 | animation-delay:2s; 18 | opacity:0; 19 | } 20 | .load .loader,.load .loader:before,.load .loader:after{ 21 | background:#efcc19; 22 | -webkit-animation:load 1s infinite ease-in-out; 23 | animation:load 1s infinite ease-in-out; 24 | width:1em; 25 | height:4em 26 | } 27 | .load .loader:before,.load .loader:after{ 28 | position:absolute; 29 | top:0; 30 | content:'' 31 | } 32 | .load .loader:before{ 33 | left:-1.5em; 34 | -webkit-animation-delay:-0.32s; 35 | animation-delay:-0.32s 36 | } 37 | .load .loader{ 38 | text-indent:-9999em; 39 | margin:8em auto; 40 | position:relative; 41 | font-size:11px; 42 | -webkit-animation-delay:-0.16s; 43 | animation-delay:-0.16s 44 | } 45 | .load .loader:after{ 46 | left:1.5em 47 | } 48 | @-webkit-keyframes load{ 49 | 0%,80%,100%{ 50 | box-shadow:0 0 #efcc19; 51 | height:4em 52 | } 53 | 40%{ 54 | box-shadow:0 -2em #efcc19;height:5em 55 | } 56 | } 57 | 58 | @keyframes load{ 59 | 0%,80%,100%{ 60 | box-shadow:0 0 #efcc19; 61 | height:4em 62 | } 63 | 40%{ 64 | box-shadow:0 -2em #efcc19; 65 | height:5em 66 | } 67 | } 68 | 69 | @-webkit-keyframes fadeIn{ 70 | 0%{ 71 | opacity:0; 72 | } 73 | 100%{ 74 | opacity:1; 75 | } 76 | } 77 | @keyframes fadeIn{ 78 | 0%{ 79 | opacity:0; 80 | } 81 | 100%{ 82 | opacity:1; 83 | } 84 | } -------------------------------------------------------------------------------- /src/resource/css/loader.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background: #009688; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | .load{ 7 | width:240px; 8 | height:240px; 9 | position:absolute; 10 | top:50%; 11 | left:50%; 12 | margin:-120px 0 0 -120px; 13 | color:#efcc19; 14 | -webkit-animation:fadeIn 2s infinite ease-in-out; 15 | animation:fadeIn 2s infinite ease-in-out; 16 | -webkit-animation-delay:2s; 17 | animation-delay:2s; 18 | opacity:0; 19 | } 20 | .load .loader,.load .loader:before,.load .loader:after{ 21 | background:#efcc19; 22 | -webkit-animation:load 1s infinite ease-in-out; 23 | animation:load 1s infinite ease-in-out; 24 | width:1em; 25 | height:4em 26 | } 27 | .load .loader:before,.load .loader:after{ 28 | position:absolute; 29 | top:0; 30 | content:'' 31 | } 32 | .load .loader:before{ 33 | left:-1.5em; 34 | -webkit-animation-delay:-0.32s; 35 | animation-delay:-0.32s 36 | } 37 | .load .loader{ 38 | text-indent:-9999em; 39 | margin:8em auto; 40 | position:relative; 41 | font-size:11px; 42 | -webkit-animation-delay:-0.16s; 43 | animation-delay:-0.16s 44 | } 45 | .load .loader:after{ 46 | left:1.5em 47 | } 48 | @-webkit-keyframes load{ 49 | 0%,80%,100%{ 50 | box-shadow:0 0 #efcc19; 51 | height:4em 52 | } 53 | 40%{ 54 | box-shadow:0 -2em #efcc19;height:5em 55 | } 56 | } 57 | 58 | @keyframes load{ 59 | 0%,80%,100%{ 60 | box-shadow:0 0 #efcc19; 61 | height:4em 62 | } 63 | 40%{ 64 | box-shadow:0 -2em #efcc19; 65 | height:5em 66 | } 67 | } 68 | 69 | @-webkit-keyframes fadeIn{ 70 | 0%{ 71 | opacity:0; 72 | } 73 | 100%{ 74 | opacity:1; 75 | } 76 | } 77 | @keyframes fadeIn{ 78 | 0%{ 79 | opacity:0; 80 | } 81 | 100%{ 82 | opacity:1; 83 | } 84 | } -------------------------------------------------------------------------------- /src/control/todo/rotate.js: -------------------------------------------------------------------------------- 1 | import { want } from '../../unit/'; 2 | import event from '../../unit/event'; 3 | import actions from '../../actions'; 4 | import states from '../states'; 5 | import { music } from '../../unit/music'; 6 | 7 | const down = (store) => { 8 | store.dispatch(actions.keyboard.rotate(true)); 9 | if (store.getState().get('cur') !== null) { 10 | event.down({ 11 | key: 'rotate', 12 | once: true, 13 | callback: () => { 14 | const state = store.getState(); 15 | if (state.get('lock')) { 16 | return; 17 | } 18 | if (state.get('pause')) { 19 | states.pause(false); 20 | } 21 | const cur = state.get('cur'); 22 | if (cur === null) { 23 | return; 24 | } 25 | if (music.rotate) { 26 | music.rotate(); 27 | } 28 | const next = cur.rotate(); 29 | if (want(next, state.get('matrix'))) { 30 | store.dispatch(actions.moveBlock(next)); 31 | } 32 | }, 33 | }); 34 | } else { 35 | event.down({ 36 | key: 'rotate', 37 | begin: 200, 38 | interval: 100, 39 | callback: () => { 40 | if (store.getState().get('lock')) { 41 | return; 42 | } 43 | if (music.move) { 44 | music.move(); 45 | } 46 | const state = store.getState(); 47 | const cur = state.get('cur'); 48 | if (cur) { 49 | return; 50 | } 51 | let startLines = state.get('startLines'); 52 | startLines = startLines + 1 > 10 ? 0 : startLines + 1; 53 | store.dispatch(actions.startLines(startLines)); 54 | }, 55 | }); 56 | } 57 | }; 58 | 59 | const up = (store) => { 60 | store.dispatch(actions.keyboard.rotate(false)); 61 | event.up({ 62 | key: 'rotate', 63 | }); 64 | }; 65 | 66 | export default { 67 | down, 68 | up, 69 | }; 70 | -------------------------------------------------------------------------------- /src/control/todo/left.js: -------------------------------------------------------------------------------- 1 | import { want } from '../../unit/'; 2 | import event from '../../unit/event'; 3 | import actions from '../../actions'; 4 | import states from '../states'; 5 | import { speeds, delays } from '../../unit/const'; 6 | import { music } from '../../unit/music'; 7 | 8 | const down = (store) => { 9 | store.dispatch(actions.keyboard.left(true)); 10 | event.down({ 11 | key: 'left', 12 | begin: 200, 13 | interval: 100, 14 | callback: () => { 15 | const state = store.getState(); 16 | if (state.get('lock')) { 17 | return; 18 | } 19 | if (music.move) { 20 | music.move(); 21 | } 22 | const cur = state.get('cur'); 23 | if (cur !== null) { 24 | if (state.get('pause')) { 25 | states.pause(false); 26 | return; 27 | } 28 | const next = cur.left(); 29 | const delay = delays[state.get('speedRun') - 1]; 30 | let timeStamp; 31 | if (want(next, state.get('matrix'))) { 32 | next.timeStamp += parseInt(delay, 10); 33 | store.dispatch(actions.moveBlock(next)); 34 | timeStamp = next.timeStamp; 35 | } else { 36 | cur.timeStamp += parseInt(parseInt(delay, 10) / 1.5, 10); // 真实移动delay多一点,碰壁delay少一点 37 | store.dispatch(actions.moveBlock(cur)); 38 | timeStamp = cur.timeStamp; 39 | } 40 | const remain = speeds[state.get('speedRun') - 1] - (Date.now() - timeStamp); 41 | states.auto(remain); 42 | } else { 43 | let speed = state.get('speedStart'); 44 | speed = speed - 1 < 1 ? 6 : speed - 1; 45 | store.dispatch(actions.speedStart(speed)); 46 | } 47 | }, 48 | }); 49 | }; 50 | 51 | const up = (store) => { 52 | store.dispatch(actions.keyboard.left(false)); 53 | event.up({ 54 | key: 'left', 55 | }); 56 | }; 57 | 58 | export default { 59 | down, 60 | up, 61 | }; 62 | -------------------------------------------------------------------------------- /src/control/todo/right.js: -------------------------------------------------------------------------------- 1 | import { want } from '../../unit/'; 2 | import event from '../../unit/event'; 3 | import actions from '../../actions'; 4 | import states from '../states'; 5 | import { speeds, delays } from '../../unit/const'; 6 | import { music } from '../../unit/music'; 7 | 8 | const down = (store) => { 9 | store.dispatch(actions.keyboard.right(true)); 10 | event.down({ 11 | key: 'right', 12 | begin: 200, 13 | interval: 100, 14 | callback: () => { 15 | const state = store.getState(); 16 | if (state.get('lock')) { 17 | return; 18 | } 19 | if (music.move) { 20 | music.move(); 21 | } 22 | const cur = state.get('cur'); 23 | if (cur !== null) { 24 | if (state.get('pause')) { 25 | states.pause(false); 26 | return; 27 | } 28 | const next = cur.right(); 29 | const delay = delays[state.get('speedRun') - 1]; 30 | let timeStamp; 31 | if (want(next, state.get('matrix'))) { 32 | next.timeStamp += parseInt(delay, 10); 33 | store.dispatch(actions.moveBlock(next)); 34 | timeStamp = next.timeStamp; 35 | } else { 36 | cur.timeStamp += parseInt(parseInt(delay, 10) / 1.5, 10); // 真实移动delay多一点,碰壁delay少一点 37 | store.dispatch(actions.moveBlock(cur)); 38 | timeStamp = cur.timeStamp; 39 | } 40 | const remain = speeds[state.get('speedRun') - 1] - (Date.now() - timeStamp); 41 | states.auto(remain); 42 | } else { 43 | let speed = state.get('speedStart'); 44 | speed = speed + 1 > 6 ? 1 : speed + 1; 45 | store.dispatch(actions.speedStart(speed)); 46 | } 47 | }, 48 | }); 49 | }; 50 | 51 | const up = (store) => { 52 | store.dispatch(actions.keyboard.right(false)); 53 | event.up({ 54 | key: 'right', 55 | }); 56 | }; 57 | 58 | export default { 59 | down, 60 | up, 61 | }; 62 | -------------------------------------------------------------------------------- /src/control/todo/space.js: -------------------------------------------------------------------------------- 1 | import { want } from '../../unit/'; 2 | import event from '../../unit/event'; 3 | import actions from '../../actions'; 4 | import states from '../states'; 5 | import { music } from '../../unit/music'; 6 | 7 | const down = (store) => { 8 | store.dispatch(actions.keyboard.drop(true)); 9 | event.down({ 10 | key: 'space', 11 | once: true, 12 | callback: () => { 13 | const state = store.getState(); 14 | if (state.get('lock')) { 15 | return; 16 | } 17 | const cur = state.get('cur'); 18 | if (cur !== null) { // 置底 19 | if (state.get('pause')) { 20 | states.pause(false); 21 | return; 22 | } 23 | if (music.fall) { 24 | music.fall(); 25 | } 26 | let index = 0; 27 | let bottom = cur.fall(index); 28 | while (want(bottom, state.get('matrix'))) { 29 | bottom = cur.fall(index); 30 | index++; 31 | } 32 | let matrix = state.get('matrix'); 33 | bottom = cur.fall(index - 2); 34 | store.dispatch(actions.moveBlock(bottom)); 35 | const shape = bottom.shape; 36 | const xy = bottom.xy; 37 | shape.forEach((m, k1) => ( 38 | m.forEach((n, k2) => { 39 | if (n && xy[0] + k1 >= 0) { // 竖坐标可以为负 40 | let line = matrix.get(xy[0] + k1); 41 | line = line.set(xy[1] + k2, 1); 42 | matrix = matrix.set(xy[0] + k1, line); 43 | } 44 | }) 45 | )); 46 | store.dispatch(actions.drop(true)); 47 | setTimeout(() => { 48 | store.dispatch(actions.drop(false)); 49 | }, 100); 50 | states.nextAround(matrix); 51 | } else { 52 | states.start(); 53 | } 54 | }, 55 | }); 56 | }; 57 | 58 | const up = (store) => { 59 | store.dispatch(actions.keyboard.drop(false)); 60 | event.up({ 61 | key: 'space', 62 | }); 63 | }; 64 | 65 | export default { 66 | down, 67 | up, 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/point/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import propTypes from 'prop-types'; 3 | 4 | import Number from '../number'; 5 | import { i18n, lan } from '../../unit/const'; 6 | 7 | const DF = i18n.point[lan]; 8 | const ZDF = i18n.highestScore[lan]; 9 | const SLDF = i18n.lastRound[lan]; 10 | 11 | export default class Point extends React.Component { 12 | constructor() { 13 | super(); 14 | this.state = { 15 | label: '', 16 | number: 0, 17 | }; 18 | } 19 | componentWillMount() { 20 | this.onChange(this.props); 21 | } 22 | componentWillReceiveProps(nextProps) { 23 | this.onChange(nextProps); 24 | } 25 | shouldComponentUpdate({ cur, point, max }) { 26 | const props = this.props; 27 | return cur !== props.cur || point !== props.point || max !== props.max || !props.cur; 28 | } 29 | onChange({ cur, point, max }) { 30 | clearInterval(Point.timeout); 31 | if (cur) { // 在游戏进行中 32 | this.setState({ 33 | label: point >= max ? ZDF : DF, 34 | number: point, 35 | }); 36 | } else { // 游戏未开始 37 | const toggle = () => { // 最高分与上轮得分交替出现 38 | this.setState({ 39 | label: SLDF, 40 | number: point, 41 | }); 42 | Point.timeout = setTimeout(() => { 43 | this.setState({ 44 | label: ZDF, 45 | number: max, 46 | }); 47 | Point.timeout = setTimeout(toggle, 3000); 48 | }, 3000); 49 | }; 50 | 51 | if (point !== 0) { // 如果为上轮没玩, 也不用提示了 52 | toggle(); 53 | } else { 54 | this.setState({ 55 | label: ZDF, 56 | number: max, 57 | }); 58 | } 59 | } 60 | } 61 | render() { 62 | return ( 63 |
64 |

{ this.state.label }

65 | 66 |
67 | ); 68 | } 69 | } 70 | 71 | Point.statics = { 72 | timeout: null, 73 | }; 74 | 75 | Point.propTypes = { 76 | cur: propTypes.bool, 77 | max: propTypes.number.isRequired, 78 | point: propTypes.number.isRequired, 79 | }; 80 | 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tetris", 3 | "version": "1.0.1", 4 | "description": "使用React、Redux、Immutable编写「俄罗斯方块」。Use Tetact, Redux, Immutable to coding \"Tetris\".", 5 | "scripts": { 6 | "start": "webpack-dev-server --progress", 7 | "build": "rm -rf ./docs/* && webpack --config ./webpack.production.config.js --progress && ls ./docs" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/chvin/react-tetris.git" 12 | }, 13 | "keywords": [ 14 | "Tetris", 15 | "React", 16 | "Redux", 17 | "Immutable", 18 | "俄罗斯方块" 19 | ], 20 | "author": "Chvin", 21 | "license": "Apache-2.0", 22 | "bugs": { 23 | "url": "https://github.com/chvin/react-tetris/issues" 24 | }, 25 | "homepage": "https://github.com/chvin/react-tetris#readme", 26 | "devDependencies": { 27 | "autoprefixer": "^6.7.2", 28 | "babel-core": "^6.13.2", 29 | "babel-loader": "^6.2.4", 30 | "babel-plugin-react-transform": "^2.0.2", 31 | "babel-preset-es2015": "^6.13.2", 32 | "babel-preset-react": "^6.16.0", 33 | "copy-webpack-plugin": "^3.0.1", 34 | "css-loader": "^0.23.1", 35 | "eslint": "^3.3.1", 36 | "eslint-config-airbnb": "^10.0.1", 37 | "eslint-loader": "^1.6.1", 38 | "eslint-plugin-import": "^1.13.0", 39 | "eslint-plugin-jsx-a11y": "^2.1.0", 40 | "eslint-plugin-react": "^6.1.1", 41 | "extract-text-webpack-plugin": "^1.0.1", 42 | "file-loader": "^0.9.0", 43 | "html-webpack-plugin": "^2.22.0", 44 | "json-loader": "^0.5.4", 45 | "less": "^2.7.1", 46 | "less-loader": "^2.2.3", 47 | "open-browser-webpack-plugin": "0.0.3", 48 | "postcss": "^5.2.12", 49 | "postcss-loader": "^1.2.2", 50 | "precss": "^1.4.0", 51 | "react-transform-hmr": "^1.0.4", 52 | "style-loader": "^0.13.1", 53 | "url-loader": "^0.5.7", 54 | "webpack": "^1.13.1", 55 | "webpack-dev-server": "^1.14.1" 56 | }, 57 | "dependencies": { 58 | "classnames": "^2.2.5", 59 | "immutable": "^3.8.1", 60 | "prop-types": "^15.5.10", 61 | "qrcode": "^1.2.0", 62 | "react": "^15.3.0", 63 | "react-dom": "^15.3.0", 64 | "react-redux": "^4.4.5", 65 | "redux": "^3.5.2", 66 | "redux-immutable": "^3.0.8" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import { getNextType } from '../unit'; 2 | import * as reducerType from '../unit/reducerType'; 3 | import Block from '../unit/block'; 4 | import keyboard from './keyboard'; 5 | 6 | function nextBlock(next = getNextType()) { 7 | return { 8 | type: reducerType.NEXT_BLOCK, 9 | data: next, 10 | }; 11 | } 12 | 13 | function moveBlock(option) { 14 | return { 15 | type: reducerType.MOVE_BLOCK, 16 | data: option.reset === true ? null : new Block(option), 17 | }; 18 | } 19 | 20 | function speedStart(n) { 21 | return { 22 | type: reducerType.SPEED_START, 23 | data: n, 24 | }; 25 | } 26 | 27 | function speedRun(n) { 28 | return { 29 | type: reducerType.SPEED_RUN, 30 | data: n, 31 | }; 32 | } 33 | 34 | function startLines(n) { 35 | return { 36 | type: reducerType.START_LINES, 37 | data: n, 38 | }; 39 | } 40 | 41 | function matrix(data) { 42 | return { 43 | type: reducerType.MATRIX, 44 | data, 45 | }; 46 | } 47 | 48 | function lock(data) { 49 | return { 50 | type: reducerType.LOCK, 51 | data, 52 | }; 53 | } 54 | 55 | function clearLines(data) { 56 | return { 57 | type: reducerType.CLEAR_LINES, 58 | data, 59 | }; 60 | } 61 | 62 | function points(data) { 63 | return { 64 | type: reducerType.POINTS, 65 | data, 66 | }; 67 | } 68 | 69 | function max(data) { 70 | return { 71 | type: reducerType.MAX, 72 | data, 73 | }; 74 | } 75 | 76 | function reset(data) { 77 | return { 78 | type: reducerType.RESET, 79 | data, 80 | }; 81 | } 82 | 83 | function drop(data) { 84 | return { 85 | type: reducerType.DROP, 86 | data, 87 | }; 88 | } 89 | 90 | function pause(data) { 91 | return { 92 | type: reducerType.PAUSE, 93 | data, 94 | }; 95 | } 96 | 97 | function music(data) { 98 | return { 99 | type: reducerType.MUSIC, 100 | data, 101 | }; 102 | } 103 | 104 | function focus(data) { 105 | return { 106 | type: reducerType.FOCUS, 107 | data, 108 | }; 109 | } 110 | 111 | export default { 112 | nextBlock, 113 | moveBlock, 114 | speedStart, 115 | speedRun, 116 | startLines, 117 | matrix, 118 | lock, 119 | clearLines, 120 | points, 121 | reset, 122 | max, 123 | drop, 124 | pause, 125 | keyboard, 126 | music, 127 | focus, 128 | }; 129 | -------------------------------------------------------------------------------- /src/components/number/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cn from 'classnames'; 3 | import propTypes from 'prop-types'; 4 | 5 | import style from './index.less'; 6 | 7 | const render = (data) => ( 8 |
9 | { 10 | data.map((e, k) => ( 11 | 12 | )) 13 | } 14 |
15 | ); 16 | 17 | const formate = (num) => ( 18 | num < 10 ? `0${num}`.split('') : `${num}`.split('') 19 | ); 20 | 21 | 22 | export default class Number extends React.Component { 23 | constructor() { 24 | super(); 25 | this.state = { 26 | time_count: false, 27 | time: new Date(), 28 | }; 29 | } 30 | componentWillMount() { 31 | if (!this.props.time) { 32 | return; 33 | } 34 | const clock = () => { 35 | const count = +Number.timeInterval; 36 | Number.timeInterval = setTimeout(() => { 37 | this.setState({ 38 | time: new Date(), 39 | time_count: count, // 用来做 shouldComponentUpdate 优化 40 | }); 41 | clock(); 42 | }, 1000); 43 | }; 44 | clock(); 45 | } 46 | shouldComponentUpdate({ number }) { 47 | if (this.props.time) { // 右下角时钟 48 | if (this.state.time_count !== Number.time_count) { 49 | if (this.state.time_count !== false) { 50 | Number.time_count = this.state.time_count; // 记录clock上一次的缓存 51 | } 52 | return true; 53 | } 54 | return false; // 经过判断这次的时间已经渲染, 返回false 55 | } 56 | return this.props.number !== number; 57 | } 58 | componentWillUnmount() { 59 | if (!this.props.time) { 60 | return; 61 | } 62 | clearTimeout(Number.timeInterval); 63 | } 64 | render() { 65 | if (this.props.time) { // 右下角时钟 66 | const now = this.state.time; 67 | const hour = formate(now.getHours()); 68 | const min = formate(now.getMinutes()); 69 | const sec = now.getSeconds() % 2; 70 | const t = hour.concat(sec ? 'd' : 'd_c', min); 71 | return (render(t)); 72 | } 73 | 74 | const num = `${this.props.number}`.split(''); 75 | for (let i = 0, len = this.props.length - num.length; i < len; i++) { 76 | num.unshift('n'); 77 | } 78 | return (render(num)); 79 | } 80 | } 81 | 82 | Number.statics = { 83 | timeInterval: null, 84 | time_count: null, 85 | }; 86 | 87 | Number.propTypes = { 88 | number: propTypes.number, 89 | length: propTypes.number, 90 | time: propTypes.bool, 91 | }; 92 | 93 | Number.defaultProps = { 94 | length: 6, 95 | }; 96 | -------------------------------------------------------------------------------- /src/control/todo/down.js: -------------------------------------------------------------------------------- 1 | import { want } from '../../unit/'; 2 | import event from '../../unit/event'; 3 | import actions from '../../actions'; 4 | import states from '../states'; 5 | import { music } from '../../unit/music'; 6 | 7 | const down = (store) => { 8 | store.dispatch(actions.keyboard.down(true)); 9 | if (store.getState().get('cur') !== null) { 10 | event.down({ 11 | key: 'down', 12 | begin: 40, 13 | interval: 40, 14 | callback: (stopDownTrigger) => { 15 | const state = store.getState(); 16 | if (state.get('lock')) { 17 | return; 18 | } 19 | if (music.move) { 20 | music.move(); 21 | } 22 | const cur = state.get('cur'); 23 | if (cur === null) { 24 | return; 25 | } 26 | if (state.get('pause')) { 27 | states.pause(false); 28 | return; 29 | } 30 | const next = cur.fall(); 31 | if (want(next, state.get('matrix'))) { 32 | store.dispatch(actions.moveBlock(next)); 33 | states.auto(); 34 | } else { 35 | let matrix = state.get('matrix'); 36 | const shape = cur.shape; 37 | const xy = cur.xy; 38 | shape.forEach((m, k1) => ( 39 | m.forEach((n, k2) => { 40 | if (n && xy.get(0) + k1 >= 0) { // 竖坐标可以为负 41 | let line = matrix.get(xy.get(0) + k1); 42 | line = line.set(xy.get(1) + k2, 1); 43 | matrix = matrix.set(xy.get(0) + k1, line); 44 | } 45 | }) 46 | )); 47 | states.nextAround(matrix, stopDownTrigger); 48 | } 49 | }, 50 | }); 51 | } else { 52 | event.down({ 53 | key: 'down', 54 | begin: 200, 55 | interval: 100, 56 | callback: () => { 57 | if (store.getState().get('lock')) { 58 | return; 59 | } 60 | const state = store.getState(); 61 | const cur = state.get('cur'); 62 | if (cur) { 63 | return; 64 | } 65 | if (music.move) { 66 | music.move(); 67 | } 68 | let startLines = state.get('startLines'); 69 | startLines = startLines - 1 < 0 ? 10 : startLines - 1; 70 | store.dispatch(actions.startLines(startLines)); 71 | }, 72 | }); 73 | } 74 | }; 75 | 76 | const up = (store) => { 77 | store.dispatch(actions.keyboard.down(false)); 78 | event.up({ 79 | key: 'down', 80 | }); 81 | }; 82 | 83 | 84 | export default { 85 | down, 86 | up, 87 | }; 88 | -------------------------------------------------------------------------------- /src/components/guide/index.less: -------------------------------------------------------------------------------- 1 | .background(@from, @to){ 2 | background: (@from + @to)/2; 3 | background: -webkit-gradient(linear, left top, left bottom, from(@from), to(@to)); 4 | background: -moz-linear-gradient(top, @from, @from); 5 | // IE9使用filter背景渐变, 但因为使用了borader-radius, 所以不兼容, IE9使用中和的背景色即可 6 | //filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@{from}', endColorstr='@{to}'); 7 | } 8 | 9 | .guide { 10 | position: absolute; 11 | left: 115%; 12 | right: 115%; 13 | text-align:center; 14 | line-height: 1; 15 | white-space: nowrap; 16 | &.right{ 17 | right: auto; 18 | bottom: 5%; 19 | } 20 | &.left{ 21 | left: auto; 22 | bottom: 5%; 23 | } 24 | p{ 25 | text-align: left; 26 | margin-bottom: 350px; 27 | opacity: .5; 28 | iframe{ 29 | margin-top: 20px; 30 | } 31 | } 32 | 33 | a{ 34 | color:#005850; 35 | font-size:30px; 36 | position:relative; 37 | z-index:1; 38 | cursor: alias; 39 | text-decoration: none; 40 | } 41 | &.qr{ 42 | left: auto; 43 | top: 5%; 44 | width: 250px; 45 | height:250px; 46 | opacity: .5; 47 | text-align: right; 48 | cursor: none; 49 | &:hover{ 50 | img{ 51 | width: 100%; 52 | height: 100%; 53 | } 54 | } 55 | img{ 56 | width: 38px; 57 | height: 38px; 58 | } 59 | } 60 | 61 | &>div{ 62 | width: 100px; 63 | height: 54px; 64 | background: /*#404040*/#364d4b; 65 | display:inline-block; 66 | border-radius: 4px; 67 | position: relative; 68 | color: #acc3c1; 69 | font-size: 16px; 70 | em { 71 | display: block; 72 | width: 0; 73 | height: 0; 74 | border: 6px solid; 75 | border-color: transparent transparent /*#ccc*/#acc3c1; 76 | position: absolute; 77 | top: 50%; 78 | left: 50%; 79 | margin: -9px 0 0 -6px; 80 | } 81 | &:before, &:after{ 82 | content: ''; 83 | display: block; 84 | width: 100%; 85 | height: 100%; 86 | position: absolute; 87 | top: 0; 88 | left: 0; 89 | border-radius:4px; 90 | box-shadow: 0 5px 10px rgba(255, 255, 255, .15) inset; 91 | } 92 | &:before{ 93 | box-shadow: 0 -5px 10px rgba(0, 0, 0, .15) inset; 94 | } 95 | } 96 | .up{ 97 | height: 60px; 98 | display:block; 99 | margin:0 auto 2px; 100 | } 101 | .down{ 102 | margin:0 10px; 103 | } 104 | .space{ 105 | left: auto; 106 | bottom: 5%; 107 | height: 80px; 108 | width: 400px; 109 | line-height: 80px; 110 | letter-spacing: 2px; 111 | } 112 | } -------------------------------------------------------------------------------- /src/unit/music.js: -------------------------------------------------------------------------------- 1 | import store from '../store'; 2 | 3 | // 使用 Web Audio API 4 | const AudioContext = ( 5 | window.AudioContext || 6 | window.webkitAudioContext || 7 | window.mozAudioContext || 8 | window.oAudioContext || 9 | window.msAudioContext 10 | ); 11 | 12 | const hasWebAudioAPI = { 13 | data: !!AudioContext && location.protocol.indexOf('http') !== -1, 14 | }; 15 | 16 | 17 | const music = {}; 18 | 19 | (() => { 20 | if (!hasWebAudioAPI.data) { 21 | return; 22 | } 23 | const url = './music.mp3'; 24 | const context = new AudioContext(); 25 | const req = new XMLHttpRequest(); 26 | req.open('GET', url, true); 27 | req.responseType = 'arraybuffer'; 28 | 29 | req.onload = () => { 30 | context.decodeAudioData(req.response, (buf) => { // 将拿到的audio解码转为buffer 31 | const getSource = () => { // 创建source源。 32 | const source = context.createBufferSource(); 33 | source.buffer = buf; 34 | source.connect(context.destination); 35 | return source; 36 | }; 37 | 38 | music.killStart = () => { // 游戏开始的音乐只播放一次 39 | music.start = () => {}; 40 | }; 41 | 42 | music.start = () => { // 游戏开始 43 | music.killStart(); 44 | if (!store.getState().get('music')) { 45 | return; 46 | } 47 | getSource().start(0, 3.7202, 3.6224); 48 | }; 49 | 50 | music.clear = () => { // 消除方块 51 | if (!store.getState().get('music')) { 52 | return; 53 | } 54 | getSource().start(0, 0, 0.7675); 55 | }; 56 | 57 | music.fall = () => { // 立即下落 58 | if (!store.getState().get('music')) { 59 | return; 60 | } 61 | getSource().start(0, 1.2558, 0.3546); 62 | }; 63 | 64 | music.gameover = () => { // 游戏结束 65 | if (!store.getState().get('music')) { 66 | return; 67 | } 68 | getSource().start(0, 8.1276, 1.1437); 69 | }; 70 | 71 | music.rotate = () => { // 旋转 72 | if (!store.getState().get('music')) { 73 | return; 74 | } 75 | getSource().start(0, 2.2471, 0.0807); 76 | }; 77 | 78 | music.move = () => { // 移动 79 | if (!store.getState().get('music')) { 80 | return; 81 | } 82 | getSource().start(0, 2.9088, 0.1437); 83 | }; 84 | }, 85 | (error) => { 86 | if (window.console && window.console.error) { 87 | window.console.error(`音频: ${url} 读取错误`, error); 88 | hasWebAudioAPI.data = false; 89 | } 90 | }); 91 | }; 92 | 93 | req.send(); 94 | })(); 95 | 96 | module.exports = { 97 | hasWebAudioAPI, 98 | music, 99 | }; 100 | 101 | -------------------------------------------------------------------------------- /src/components/keyboard/button/index.less: -------------------------------------------------------------------------------- 1 | .background(@from, @to){ 2 | background: (@from + @to)/2; 3 | background: -webkit-gradient(linear, left top, left bottom, from(@from), to(@to)); 4 | background: -moz-linear-gradient(top, @from, @from); 5 | // IE9使用filter背景渐变, 但因为使用了borader-radius, 所以不兼容, IE9使用中和的背景色即可 6 | //filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@{from}', endColorstr='@{to}'); 7 | } 8 | 9 | .button{ 10 | position: absolute; 11 | text-align: center; 12 | color: #111; 13 | position: absolute; 14 | white-space: nowrap; 15 | line-height: 1.6; 16 | &.s2{ 17 | font-size: 16px; 18 | } 19 | span.position{ 20 | position: absolute; 21 | top: 5px; 22 | left: 102px; 23 | } 24 | i{ 25 | display: block; 26 | position: relative; 27 | border: 1px solid #000; 28 | border-radius: 50%; 29 | &:before, &:after{ 30 | content: ''; 31 | display: block; 32 | width: 100%; 33 | height: 100%; 34 | position: absolute; 35 | top: 0; 36 | left: 0; 37 | border-radius:50%; 38 | box-shadow: 0 5px 10px rgba(255, 255, 255, .8) inset; 39 | } 40 | &:after{ 41 | box-shadow: 0 -5px 10px rgba(0, 0, 0, .8) inset; 42 | } 43 | &.active{ 44 | &:before{ 45 | box-shadow: 0 -3px 6px rgba(255, 255, 255, .6) inset; 46 | } 47 | &:after{ 48 | box-shadow: 0 5px 5px rgba(0, 0, 0, .6) inset; 49 | } 50 | } 51 | box-shadow: 0 3px 3px rgba(0, 0, 0, .2); 52 | 53 | } 54 | 55 | &.blue i{ 56 | .background(#6e77ef, #4652f3); 57 | } 58 | &.green i{ 59 | .background(#4bc441, #0ec400); 60 | } 61 | &.red i{ 62 | .background(#dc3333, #de0000); 63 | } 64 | &.s0 i{ 65 | width: 160px; 66 | height: 160px; 67 | 68 | } 69 | &.s1 i{ 70 | width: 100px; 71 | height: 100px; 72 | } 73 | &.s2 i{ 74 | width: 52px; 75 | height: 52px; 76 | &:before, &:after{ 77 | box-shadow: 0px 3px 6px rgba(255, 255, 255, .8) inset; 78 | } 79 | &:after{ 80 | box-shadow: 0px -3px 6px rgba(0, 0, 0, .8) inset; 81 | } 82 | &.active{ 83 | &:before{ 84 | box-shadow: 0px -1px 2px rgba(255, 255, 255, .6) inset; 85 | } 86 | &:after{ 87 | box-shadow: 0px 3px 3px rgba(0, 0, 0, .7) inset; 88 | } 89 | } 90 | box-shadow: 1px 1px 1px rgba(0, 0, 0, .2); 91 | } 92 | 93 | &.s1{ 94 | em{ 95 | display: block; 96 | width: 0; 97 | height: 0; 98 | border: 8px solid; 99 | border-color: transparent transparent #111; 100 | position: absolute; 101 | top: 50%; 102 | left: 50%; 103 | margin: -12px 0 0 -8px; 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "lan": ["cn", "en", "fr", "fa"], 3 | "default": "cn", 4 | "data": { 5 | "title": { 6 | "cn": "俄罗斯方块", 7 | "en": "T E T R I S", 8 | "fr": "T E T R I S", 9 | "fa": "خانه سازی" 10 | }, 11 | "github": { 12 | "cn": "GitHub", 13 | "en": "GitHub", 14 | "fr": "GitHub", 15 | "fa": "گیت‌هاب" 16 | }, 17 | "linkTitle": { 18 | "cn": "查看源代码", 19 | "en": "View data source", 20 | "fr": "Afficher la source des données", 21 | "fa": "مشاهده سورس پروژه" 22 | }, 23 | "QRCode":{ 24 | "cn": "二维码", 25 | "en": "QR code", 26 | "fr": "QR code", 27 | "fa": "کیوآر کد" 28 | }, 29 | "titleCenter": { 30 | "cn": "俄罗斯方块
TETRIS", 31 | "en": "TETRIS", 32 | "fr": "TETRIS", 33 | "fa": "خانه سازی" 34 | }, 35 | "point": { 36 | "cn": "得分", 37 | "en": "Point", 38 | "fr": "Score", 39 | "fa": "امتیاز" 40 | }, 41 | "highestScore": { 42 | "cn": "最高分", 43 | "en": "Max", 44 | "fr": "Max", 45 | "fa": "حداکثر" 46 | }, 47 | "lastRound": { 48 | "cn": "上轮得分", 49 | "en": "Last Round", 50 | "fr": "Dernier Tour", 51 | "fa": "آخرین دور" 52 | }, 53 | "cleans": { 54 | "cn": "消除行", 55 | "en": "Cleans", 56 | "fr": "Lignes", 57 | "fa": "پاک کرد" 58 | }, 59 | "level": { 60 | "cn": "级别", 61 | "en": "Level", 62 | "fr": "Difficulté", 63 | "fa": "سطح" 64 | }, 65 | "startLine": { 66 | "cn": "起始行", 67 | "en": "Start Line", 68 | "fr": "Ligne Départ", 69 | "fa": "خط شروع" 70 | }, 71 | "next": { 72 | "cn": "下一个", 73 | "en": "Next", 74 | "fr": "Prochain", 75 | "fa": "بعدی" 76 | }, 77 | "pause": { 78 | "cn": "暂停", 79 | "en": "Pause", 80 | "fr": "Pause", 81 | "fa": "مکث" 82 | }, 83 | "sound": { 84 | "cn": "音效", 85 | "en": "Sound", 86 | "fr": "Sonore", 87 | "fa": "صدا" 88 | }, 89 | "reset": { 90 | "cn": "重玩", 91 | "en": "Reset", 92 | "fr": "Réinitialiser", 93 | "fa": "ریست" 94 | }, 95 | "rotation": { 96 | "cn": "旋转", 97 | "en": "Rotation", 98 | "fr": "Rotation", 99 | "fa": "چرخش" 100 | }, 101 | "left": { 102 | "cn": "左移", 103 | "en": "Left", 104 | "fr": "Gauche", 105 | "fa": "چپ" 106 | }, 107 | "right": { 108 | "cn": "右移", 109 | "en": "Right", 110 | "fr": "Droite", 111 | "fa": "راست" 112 | }, 113 | "down": { 114 | "cn": "下移", 115 | "en": "Down", 116 | "fr": "Bas", 117 | "fa": "پایین" 118 | }, 119 | "drop": { 120 | "cn": "掉落", 121 | "en": "Drop", 122 | "fr": "Tomber", 123 | "fa": "سقوط" 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/unit/const.js: -------------------------------------------------------------------------------- 1 | import { List } from 'immutable'; 2 | import i18n from '../../i18n.json'; 3 | 4 | const blockShape = { 5 | I: [ 6 | [1, 1, 1, 1], 7 | ], 8 | L: [ 9 | [0, 0, 1], 10 | [1, 1, 1], 11 | ], 12 | J: [ 13 | [1, 0, 0], 14 | [1, 1, 1], 15 | ], 16 | Z: [ 17 | [1, 1, 0], 18 | [0, 1, 1], 19 | ], 20 | S: [ 21 | [0, 1, 1], 22 | [1, 1, 0], 23 | ], 24 | O: [ 25 | [1, 1], 26 | [1, 1], 27 | ], 28 | T: [ 29 | [0, 1, 0], 30 | [1, 1, 1], 31 | ], 32 | }; 33 | 34 | const origin = { 35 | I: [[-1, 1], [1, -1]], 36 | L: [[0, 0]], 37 | J: [[0, 0]], 38 | Z: [[0, 0]], 39 | S: [[0, 0]], 40 | O: [[0, 0]], 41 | T: [[0, 0], [1, 0], [-1, 1], [0, -1]], 42 | }; 43 | 44 | const blockType = Object.keys(blockShape); 45 | 46 | const speeds = [800, 650, 500, 370, 250, 160]; 47 | 48 | const delays = [50, 60, 70, 80, 90, 100]; 49 | 50 | const fillLine = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; 51 | 52 | const blankLine = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 53 | 54 | const blankMatrix = (() => { 55 | const matrix = []; 56 | for (let i = 0; i < 20; i++) { 57 | matrix.push(List(blankLine)); 58 | } 59 | return List(matrix); 60 | })(); 61 | 62 | const clearPoints = [100, 300, 700, 1500]; 63 | 64 | const StorageKey = 'REACT_TETRIS'; 65 | 66 | const lastRecord = (() => { // 上一把的状态 67 | let data = localStorage.getItem(StorageKey); 68 | if (!data) { 69 | return false; 70 | } 71 | try { 72 | if (window.btoa) { 73 | data = atob(data); 74 | } 75 | data = decodeURIComponent(data); 76 | data = JSON.parse(data); 77 | } catch (e) { 78 | if (window.console || window.console.error) { 79 | window.console.error('读取记录错误:', e); 80 | } 81 | return false; 82 | } 83 | return data; 84 | })(); 85 | 86 | const maxPoint = 999999; 87 | 88 | const transform = (function () { 89 | const trans = ['transform', 'webkitTransform', 'msTransform', 'mozTransform', 'oTransform']; 90 | const body = document.body; 91 | return trans.filter((e) => body.style[e] !== undefined)[0]; 92 | }()); 93 | 94 | const eachLines = 20; // 每消除eachLines行, 增加速度 95 | 96 | const getParam = (param) => { // 获取浏览器参数 97 | const r = new RegExp(`\\?(?:.+&)?${param}=(.*?)(?:&.*)?$`); 98 | const m = window.location.toString().match(r); 99 | return m ? decodeURI(m[1]) : ''; 100 | }; 101 | 102 | const lan = (() => { 103 | let l = getParam('lan').toLowerCase(); 104 | l = i18n.lan.indexOf(l) === -1 ? i18n.default : l; 105 | return l; 106 | })(); 107 | 108 | document.title = i18n.data.title[lan]; 109 | 110 | module.exports = { 111 | blockShape, 112 | origin, 113 | blockType, 114 | speeds, 115 | delays, 116 | fillLine, 117 | blankLine, 118 | blankMatrix, 119 | clearPoints, 120 | StorageKey, 121 | lastRecord, 122 | maxPoint, 123 | eachLines, 124 | transform, 125 | lan, 126 | i18n: i18n.data, 127 | }; 128 | -------------------------------------------------------------------------------- /src/unit/index.js: -------------------------------------------------------------------------------- 1 | import { blockType, StorageKey } from './const'; 2 | 3 | const hiddenProperty = (() => { // document[hiddenProperty] 可以判断页面是否失焦 4 | let names = [ 5 | 'hidden', 6 | 'webkitHidden', 7 | 'mozHidden', 8 | 'msHidden', 9 | ]; 10 | names = names.filter((e) => (e in document)); 11 | return names.length > 0 ? names[0] : false; 12 | })(); 13 | 14 | const visibilityChangeEvent = (() => { 15 | if (!hiddenProperty) { 16 | return false; 17 | } 18 | return hiddenProperty.replace(/hidden/i, 'visibilitychange'); // 如果属性有前缀, 相应的事件也有前缀 19 | })(); 20 | 21 | const isFocus = () => { 22 | if (!hiddenProperty) { // 如果不存在该特性, 认为一直聚焦 23 | return true; 24 | } 25 | return !document[hiddenProperty]; 26 | }; 27 | 28 | const unit = { 29 | getNextType() { // 随机获取下一个方块类型 30 | const len = blockType.length; 31 | return blockType[Math.floor(Math.random() * len)]; 32 | }, 33 | want(next, matrix) { // 方块是否能移到到指定位置 34 | const xy = next.xy; 35 | const shape = next.shape; 36 | const horizontal = shape.get(0).size; 37 | return shape.every((m, k1) => ( 38 | m.every((n, k2) => { 39 | if (xy[1] < 0) { // left 40 | return false; 41 | } 42 | if (xy[1] + horizontal > 10) { // right 43 | return false; 44 | } 45 | if (xy[0] + k1 < 0) { // top 46 | return true; 47 | } 48 | if (xy[0] + k1 >= 20) { // bottom 49 | return false; 50 | } 51 | if (n) { 52 | if (matrix.get(xy[0] + k1).get(xy[1] + k2)) { 53 | return false; 54 | } 55 | return true; 56 | } 57 | return true; 58 | }) 59 | )); 60 | }, 61 | isClear(matrix) { // 是否达到消除状态 62 | const clearLines = []; 63 | matrix.forEach((m, k) => { 64 | if (m.every(n => !!n)) { 65 | clearLines.push(k); 66 | } 67 | }); 68 | if (clearLines.length === 0) { 69 | return false; 70 | } 71 | return clearLines; 72 | }, 73 | isOver(matrix) { // 游戏是否结束, 第一行落下方块为依据 74 | return matrix.get(0).some(n => !!n); 75 | }, 76 | subscribeRecord(store) { // 将状态记录到 localStorage 77 | store.subscribe(() => { 78 | let data = store.getState().toJS(); 79 | if (data.lock) { // 当状态为锁定, 不记录 80 | return; 81 | } 82 | data = JSON.stringify(data); 83 | data = encodeURIComponent(data); 84 | if (window.btoa) { 85 | data = btoa(data); 86 | } 87 | localStorage.setItem(StorageKey, data); 88 | }); 89 | }, 90 | isMobile() { // 判断是否为移动端 91 | const ua = navigator.userAgent; 92 | const android = /Android (\d+\.\d+)/.test(ua); 93 | const iphone = ua.indexOf('iPhone') > -1; 94 | const ipod = ua.indexOf('iPod') > -1; 95 | const ipad = ua.indexOf('iPad') > -1; 96 | const nokiaN = ua.indexOf('NokiaN') > -1; 97 | return android || iphone || ipod || ipad || nokiaN; 98 | }, 99 | visibilityChangeEvent, 100 | isFocus, 101 | }; 102 | 103 | module.exports = unit; 104 | -------------------------------------------------------------------------------- /src/components/guide/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import QRCode from 'qrcode'; 3 | import style from './index.less'; 4 | import { transform, i18n, lan } from '../../unit/const'; 5 | import { isMobile } from '../../unit'; 6 | 7 | 8 | export default class Guide extends React.Component { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | isMobile: isMobile(), 13 | QRCode: '', 14 | }; 15 | } 16 | componentWillMount() { 17 | if (this.state.isMobile) { 18 | return; 19 | } 20 | QRCode.toDataURL(location.href, { margin: 1 }) 21 | .then(dataUrl => this.setState({ QRCode: dataUrl })); 22 | } 23 | shouldComponentUpdate(state) { 24 | if (state.QRCode === this.state.QRCode) { 25 | return false; 26 | } 27 | return true; 28 | } 29 | render() { 30 | if (this.state.isMobile) { 31 | return ( 32 | null 33 | ); 34 | } 35 | return ( 36 |
37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 |

52 | {`${i18n.github[lan]}:`}
53 |