├── .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 |
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 |
19 |
20 |
--------------------------------------------------------------------------------
/server/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 俄罗斯方块
11 |
12 |
13 |
14 |
15 |
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 |
73 | { this.state.QRCode !== '' ? (
74 |
75 |
![{i18n.QRCode[lan]}]({this.state.QRCode})
79 |
80 | ) : null }
81 |
82 | );
83 | }
84 | }
85 |
86 |
--------------------------------------------------------------------------------
/src/containers/index.less:
--------------------------------------------------------------------------------
1 | body{
2 | background: #009688;
3 | padding: 0;margin:0;
4 | font: 20px/1 "HanHei SC","PingHei","PingFang SC","STHeitiSC-Light","Helvetica Neue","Helvetica","Arial",sans-serif;
5 | overflow: hidden;
6 | cursor: default;
7 | text-rendering: optimizeLegibility;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | -moz-font-feature-settings: 'liga', 'kern';
11 | direction: ltr;
12 | text-align: left;
13 | }
14 |
15 | :global{
16 |
17 | .r{
18 | float: right;
19 | }
20 | .l{
21 | float: left;
22 | }
23 |
24 | .clear{
25 | clear: both;
26 | }
27 |
28 | .bg{
29 | background:url('//img.alicdn.com/tps/TB1qq7kNXXXXXacXFXXXXXXXXXX-400-186.png') no-repeat;
30 | overflow:hidden;
31 | }
32 | }
33 |
34 |
35 | *{
36 | box-sizing: border-box;
37 | margin: 0;
38 | padding: 0;
39 | -moz-user-select: none;
40 | -webkit-user-select: none;
41 | -ms-user-select: none;
42 | -khtml-user-select: none;
43 | user-select: none;
44 | }
45 |
46 |
47 | .app{
48 | width: 640px;
49 | padding-top: 42px;
50 | box-shadow: 0 0 10px #fff inset;
51 | border-radius: 20px;
52 | position: absolute;
53 | top: 50%;
54 | left: 50%;
55 | margin: -480px 0 0 -320px;
56 | background: #efcc19;
57 | :global{
58 | b {
59 | display: block;
60 | width: 20px;
61 | height: 20px;
62 | padding: 2px;
63 | border: 2px solid #879372;
64 | margin: 0 2px 2px 0;
65 | float: left;
66 | &:after {
67 | content: '';
68 | display: block;
69 | width: 12px;
70 | height: 12px;
71 | background: #879372;
72 | overflow: hidden;
73 | }
74 | &.c {
75 | border-color: #000;
76 | &:after {
77 | background: #000;
78 | }
79 | }
80 | &.d {
81 | border-color: #560000;
82 | &:after {
83 | background: #560000;
84 | }
85 | }
86 | }
87 | }
88 | }
89 |
90 | .rect{
91 | width: 480px;
92 | padding: 45px 0 35px;
93 | border: #000 solid;
94 | border-width: 0 10px 10px;
95 | margin: 0 auto;
96 | position: relative;
97 | &.drop{ -webkit-transform:translateY(5px);transform:translateY(5px); }
98 |
99 | }
100 |
101 | .screen{
102 | width: 390px;
103 | height: 478px;
104 | border: solid 5px;
105 | border-color: #987f0f #fae36c #fae36c #987f0f;
106 | margin: 0 auto;
107 | position: relative;
108 | .panel{
109 | width: 380px;
110 | height: 468px;
111 | margin: 0 auto;
112 | background: #9ead86;
113 | padding: 8px;
114 | border: 2px solid #494536;
115 | }
116 | }
117 |
118 | .state {
119 | width: 108px;
120 | position: absolute;
121 | top: 0;
122 | right: 15px;
123 | p {
124 | line-height: 47px;
125 | height: 57px;
126 | padding: 10px 0 0;
127 | white-space: nowrap;
128 | clear: both;
129 | }
130 | .bottom {
131 | position: absolute;
132 | width: 114px;
133 | top: 426px;
134 | left: 0;
135 | }
136 | }
--------------------------------------------------------------------------------
/src/unit/block.js:
--------------------------------------------------------------------------------
1 | import { List } from 'immutable';
2 | import { blockShape, origin } from './const';
3 |
4 | class Block {
5 | constructor(option) {
6 | this.type = option.type;
7 |
8 | if (!option.rotateIndex) {
9 | this.rotateIndex = 0;
10 | } else {
11 | this.rotateIndex = option.rotateIndex;
12 | }
13 |
14 | if (!option.timeStamp) {
15 | this.timeStamp = Date.now();
16 | } else {
17 | this.timeStamp = option.timeStamp;
18 | }
19 |
20 | if (!option.shape) { // init
21 | this.shape = List(blockShape[option.type].map(e => List(e)));
22 | } else {
23 | this.shape = option.shape;
24 | }
25 | if (!option.xy) {
26 | switch (option.type) {
27 | case 'I': // I
28 | this.xy = List([0, 3]);
29 | break;
30 | case 'L': // L
31 | this.xy = List([-1, 4]);
32 | break;
33 | case 'J': // J
34 | this.xy = List([-1, 4]);
35 | break;
36 | case 'Z': // Z
37 | this.xy = List([-1, 4]);
38 | break;
39 | case 'S': // S
40 | this.xy = List([-1, 4]);
41 | break;
42 | case 'O': // O
43 | this.xy = List([-1, 4]);
44 | break;
45 | case 'T': // T
46 | this.xy = List([-1, 4]);
47 | break;
48 | default:
49 | break;
50 | }
51 | } else {
52 | this.xy = List(option.xy);
53 | }
54 | }
55 | rotate() {
56 | const shape = this.shape;
57 | let result = List([]);
58 | shape.forEach(m => m.forEach((n, k) => {
59 | const index = m.size - k - 1;
60 | if (result.get(index) === undefined) {
61 | result = result.set(index, List([]));
62 | }
63 | const tempK = result.get(index).push(n);
64 | result = result.set(index, tempK);
65 | }));
66 | const nextXy = [
67 | this.xy.get(0) + origin[this.type][this.rotateIndex][0],
68 | this.xy.get(1) + origin[this.type][this.rotateIndex][1],
69 | ];
70 | const nextRotateIndex = this.rotateIndex + 1 >= origin[this.type].length ?
71 | 0 : this.rotateIndex + 1;
72 | return {
73 | shape: result,
74 | type: this.type,
75 | xy: nextXy,
76 | rotateIndex: nextRotateIndex,
77 | timeStamp: this.timeStamp,
78 | };
79 | }
80 | fall(n = 1) {
81 | return {
82 | shape: this.shape,
83 | type: this.type,
84 | xy: [this.xy.get(0) + n, this.xy.get(1)],
85 | rotateIndex: this.rotateIndex,
86 | timeStamp: Date.now(),
87 | };
88 | }
89 | right() {
90 | return {
91 | shape: this.shape,
92 | type: this.type,
93 | xy: [this.xy.get(0), this.xy.get(1) + 1],
94 | rotateIndex: this.rotateIndex,
95 | timeStamp: this.timeStamp,
96 | };
97 | }
98 | left() {
99 | return {
100 | shape: this.shape,
101 | type: this.type,
102 | xy: [this.xy.get(0), this.xy.get(1) - 1],
103 | rotateIndex: this.rotateIndex,
104 | timeStamp: this.timeStamp,
105 | };
106 | }
107 | }
108 |
109 | export default Block;
110 |
--------------------------------------------------------------------------------
/w.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var OpenBrowserPlugin = require('open-browser-webpack-plugin');
3 | var HtmlWebpackPlugin = require('html-webpack-plugin');
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | var precss = require('precss');
6 | var autoprefixer = require('autoprefixer');
7 | var CopyWebpackPlugin = require('copy-webpack-plugin');
8 | var version = require('./package.json').version;
9 |
10 |
11 | // 程序入口
12 | var entry = __dirname + '/src/index.js';
13 |
14 | // 输出文件
15 | var output = {
16 | filename: 'page/[name]/index.js',
17 | chunkFilename: 'chunk/[name].[chunkhash:5].chunk.js',
18 | };
19 |
20 | // 生成source-map追踪js错误
21 | var devtool = 'source-map';
22 |
23 | // eslint
24 | var eslint = {
25 | configFile: __dirname + '/.eslintrc.js',
26 | }
27 |
28 | // loader
29 | var loaders = [
30 | {
31 | test: /\.(json)$/,
32 | exclude: /node_modules/,
33 | loader: 'json',
34 | },
35 | {
36 | test: /\.(js|jsx)$/,
37 | exclude: /node_modules/,
38 | loader: 'babel!eslint-loader',
39 | },
40 | {
41 | test: /\.(?:png|jpg|gif)$/,
42 | loader: 'url?limit=8192', //小于8k,内嵌;大于8k生成文件
43 | },
44 | {
45 | test: /\.less/,
46 | loader: ExtractTextPlugin.extract('style', 'css?modules&localIdentName=[hash:base64:4]!postcss!less'),
47 | }
48 | ];
49 |
50 | // dev plugin
51 | var devPlugins = [
52 | new CopyWebpackPlugin([
53 | { from: './src/resource/music/music.mp3' },
54 | { from: './src/resource/css/loader.css' },
55 | ]),
56 | // 热更新
57 | new webpack.HotModuleReplacementPlugin(),
58 | // 允许错误不打断程序, 仅开发模式需要
59 | new webpack.NoErrorsPlugin(),
60 | // 打开浏览器页面
61 | new OpenBrowserPlugin({
62 | url: 'http://127.0.0.1:8080/'
63 | }),
64 | // css打包
65 | new ExtractTextPlugin('css.css', {
66 | allChunks: true
67 | }),
68 | ]
69 |
70 | // production plugin
71 | var productionPlugins = [
72 | // 定义生产环境
73 | new webpack.DefinePlugin({
74 | 'process.env.NODE_ENV': '"production"',
75 | }),
76 | // 复制
77 | new CopyWebpackPlugin([
78 | { from: './src/resource/music/music.mp3' },
79 | { from: './src/resource/css/loader.css' },
80 | ]),
81 | // HTML 模板
82 | new HtmlWebpackPlugin({
83 | template: __dirname + '/server/index.tmpl.html'
84 | }),
85 | // JS压缩
86 | new webpack.optimize.UglifyJsPlugin({
87 | compress: {
88 | warnings: false
89 | }}
90 | ),
91 | // css打包
92 | new ExtractTextPlugin('css-' + version + '.css', {
93 | allChunks: true
94 | }),
95 | ];
96 |
97 | // dev server
98 | var devServer = {
99 | contentBase: './server',
100 | colors: true,
101 | historyApiFallback: false,
102 | port: 8080, // defaults to "8080"
103 | hot: true, // Hot Module Replacement
104 | inline: true, // Livereload
105 | host: '0.0.0.0',
106 | disableHostCheck: true
107 | };
108 |
109 | module.exports = {
110 | entry: entry,
111 | devtool: devtool,
112 | output: output,
113 | loaders: loaders,
114 | devPlugins: devPlugins,
115 | productionPlugins: productionPlugins,
116 | devServer: devServer,
117 | postcss: function () {
118 | return [precss, autoprefixer];
119 | },
120 | version: version
121 | };
122 |
--------------------------------------------------------------------------------
/src/components/logo/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 { i18n, lan } from '../../unit/const';
7 |
8 | export default class Logo extends React.Component {
9 | constructor() {
10 | super();
11 | this.state = {
12 | style: style.r1,
13 | display: 'none',
14 | };
15 | }
16 | componentWillMount() {
17 | this.animate(this.props);
18 | }
19 | componentWillReceiveProps(nextProps) {
20 | if ( // 只有在游戏进入开始, 或结束时 触发改变
21 | (
22 | [this.props.cur, nextProps.cur].indexOf(false) !== -1 &&
23 | (this.props.cur !== nextProps.cur)
24 | ) ||
25 | (this.props.reset !== nextProps.reset)
26 | ) {
27 | this.animate(nextProps);
28 | }
29 | }
30 | shouldComponentUpdate({ cur, reset }) {
31 | return cur !== this.props.cur || reset !== this.props.reset || !cur;
32 | }
33 | animate({ cur, reset }) {
34 | clearTimeout(Logo.timeout);
35 | this.setState({
36 | style: style.r1,
37 | display: 'none',
38 | });
39 | if (cur || reset) {
40 | this.setState({ display: 'none' });
41 | return;
42 | }
43 |
44 | let m = 'r'; // 方向
45 | let count = 0;
46 |
47 | const set = (func, delay) => {
48 | if (!func) {
49 | return;
50 | }
51 | Logo.timeout = setTimeout(func, delay);
52 | };
53 |
54 | const show = (func) => { // 显示
55 | set(() => {
56 | this.setState({
57 | display: 'block',
58 | });
59 | if (func) {
60 | func();
61 | }
62 | }, 150);
63 | };
64 |
65 | const hide = (func) => { // 隐藏
66 | set(() => {
67 | this.setState({
68 | display: 'none',
69 | });
70 | if (func) {
71 | func();
72 | }
73 | }, 150);
74 | };
75 |
76 | const eyes = (func, delay1, delay2) => { // 龙在眨眼睛
77 | set(() => {
78 | this.setState({ style: style[m + 2] });
79 | set(() => {
80 | this.setState({ style: style[m + 1] });
81 | if (func) {
82 | func();
83 | }
84 | }, delay2);
85 | }, delay1);
86 | };
87 |
88 | const run = (func) => { // 开始跑步啦!
89 | set(() => {
90 | this.setState({ style: style[m + 4] });
91 | set(() => {
92 | this.setState({ style: style[m + 3] });
93 | count++;
94 | if (count === 10 || count === 20 || count === 30) {
95 | m = m === 'r' ? 'l' : 'r';
96 | }
97 | if (count < 40) {
98 | run(func);
99 | return;
100 | }
101 | this.setState({ style: style[m + 1] });
102 | if (func) {
103 | set(func, 4000);
104 | }
105 | }, 100);
106 | }, 100);
107 | };
108 |
109 | const dra = () => {
110 | count = 0;
111 | eyes(() => {
112 | eyes(() => {
113 | eyes(() => {
114 | this.setState({ style: style[m + 2] });
115 | run(dra);
116 | }, 150, 150);
117 | }, 150, 150);
118 | }, 1000, 1500);
119 | };
120 |
121 | show(() => { // 忽隐忽现
122 | hide(() => {
123 | show(() => {
124 | hide(() => {
125 | show(() => {
126 | dra(); // 开始运动
127 | });
128 | });
129 | });
130 | });
131 | });
132 | }
133 | render() {
134 | if (this.props.cur) {
135 | return null;
136 | }
137 | return (
138 |
142 | );
143 | }
144 | }
145 |
146 | Logo.propTypes = {
147 | cur: propTypes.bool,
148 | reset: propTypes.bool.isRequired,
149 | };
150 | Logo.statics = {
151 | timeout: null,
152 | };
153 |
--------------------------------------------------------------------------------
/src/components/decorate/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import cn from 'classnames';
3 |
4 | import { i18n, lan } from '../../unit/const';
5 | import style from './index.less';
6 |
7 | export default class Decorate extends React.Component {
8 | shouldComponentUpdate() {
9 | return false;
10 | }
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
{i18n.title[lan]}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | );
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/components/matrix/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import immutable, { List } from 'immutable';
3 | import classnames from 'classnames';
4 | import propTypes from 'prop-types';
5 |
6 | import style from './index.less';
7 | import { isClear } from '../../unit/';
8 | import { fillLine, blankLine } from '../../unit/const';
9 | import states from '../../control/states';
10 |
11 | const t = setTimeout;
12 |
13 | export default class Matrix extends React.Component {
14 | constructor() {
15 | super();
16 | this.state = {
17 | clearLines: false,
18 | animateColor: 2,
19 | isOver: false,
20 | overState: null,
21 | };
22 | }
23 | componentWillReceiveProps(nextProps = {}) {
24 | const clears = isClear(nextProps.matrix);
25 | const overs = nextProps.reset;
26 | this.setState({
27 | clearLines: clears,
28 | isOver: overs,
29 | });
30 | if (clears && !this.state.clearLines) {
31 | this.clearAnimate(clears);
32 | }
33 | if (!clears && overs && !this.state.isOver) {
34 | this.over(nextProps);
35 | }
36 | }
37 | shouldComponentUpdate(nextProps = {}) { // 使用Immutable 比较两个List 是否相等
38 | const props = this.props;
39 | return !(
40 | immutable.is(nextProps.matrix, props.matrix) &&
41 | immutable.is(
42 | (nextProps.cur && nextProps.cur.shape),
43 | (props.cur && props.cur.shape)
44 | ) &&
45 | immutable.is(
46 | (nextProps.cur && nextProps.cur.xy),
47 | (props.cur && props.cur.xy)
48 | )
49 | ) || this.state.clearLines
50 | || this.state.isOver;
51 | }
52 | getResult(props = this.props) {
53 | const cur = props.cur;
54 | const shape = cur && cur.shape;
55 | const xy = cur && cur.xy;
56 |
57 | let matrix = props.matrix;
58 | const clearLines = this.state.clearLines;
59 | if (clearLines) {
60 | const animateColor = this.state.animateColor;
61 | clearLines.forEach((index) => {
62 | matrix = matrix.set(index, List([
63 | animateColor,
64 | animateColor,
65 | animateColor,
66 | animateColor,
67 | animateColor,
68 | animateColor,
69 | animateColor,
70 | animateColor,
71 | animateColor,
72 | animateColor,
73 | ]));
74 | });
75 | } else if (shape) {
76 | shape.forEach((m, k1) => (
77 | m.forEach((n, k2) => {
78 | if (n && xy.get(0) + k1 >= 0) { // 竖坐标可以为负
79 | let line = matrix.get(xy.get(0) + k1);
80 | let color;
81 | if (line.get(xy.get(1) + k2) === 1 && !clearLines) { // 矩阵与方块重合
82 | color = 2;
83 | } else {
84 | color = 1;
85 | }
86 | line = line.set(xy.get(1) + k2, color);
87 | matrix = matrix.set(xy.get(0) + k1, line);
88 | }
89 | })
90 | ));
91 | }
92 | return matrix;
93 | }
94 | clearAnimate() {
95 | const anima = (callback) => {
96 | t(() => {
97 | this.setState({
98 | animateColor: 0,
99 | });
100 | t(() => {
101 | this.setState({
102 | animateColor: 2,
103 | });
104 | if (typeof callback === 'function') {
105 | callback();
106 | }
107 | }, 100);
108 | }, 100);
109 | };
110 | anima(() => {
111 | anima(() => {
112 | anima(() => {
113 | t(() => {
114 | states.clearLines(this.props.matrix, this.state.clearLines);
115 | }, 100);
116 | });
117 | });
118 | });
119 | }
120 | over(nextProps) {
121 | let overState = this.getResult(nextProps);
122 | this.setState({
123 | overState,
124 | });
125 |
126 | const exLine = (index) => {
127 | if (index <= 19) {
128 | overState = overState.set(19 - index, List(fillLine));
129 | } else if (index >= 20 && index <= 39) {
130 | overState = overState.set(index - 20, List(blankLine));
131 | } else {
132 | states.overEnd();
133 | return;
134 | }
135 | this.setState({
136 | overState,
137 | });
138 | };
139 |
140 | for (let i = 0; i <= 40; i++) {
141 | t(exLine.bind(null, i), 40 * (i + 1));
142 | }
143 | }
144 | render() {
145 | let matrix;
146 | if (this.state.isOver) {
147 | matrix = this.state.overState;
148 | } else {
149 | matrix = this.getResult();
150 | }
151 | return (
152 | {
153 | matrix.map((p, k1) => (
154 | {
155 | p.map((e, k2) => )
162 | }
163 |
))
164 | }
165 |
166 | );
167 | }
168 | }
169 |
170 | Matrix.propTypes = {
171 | matrix: propTypes.object.isRequired,
172 | cur: propTypes.object,
173 | reset: propTypes.bool.isRequired,
174 | };
175 |
--------------------------------------------------------------------------------
/src/components/keyboard/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Immutable from 'immutable';
3 | import propTypes from 'prop-types';
4 |
5 | import style from './index.less';
6 | import Button from './button';
7 | import store from '../../store';
8 | import todo from '../../control/todo';
9 | import { i18n, lan } from '../../unit/const';
10 |
11 | export default class Keyboard extends React.Component {
12 | componentDidMount() {
13 | const touchEventCatch = {}; // 对于手机操作, 触发了touchstart, 将作出记录, 不再触发后面的mouse事件
14 |
15 | // 在鼠标触发mousedown时, 移除元素时可以不触发mouseup, 这里做一个兼容, 以mouseout模拟mouseup
16 | const mouseDownEventCatch = {};
17 | document.addEventListener('touchstart', (e) => {
18 | if (e.preventDefault) {
19 | e.preventDefault();
20 | }
21 | }, true);
22 |
23 | // 解决issue: https://github.com/chvin/react-tetris/issues/24
24 | document.addEventListener('touchend', (e) => {
25 | if (e.preventDefault) {
26 | e.preventDefault();
27 | }
28 | }, true);
29 |
30 | // 阻止双指放大
31 | document.addEventListener('gesturestart', (e) => {
32 | if (e.preventDefault) {
33 | event.preventDefault();
34 | }
35 | });
36 |
37 | document.addEventListener('mousedown', (e) => {
38 | if (e.preventDefault) {
39 | e.preventDefault();
40 | }
41 | }, true);
42 |
43 | Object.keys(todo).forEach((key) => {
44 | this[`dom_${key}`].dom.addEventListener('mousedown', () => {
45 | if (touchEventCatch[key] === true) {
46 | return;
47 | }
48 | todo[key].down(store);
49 | mouseDownEventCatch[key] = true;
50 | }, true);
51 | this[`dom_${key}`].dom.addEventListener('mouseup', () => {
52 | if (touchEventCatch[key] === true) {
53 | touchEventCatch[key] = false;
54 | return;
55 | }
56 | todo[key].up(store);
57 | mouseDownEventCatch[key] = false;
58 | }, true);
59 | this[`dom_${key}`].dom.addEventListener('mouseout', () => {
60 | if (mouseDownEventCatch[key] === true) {
61 | todo[key].up(store);
62 | }
63 | }, true);
64 | this[`dom_${key}`].dom.addEventListener('touchstart', () => {
65 | touchEventCatch[key] = true;
66 | todo[key].down(store);
67 | }, true);
68 | this[`dom_${key}`].dom.addEventListener('touchend', () => {
69 | todo[key].up(store);
70 | }, true);
71 | });
72 | }
73 | shouldComponentUpdate({ keyboard, filling }) {
74 | return !Immutable.is(keyboard, this.props.keyboard) || filling !== this.props.filling;
75 | }
76 | render() {
77 | const keyboard = this.props.keyboard;
78 | return (
79 |
85 |
163 | );
164 | }
165 | }
166 |
167 | Keyboard.propTypes = {
168 | filling: propTypes.number.isRequired,
169 | keyboard: propTypes.object.isRequired,
170 | };
171 |
--------------------------------------------------------------------------------
/src/containers/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import classnames from 'classnames';
4 | import propTypes from 'prop-types';
5 |
6 | import style from './index.less';
7 |
8 | import Matrix from '../components/matrix';
9 | import Decorate from '../components/decorate';
10 | import Number from '../components/number';
11 | import Next from '../components/next';
12 | import Music from '../components/music';
13 | import Pause from '../components/pause';
14 | import Point from '../components/point';
15 | import Logo from '../components/logo';
16 | import Keyboard from '../components/keyboard';
17 | import Guide from '../components/guide';
18 |
19 | import { transform, lastRecord, speeds, i18n, lan } from '../unit/const';
20 | import { visibilityChangeEvent, isFocus } from '../unit/';
21 | import states from '../control/states';
22 |
23 | class App extends React.Component {
24 | constructor() {
25 | super();
26 | this.state = {
27 | w: document.documentElement.clientWidth,
28 | h: document.documentElement.clientHeight,
29 | };
30 | }
31 | componentWillMount() {
32 | window.addEventListener('resize', this.resize.bind(this), true);
33 | }
34 | componentDidMount() {
35 | if (visibilityChangeEvent) { // 将页面的焦点变换写入store
36 | document.addEventListener(visibilityChangeEvent, () => {
37 | states.focus(isFocus());
38 | }, false);
39 | }
40 |
41 | if (lastRecord) { // 读取记录
42 | if (lastRecord.cur && !lastRecord.pause) { // 拿到上一次游戏的状态, 如果在游戏中且没有暂停, 游戏继续
43 | const speedRun = this.props.speedRun;
44 | let timeout = speeds[speedRun - 1] / 2; // 继续时, 给予当前下落速度一半的停留时间
45 | // 停留时间不小于最快速的速度
46 | timeout = speedRun < speeds[speeds.length - 1] ? speeds[speeds.length - 1] : speedRun;
47 | states.auto(timeout);
48 | }
49 | if (!lastRecord.cur) {
50 | states.overStart();
51 | }
52 | } else {
53 | states.overStart();
54 | }
55 | }
56 | resize() {
57 | this.setState({
58 | w: document.documentElement.clientWidth,
59 | h: document.documentElement.clientHeight,
60 | });
61 | }
62 | render() {
63 | let filling = 0;
64 | const size = (() => {
65 | const w = this.state.w;
66 | const h = this.state.h;
67 | const ratio = h / w;
68 | let scale;
69 | let css = {};
70 | if (ratio < 1.5) {
71 | scale = h / 960;
72 | } else {
73 | scale = w / 640;
74 | filling = (h - (960 * scale)) / scale / 3;
75 | css = {
76 | paddingTop: Math.floor(filling) + 42,
77 | paddingBottom: Math.floor(filling),
78 | marginTop: Math.floor(-480 - (filling * 1.5)),
79 | };
80 | }
81 | css[transform] = `scale(${scale})`;
82 | return css;
83 | })();
84 |
85 | return (
86 |
90 |
91 |
92 |
93 |
94 |
99 |
100 |
101 |
102 |
{ this.props.cur ? i18n.cleans[lan] : i18n.startLine[lan] }
103 |
104 |
{i18n.level[lan]}
105 |
109 |
{i18n.next[lan]}
110 |
111 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | );
124 | }
125 | }
126 |
127 | App.propTypes = {
128 | music: propTypes.bool.isRequired,
129 | pause: propTypes.bool.isRequired,
130 | matrix: propTypes.object.isRequired,
131 | next: propTypes.string.isRequired,
132 | cur: propTypes.object,
133 | dispatch: propTypes.func.isRequired,
134 | speedStart: propTypes.number.isRequired,
135 | speedRun: propTypes.number.isRequired,
136 | startLines: propTypes.number.isRequired,
137 | clearLines: propTypes.number.isRequired,
138 | points: propTypes.number.isRequired,
139 | max: propTypes.number.isRequired,
140 | reset: propTypes.bool.isRequired,
141 | drop: propTypes.bool.isRequired,
142 | keyboard: propTypes.object.isRequired,
143 | };
144 |
145 | const mapStateToProps = (state) => ({
146 | pause: state.get('pause'),
147 | music: state.get('music'),
148 | matrix: state.get('matrix'),
149 | next: state.get('next'),
150 | cur: state.get('cur'),
151 | speedStart: state.get('speedStart'),
152 | speedRun: state.get('speedRun'),
153 | startLines: state.get('startLines'),
154 | clearLines: state.get('clearLines'),
155 | points: state.get('points'),
156 | max: state.get('max'),
157 | reset: state.get('reset'),
158 | drop: state.get('drop'),
159 | keyboard: state.get('keyboard'),
160 | });
161 |
162 | export default connect(mapStateToProps)(App);
163 |
--------------------------------------------------------------------------------
/docs/css-1.0.1.css:
--------------------------------------------------------------------------------
1 | body{background:#009688;padding:0;margin:0;font:20px/1 HanHei SC,PingHei,PingFang SC,STHeitiSC-Light,Helvetica Neue,Helvetica,Arial,sans-serif;overflow:hidden;cursor:default;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-moz-font-feature-settings:"liga","kern";direction:ltr;text-align:left}.r{float:right}.l{float:left}.clear{clear:both}.bg{background:url("//img.alicdn.com/tps/TB1qq7kNXXXXXacXFXXXXXXXXXX-400-186.png") no-repeat;overflow:hidden}*{box-sizing:border-box;margin:0;padding:0;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}._3Lk6{width:640px;padding-top:42px;box-shadow:inset 0 0 10px #fff;border-radius:20px;position:absolute;top:50%;left:50%;margin:-480px 0 0 -320px;background:#efcc19}._3Lk6 b{display:block;width:20px;height:20px;padding:2px;border:2px solid #879372;margin:0 2px 2px 0;float:left}._3Lk6 b:after{content:"";display:block;width:12px;height:12px;background:#879372;overflow:hidden}._3Lk6 b.c{border-color:#000}._3Lk6 b.c:after{background:#000}._3Lk6 b.d{border-color:#560000}._3Lk6 b.d:after{background:#560000}._1fjB{width:480px;padding:45px 0 35px;border:solid #000;border-width:0 10px 10px;margin:0 auto;position:relative}._1fjB._3YUe{transform:translateY(5px)}._2iZA{width:390px;height:478px;border:5px solid;border-color:#987f0f #fae36c #fae36c #987f0f;margin:0 auto;position:relative}._2iZA ._2lJh{width:380px;height:468px;margin:0 auto;background:#9ead86;padding:8px;border:2px solid #494536}._1deS{width:108px;position:absolute;top:0;right:15px}._1deS p{line-height:47px;height:57px;padding:10px 0 0;white-space:nowrap;clear:both}._1deS ._8hag{position:absolute;width:114px;top:426px;left:0}._6pVK{border:2px solid #000;padding:3px 1px 1px 3px;width:228px}._6pVK p{width:220px;height:22px}._2OLA h1{text-align:center;font-weight:400;top:-12px;margin:0;padding:0;font-size:30px}._2OLA .DOXx,._2OLA h1{position:absolute;width:100%;left:0}._2OLA .DOXx{height:10px;top:0;overflow:hidden}._2OLA .DOXx span{display:block;width:10px;height:10px;overflow:hidden;background:#000}._2OLA .DOXx span._1xND{margin-right:10px}._2OLA .DOXx span._1cYd{margin-left:10px}._2OLA .nVeA{position:absolute;right:-70px;top:20px;width:44px}._2OLA .nVeA em{display:block;width:22px;height:22px;overflow:hidden;float:left}._2OLA .nVeA p{height:22px;clear:both}._2OLA .nVeA._395z{right:auto;left:-70px}.iHKP{height:24px;font-size:14px;float:right}.iHKP span{float:left;width:14px;height:24px}.iHKP ._2hru{background-position:-75px -25px}.iHKP ._2B-l{background-position:-89px -25px}.iHKP .ShGQ{background-position:-103px -25px}.iHKP ._2V1K{background-position:-117px -25px}.iHKP ._3bYF{background-position:-131px -25px}.iHKP ._1Z7B{background-position:-145px -25px}.iHKP ._1-BZ{background-position:-159px -25px}.iHKP ._3_id{background-position:-173px -25px}.iHKP ._3_Z_{background-position:-187px -25px}.iHKP .bNJM{background-position:-201px -25px}.iHKP ._2kln{background-position:-215px -25px}.iHKP .hOfM{background-position:-243px -25px}.iHKP ._2tuY{background-position:-229px -25px}._3Wmt div{height:22px;width:88px;float:right}.EHci{width:25px;height:21px;background-position:-175px -75px;position:absolute;top:2px;left:-12px}.EHci.TTF4{background-position:-150px -75px}._37mu{width:20px;height:18px;background-position:-100px -75px;position:absolute;top:3px;left:18px}._37mu._1vhq{background-position:-75px -75px}._20Jp{width:224px;height:200px;left:12px;text-align:center;overflow:hidden}._20Jp,._20Jp p{position:absolute;top:100px}._20Jp p{width:100%;line-height:1.4;left:0;font-family:initial;letter-spacing:6px;text-shadow:1px 1px 1px hsla(0,0%,100%,.35)}._20Jp .AFTs{width:80px;height:86px;margin:0 auto}._20Jp .AFTs,._20Jp .AFTs._3j_b,._20Jp .AFTs._26pe{background-position:0 -100px}._20Jp .AFTs._1Fxd,._20Jp .AFTs._7ELJ{background-position:-100px -100px}._20Jp .AFTs._1JBw,._20Jp .AFTs._9lMe{background-position:-200px -100px}._20Jp .AFTs._2aGx,._20Jp .AFTs._3aQ-{background-position:-300px -100px}._20Jp .AFTs._1JBw,._20Jp .AFTs._2aGx,._20Jp .AFTs._7ELJ,._20Jp .AFTs._26pe{transform:scaleX(-1);-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);-moz-transform:scaleX(-1);-o-transform:scaleX(-1)}.J9SA{width:580px;height:330px;margin:20px auto 0;position:relative}._1pg0{text-align:center;color:#111;position:absolute;white-space:nowrap;line-height:1.6}._1pg0.oW6K{font-size:16px}._1pg0 span._1zCL{position:absolute;top:5px;left:102px}._1pg0 i{display:block;position:relative;border:1px solid #000;border-radius:50%;box-shadow:0 3px 3px rgba(0,0,0,.2)}._1pg0 i:after,._1pg0 i:before{content:"";display:block;width:100%;height:100%;position:absolute;top:0;left:0;border-radius:50%;box-shadow:inset 0 5px 10px hsla(0,0%,100%,.8)}._1pg0 i:after{box-shadow:inset 0 -5px 10px rgba(0,0,0,.8)}._1pg0 i._23aw:before{box-shadow:inset 0 -3px 6px hsla(0,0%,100%,.6)}._1pg0 i._23aw:after{box-shadow:inset 0 5px 5px rgba(0,0,0,.6)}._1pg0._23pZ i{background:#5a65f1;background:-moz-linear-gradient(top,#6e77ef,#6e77ef)}._1pg0.RBZg i{background:#2dc421;background:-moz-linear-gradient(top,#4bc441,#4bc441)}._1pg0._3kg_ i{background:#dd1a1a;background:-moz-linear-gradient(top,#dc3333,#dc3333)}._1pg0.p4fG i{width:160px;height:160px}._1pg0._2TvZ i{width:100px;height:100px}._1pg0.oW6K i{width:52px;height:52px;box-shadow:1px 1px 1px rgba(0,0,0,.2)}._1pg0.oW6K i:after,._1pg0.oW6K i:before{box-shadow:inset 0 3px 6px hsla(0,0%,100%,.8)}._1pg0.oW6K i:after{box-shadow:inset 0 -3px 6px rgba(0,0,0,.8)}._1pg0.oW6K i._23aw:before{box-shadow:inset 0 -1px 2px hsla(0,0%,100%,.6)}._1pg0.oW6K i._23aw:after{box-shadow:inset 0 3px 3px rgba(0,0,0,.7)}._1pg0._2TvZ em{display:block;width:0;height:0;border:8px solid;border-color:transparent transparent #111;position:absolute;top:50%;left:50%;margin:-12px 0 0 -8px}._2iIk{position:absolute;left:115%;right:115%;text-align:center;line-height:1;white-space:nowrap}._2iIk._15Dj{right:auto;bottom:5%}._2iIk._I0Q{left:auto;bottom:5%}._2iIk p{text-align:left;margin-bottom:350px;opacity:.5}._2iIk p iframe{margin-top:20px}._2iIk a{color:#005850;font-size:30px;position:relative;z-index:1;cursor:alias;text-decoration:none}._2iIk._111n{left:auto;top:5%;width:250px;height:250px;opacity:.5;text-align:right;cursor:none}._2iIk._111n:hover img{width:100%;height:100%}._2iIk._111n img{width:38px;height:38px}._2iIk>div{width:100px;height:54px;background:#364d4b;display:inline-block;border-radius:4px;position:relative;color:#acc3c1;font-size:16px}._2iIk>div em{display:block;width:0;height:0;border:6px solid;border-color:transparent transparent #acc3c1;position:absolute;top:50%;left:50%;margin:-9px 0 0 -6px}._2iIk>div:after,._2iIk>div:before{content:"";display:block;width:100%;height:100%;position:absolute;top:0;left:0;border-radius:4px;box-shadow:inset 0 5px 10px hsla(0,0%,100%,.15)}._2iIk>div:before{box-shadow:inset 0 -5px 10px rgba(0,0,0,.15)}._2iIk ._2fH-{height:60px;display:block;margin:0 auto 2px}._2iIk ._1Pbk{margin:0 10px}._2iIk ._3qj_{left:auto;bottom:5%;height:80px;width:400px;line-height:80px;letter-spacing:2px}
2 | /*# sourceMappingURL=css-1.0.1.css.map*/
--------------------------------------------------------------------------------
/src/control/states.js:
--------------------------------------------------------------------------------
1 | import { List } from 'immutable';
2 | import store from '../store';
3 | import { want, isClear, isOver } from '../unit/';
4 | import actions from '../actions';
5 | import { speeds, blankLine, blankMatrix, clearPoints, eachLines } from '../unit/const';
6 | import { music } from '../unit/music';
7 |
8 |
9 | const getStartMatrix = (startLines) => { // 生成startLines
10 | const getLine = (min, max) => { // 返回标亮个数在min~max之间一行方块, (包含边界)
11 | const count = parseInt((((max - min) + 1) * Math.random()) + min, 10);
12 | const line = [];
13 | for (let i = 0; i < count; i++) { // 插入高亮
14 | line.push(1);
15 | }
16 | for (let i = 0, len = 10 - count; i < len; i++) { // 在随机位置插入灰色
17 | const index = parseInt(((line.length + 1) * Math.random()), 10);
18 | line.splice(index, 0, 0);
19 | }
20 |
21 | return List(line);
22 | };
23 | let startMatrix = List([]);
24 |
25 | for (let i = 0; i < startLines; i++) {
26 | if (i <= 2) { // 0-3
27 | startMatrix = startMatrix.push(getLine(5, 8));
28 | } else if (i <= 6) { // 4-6
29 | startMatrix = startMatrix.push(getLine(4, 9));
30 | } else { // 7-9
31 | startMatrix = startMatrix.push(getLine(3, 9));
32 | }
33 | }
34 | for (let i = 0, len = 20 - startLines; i < len; i++) { // 插入上部分的灰色
35 | startMatrix = startMatrix.unshift(List(blankLine));
36 | }
37 | return startMatrix;
38 | };
39 |
40 | const states = {
41 | // 自动下落setTimeout变量
42 | fallInterval: null,
43 |
44 | // 游戏开始
45 | start: () => {
46 | if (music.start) {
47 | music.start();
48 | }
49 | const state = store.getState();
50 | states.dispatchPoints(0);
51 | store.dispatch(actions.speedRun(state.get('speedStart')));
52 | const startLines = state.get('startLines');
53 | const startMatrix = getStartMatrix(startLines);
54 | store.dispatch(actions.matrix(startMatrix));
55 | store.dispatch(actions.moveBlock({ type: state.get('next') }));
56 | store.dispatch(actions.nextBlock());
57 | states.auto();
58 | },
59 |
60 | // 自动下落
61 | auto: (timeout) => {
62 | const out = (timeout < 0 ? 0 : timeout);
63 | let state = store.getState();
64 | let cur = state.get('cur');
65 | const fall = () => {
66 | state = store.getState();
67 | cur = state.get('cur');
68 | const next = cur.fall();
69 | if (want(next, state.get('matrix'))) {
70 | store.dispatch(actions.moveBlock(next));
71 | states.fallInterval = setTimeout(fall, speeds[state.get('speedRun') - 1]);
72 | } else {
73 | let matrix = state.get('matrix');
74 | const shape = cur && cur.shape;
75 | const xy = cur && cur.xy;
76 | shape.forEach((m, k1) => (
77 | m.forEach((n, k2) => {
78 | if (n && xy.get(0) + k1 >= 0) { // 竖坐标可以为负
79 | let line = matrix.get(xy.get(0) + k1);
80 | line = line.set(xy.get(1) + k2, 1);
81 | matrix = matrix.set(xy.get(0) + k1, line);
82 | }
83 | })
84 | ));
85 | states.nextAround(matrix);
86 | }
87 | };
88 | clearTimeout(states.fallInterval);
89 | states.fallInterval = setTimeout(fall,
90 | out === undefined ? speeds[state.get('speedRun') - 1] : out);
91 | },
92 |
93 | // 一个方块结束, 触发下一个
94 | nextAround: (matrix, stopDownTrigger) => {
95 | clearTimeout(states.fallInterval);
96 | store.dispatch(actions.lock(true));
97 | store.dispatch(actions.matrix(matrix));
98 | if (typeof stopDownTrigger === 'function') {
99 | stopDownTrigger();
100 | }
101 |
102 | const addPoints = (store.getState().get('points') + 10) +
103 | ((store.getState().get('speedRun') - 1) * 2); // 速度越快, 得分越高
104 |
105 | states.dispatchPoints(addPoints);
106 |
107 | if (isClear(matrix)) {
108 | if (music.clear) {
109 | music.clear();
110 | }
111 | return;
112 | }
113 | if (isOver(matrix)) {
114 | if (music.gameover) {
115 | music.gameover();
116 | }
117 | states.overStart();
118 | return;
119 | }
120 | setTimeout(() => {
121 | store.dispatch(actions.lock(false));
122 | store.dispatch(actions.moveBlock({ type: store.getState().get('next') }));
123 | store.dispatch(actions.nextBlock());
124 | states.auto();
125 | }, 100);
126 | },
127 |
128 | // 页面焦点变换
129 | focus: (isFocus) => {
130 | store.dispatch(actions.focus(isFocus));
131 | if (!isFocus) {
132 | clearTimeout(states.fallInterval);
133 | return;
134 | }
135 | const state = store.getState();
136 | if (state.get('cur') && !state.get('reset') && !state.get('pause')) {
137 | states.auto();
138 | }
139 | },
140 |
141 | // 暂停
142 | pause: (isPause) => {
143 | store.dispatch(actions.pause(isPause));
144 | if (isPause) {
145 | clearTimeout(states.fallInterval);
146 | return;
147 | }
148 | states.auto();
149 | },
150 |
151 | // 消除行
152 | clearLines: (matrix, lines) => {
153 | const state = store.getState();
154 | let newMatrix = matrix;
155 | lines.forEach(n => {
156 | newMatrix = newMatrix.splice(n, 1);
157 | newMatrix = newMatrix.unshift(List(blankLine));
158 | });
159 | store.dispatch(actions.matrix(newMatrix));
160 | store.dispatch(actions.moveBlock({ type: state.get('next') }));
161 | store.dispatch(actions.nextBlock());
162 | states.auto();
163 | store.dispatch(actions.lock(false));
164 | const clearLines = state.get('clearLines') + lines.length;
165 | store.dispatch(actions.clearLines(clearLines)); // 更新消除行
166 |
167 | const addPoints = store.getState().get('points') +
168 | clearPoints[lines.length - 1]; // 一次消除的行越多, 加分越多
169 | states.dispatchPoints(addPoints);
170 |
171 | const speedAdd = Math.floor(clearLines / eachLines); // 消除行数, 增加对应速度
172 | let speedNow = state.get('speedStart') + speedAdd;
173 | speedNow = speedNow > 6 ? 6 : speedNow;
174 | store.dispatch(actions.speedRun(speedNow));
175 | },
176 |
177 | // 游戏结束, 触发动画
178 | overStart: () => {
179 | clearTimeout(states.fallInterval);
180 | store.dispatch(actions.lock(true));
181 | store.dispatch(actions.reset(true));
182 | store.dispatch(actions.pause(false));
183 | },
184 |
185 | // 游戏结束动画完成
186 | overEnd: () => {
187 | store.dispatch(actions.matrix(blankMatrix));
188 | store.dispatch(actions.moveBlock({ reset: true }));
189 | store.dispatch(actions.reset(false));
190 | store.dispatch(actions.lock(false));
191 | store.dispatch(actions.clearLines(0));
192 | },
193 |
194 | // 写入分数
195 | dispatchPoints: (point) => { // 写入分数, 同时判断是否创造最高分
196 | store.dispatch(actions.points(point));
197 | if (point > 0 && point > store.getState().get('max')) {
198 | store.dispatch(actions.max(point));
199 | }
200 | },
201 | };
202 |
203 | export default states;
204 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### English introduction
2 | Please view [README-EN.md](https://github.com/chvin/react-tetris/blob/master/README-EN.md)
3 |
4 | ----
5 | ## 用React、Redux、Immutable做俄罗斯方块
6 |
7 | ----
8 | 俄罗斯方块是一直各类程序语言热衷实现的经典游戏,JavaScript的实现版本也有很多,用React 做好俄罗斯方块则成了我一个目标。
9 |
10 | 戳:[https://chvin.github.io/react-tetris/](https://chvin.github.io/react-tetris/) 玩一玩!
11 |
12 | ----
13 | ### 效果预览
14 | 
15 |
16 | 正常速度的录制,体验流畅。
17 |
18 | ### 响应式
19 | 
20 |
21 | 不仅指屏幕的自适应,而是`在PC使用键盘、在手机使用手指的响应式操作`:
22 |
23 | 
24 |
25 | ### 数据持久化
26 | 
27 |
28 | 玩单机游戏最怕什么?断电。通过订阅 `store.subscribe`,将state储存在localStorage,精确记录所有状态。网页关了刷新了、程序崩溃了、手机没电了,重新打开连接,都可以继续。
29 |
30 | ### Redux 状态预览([Redux DevTools extension](https://github.com/zalmoxisus/redux-devtools-extension))
31 | 
32 |
33 | Redux设计管理了所有应存的状态,这是上面持久化的保证。
34 |
35 | ----
36 | 游戏框架使用的是 React + Redux,其中再加入了 Immutable,用它的实例来做来Redux的state。(有关React和Redux的介绍可以看:[React入门实例](http://www.ruanyifeng.com/blog/2015/03/react.html)、[Redux中文文档](https://camsong.github.io/redux-in-chinese/index.html))
37 |
38 | ## 1、什么是 Immutable?
39 | Immutable 是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。
40 |
41 | ### 初识:
42 | 让我们看下面一段代码:
43 | ``` JavaScript
44 | function keyLog(touchFn) {
45 | let data = { key: 'value' };
46 | f(data);
47 | console.log(data.key); // 猜猜会打印什么?
48 | }
49 | ```
50 | 不查看f,不知道它对 `data` 做了什么,无法确认会打印什么。但如果 `data` 是 Immutable,你可以确定打印的是 `value`:
51 | ``` JavaScript
52 | function keyLog(touchFn) {
53 | let data = Immutable.Map({ key: 'value' });
54 | f(data);
55 | console.log(data.get('key')); // value
56 | }
57 | ```
58 |
59 | JavaScript 中的`Object`与`Array`等使用的是引用赋值,新的对象简单的引用了原始对象,改变新也将影响旧的:
60 | ``` JavaScript
61 | foo = {a: 1}; bar = foo; bar.a = 2;
62 | foo.a // 2
63 | ```
64 | 虽然这样做可以节约内存,但当应用复杂后,造成了状态不可控,是很大的隐患,节约的内存优点变得得不偿失。
65 |
66 | Immutable则不一样,相应的:
67 | ``` JavaScript
68 | foo = Immutable.Map({ a: 1 }); bar = foo.set('a', 2);
69 | foo.get('a') // 1
70 | ```
71 |
72 | ### 简洁:
73 | 在`Redux`中,它的最优做法是每个`reducer`都返回一个新的对象(数组),所以我们常常会看到这样的代码:
74 | ``` JavaScript
75 | // reducer
76 | ...
77 | return [
78 | ...oldArr.slice(0, 3),
79 | newValue,
80 | ...oldArr.slice(4)
81 | ];
82 | ```
83 | 为了返回新的对象(数组),不得不有上面奇怪的样子,而在使用更深的数据结构时会变的更棘手。
84 | 让我们看看Immutable的做法:
85 | ``` JavaScript
86 | // reducer
87 | ...
88 | return oldArr.set(4, newValue);
89 | ```
90 | 是不是很简洁?
91 |
92 | ### 关于 “===”:
93 | 我们知道对于`Object`与`Array`的`===`比较,是对引用地址的比较而不是“值比较”,如:
94 | ``` JavaScript
95 | {a:1, b:2, c:3} === {a:1, b:2, c:3}; // false
96 | [1, 2, [3, 4]] === [1, 2, [3, 4]]; // false
97 | ```
98 | 对于上面只能采用 `deepCopy`、`deepCompare`来遍历比较,不仅麻烦且好性能。
99 |
100 | 我们感受来一下`Immutable`的做法!
101 | ``` JavaScript
102 | map1 = Immutable.Map({a:1, b:2, c:3});
103 | map2 = Immutable.Map({a:1, b:2, c:3});
104 | Immutable.is(map1, map2); // true
105 |
106 | // List1 = Immutable.List([1, 2, Immutable.List[3, 4]]);
107 | List1 = Immutable.fromJS([1, 2, [3, 4]]);
108 | List2 = Immutable.fromJS([1, 2, [3, 4]]);
109 | Immutable.is(List1, List2); // true
110 | ```
111 | 似乎有阵清风吹过。
112 |
113 | React 做性能优化时有一个`大招`,就是使用 `shouldComponentUpdate()`,但它默认返回 `true`,即始终会执行 `render()` 方法,后面做 Virtual DOM 比较。
114 |
115 | 在使用原生属性时,为了得出shouldComponentUpdate正确的`true` or `false`,不得不用deepCopy、deepCompare来算出答案,消耗的性能很不划算。而在有了Immutable之后,使用上面的方法对深层结构的比较就变的易如反掌。
116 |
117 | 对于「俄罗斯方块」,试想棋盘是一个`二维数组`,可以移动的方块则是`形状(也是二维数组)`+`坐标`。棋盘与方块的叠加则组成了最后的结果`Matrix`。游戏中上面的属性都由`Immutable`构建,通过它的比较方法,可以轻松写好`shouldComponentUpdate`。源代码:[/src/components/matrix/index.js#L35](https://github.com/chvin/react-tetris/blob/master/src/components/matrix/index.js#L35)
118 |
119 | Immutable学习资料:
120 | * [Immutable.js](http://facebook.github.io/immutable-js/)
121 | * [Immutable 详解及 React 中实践](https://github.com/camsong/blog/issues/3)
122 |
123 |
124 | ----
125 | ## 2、如何在Redux中使用Immutable
126 | 目标:将`state` -> Immutable化。
127 | 关键的库:[gajus/redux-immutable](https://github.com/gajus/redux-immutable)
128 | 将原来 Redux提供的combineReducers改由上面的库提供:
129 | ``` JavaScript
130 | // rootReducers.js
131 | // import { combineReducers } from 'redux'; // 旧的方法
132 | import { combineReducers } from 'redux-immutable'; // 新的方法
133 |
134 | import prop1 from './prop1';
135 | import prop2 from './prop2';
136 | import prop3 from './prop3';
137 |
138 | const rootReducer = combineReducers({
139 | prop1, prop2, prop3,
140 | });
141 |
142 |
143 | // store.js
144 | // 创建store的方法和常规一样
145 | import { createStore } from 'redux';
146 | import rootReducer from './reducers';
147 |
148 | const store = createStore(rootReducer);
149 | export default store;
150 | ```
151 | 通过新的`combineReducers`将把store对象转化成Immutable,在container中使用时也会略有不同(但这正是我们想要的):
152 |
153 | ``` JavaScript
154 | const mapStateToProps = (state) => ({
155 | prop1: state.get('prop1'),
156 | prop2: state.get('prop2'),
157 | prop3: state.get('prop3'),
158 | next: state.get('next'),
159 | });
160 | export default connect(mapStateToProps)(App);
161 | ```
162 |
163 | ----
164 | ## 3、Web Audio Api
165 | 游戏里有很多不同的音效,而实际上只引用了一个音效文件:[/build/music.mp3](https://github.com/chvin/react-tetris/blob/master/build/music.mp3)。借助`Web Audio Api`能够以毫秒级精确、高频率的播放音效,这是`