提交 351db43c 编写于 作者: travelInMoonnight's avatar travelInMoonnight

Initial commit

上级 984b599e
{
"presets": ["react", "es2015"]
}
module.exports = {
"extends": "airbnb",
"installedESLint": true,
"plugins": [
"react"
],
"rules": {
"react/jsx-filename-extension": [2, { extensions: ['.js','.jsx'] }],
"func-names": [0],
"new-cap": [2, { newIsCap: true ,capIsNew: true, capIsNewExceptions: ['List', 'Map']}],
"linebreak-style": [0]
},
"env": {
"browser": true
}
};
\ No newline at end of file
### Node template
# Logs
logs
*.log
npm-debug.log*
.DS_Store
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Created by .ignore support plugin (hsz.mobi)
.idea
.idea/workspace.xml
.idea/encodings.xml
.idea/jsLibraryMappings.xml
.idea/misc.xml
.idea/modules.xml
.idea/react-tetris.iml
.idea/vcs.xml
.idea/watcherTasks.xml
\ No newline at end of file
### 中文介绍
请查看 [README.md](https://github.com/chvin/react-tetris/blob/master/README.md)
----
## Use React, Redux, Immutable to code Tetris.
----
Tetris is a classic game that has always been enthusiastically implemented in various languages. There are many versions of it in Javascript, and using React to do Tetris has become my goal.
Open [https://chvin.github.io/react-tetris/?lan=en](https://chvin.github.io/react-tetris/?lan=en) to play!
----
### Interface preview
![Interface review](https://img.alicdn.com/tps/TB1Ag7CNXXXXXaoXXXXXXXXXXXX-320-483.gif)
This is the normal speed of recording, you can see it has a smooth experience.
### Responsive
![Responsive](https://img.alicdn.com/tps/TB1AdjZNXXXXXcCapXXXXXXXXXX-480-343.gif)
Not only refers to the screen adaptation, `but the change of input depending on your platform, use of the keyboard in the PC and in the phone using the touch as input`:
![phone](https://img.alicdn.com/tps/TB1kvJyOVXXXXbhaFXXXXXXXXXX-320-555.gif)
### Data persistence
![Data persistence](https://img.alicdn.com/tps/TB1EY7cNXXXXXXraXXXXXXXXXXX-320-399.gif)
What's the worst can happen when you're playing stand-alone games? Power outage. The state is stored in the `localStorage` by subscribing to `store.subscribe`, which records exactly all the state. Web page refreshes, the program crashes, the phone is dead, just re-open the connection and you can continue playing.
### Redux state preview ([Redux DevTools extension](https://github.com/zalmoxisus/redux-devtools-extension))
![Redux state preview](https://img.alicdn.com/tps/TB1hGQqNXXXXXX3XFXXXXXXXXXX-640-381.gif)
Redux manages all the state that should be stored, which is a guarantee to be persisted as mentioned above.
----
The Game framework is the use of [React](https://facebook.github.io/react/) + [Redux](http://redux.js.org/), together with [Immutable.js](https://facebook.github.io/immutable-js/).
## 1. What is Immutable.js?
Immutable is data that can not be changed once it is created. Any modification or addition to or deletion of an Immutable object returns a new Immutable object.
### Acquaintance:
Let's look at the following code:
``` JavaScript
function keyLog(touchFn) {
let data = { key: 'value' };
f(data);
console.log(data.key); // Guess what will be printed?
}
```
If we do not look at `f`, and do not know what it did to `data`, we can not confirm what will be printed. But if `data` is *Immutable*, you can be sure that `data` haven't changed and `value` is printed:
``` JavaScript
function keyLog(touchFn) {
let data = Immutable.Map({ key: 'value' });
f(data);
console.log(data.get('key')); // value
}
```
JavaScript uses a reference assignment, meaning that the new object simply refers to the original object, changing the new will also affect the old:
``` JavaScript
foo = {a: 1}; bar = foo; bar.a = 2;
foo.a // 2
```
Although this can save memory, when the application is complex, it can result in the state not being controllable, posing a big risk. The advantages of saving memory, in this case, become more harm than good.
With Immutable.js the same doesn't happen:
``` JavaScript
foo = Immutable.Map({ a: 1 }); bar = foo.set('a', 2);
foo.get('a') // 1
```
### Concise:
In `Redux`, it's a good practice to return a new object (array) to each `reducer`, so we often see code like this:
``` JavaScript
// reducer
...
return [
...oldArr.slice(0, 3),
newValue,
...oldArr.slice(4)
];
```
In order modify one item in the array and return the new object (array), the code has this strange appearance above, and it becomes worse the deeper the data structure.
Let's take a look at Immutable.js's approach:
``` JavaScript
// reducer
...
return oldArr.set(4, newValue);
```
Isn't it simpler?
### About “===”:
We know that ```===``` operator for the `Object` and `Array` compares the reference to the address of the object rather than its "value comparison", such as:
``` JavaScript
{a:1, b:2, c:3} === {a:1, b:2, c:3}; // false
[1, 2, [3, 4]] === [1, 2, [3, 4]]; // false
```
To achieve the above we could only `deepCopy` and `deepCompare` to traverse the objects, but this is not only cumbersome it also harms performance.
Let's check `Immutable.js` approach!
``` JavaScript
map1 = Immutable.Map({a:1, b:2, c:3});
map2 = Immutable.Map({a:1, b:2, c:3});
Immutable.is(map1, map2); // true
// List1 = Immutable.List([1, 2, Immutable.List[3, 4]]);
List1 = Immutable.fromJS([1, 2, [3, 4]]);
List2 = Immutable.fromJS([1, 2, [3, 4]]);
Immutable.is(List1, List2); // true
```
It's smooth like a breeze blowing.
React has a big trick when it comes to performance tuning. It uses `shouldComponentUpdate()` to check (as the name says) if the component should be re-rendered, it returns `true` by default, which always executes the `render()` method followed by the Virtual DOM comparison.
If we don't return a new object when making state updates, we would have to use `deepCopy` and `deepCompare` to calculate if the new state is equal to the previous one, the consumption of the performance is not worth it. With Immutable.js, it's easy to compare deep structures using the method above.
For Tetris, imagine that the board is a `two-dimensional array`. The square that can be moved is `shape (also a two-dimensional array) + coordinates`. The superposition of the board and the box is composed of the final result of `Matrix`. The properties above are built by `Immutable.js`, through its comparison method, you can easily write `shouldComponentUpdate`. Source Code:[/src/components/matrix/index.js#L35](https://github.com/chvin/react-tetris/blob/master/src/components/matrix/index.js#L35)
Immutable learning materials:
* [Immutable.js](http://facebook.github.io/immutable-js/)
* [Immutable Detailed and React in practice](https://github.com/camsong/blog/issues/3)
----
## 2. How to use Immutable.js in Redux
Goal: `state` -> Immutable.
Important plug-ins: [gajus/redux-immutable](https://github.com/gajus/redux-immutable)
Will be provided by the original Redux combineReducers provided by the above plug-ins:
``` JavaScript
// rootReducers.js
// import { combineReducers } from 'redux'; // The old method
import { combineReducers } from 'redux-immutable'; // The new method
import prop1 from './prop1';
import prop2 from './prop2';
import prop3 from './prop3';
const rootReducer = combineReducers({
prop1, prop2, prop3,
});
// store.js
// Create a store method and the same general
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;
```
Through the new `combineReducers` the store object will be stored as an Immutable.js object, the container will be slightly different, but this is what we want:
``` JavaScript
const mapStateToProps = (state) => ({
prop1: state.get('prop1'),
prop2: state.get('prop2'),
prop3: state.get('prop3'),
next: state.get('next'),
});
export default connect(mapStateToProps)(App);
```
----
## 3. Web Audio Api
There are many different sound effects in the game, but in fact we keep only a reference to a sound file: [/build/music.mp3](https://github.com/chvin/react-tetris/blob/master/build/music.mp3). With the help of `Web Audio Api`, you can play audio in millisecond precision, with a high frequency, which is not possible with the `<audio>` tag. Press the arrow keys to move the box while the game is in progress, you can hear high-frequency sound.
![Web audio advanced](https://img.alicdn.com/tps/TB1fYgzNXXXXXXnXpXXXXXXXXXX-633-358.png)
`WAA` is a new set of relatively independent interface system, the audio file has a higher processing power and more professional built-in audio effects, is the W3C recommended interface, can deal with professional "sound speed, volume, environment, sound visualization, High-frequency, sound to " and other needs. The following figure describes the use of WAA process.
![Process](https://img.alicdn.com/tps/TB1nBf1NXXXXXagapXXXXXXXXXX-520-371.png)
Where `Source` represents an audio source, `Destination` represents the final output. Multiple Sources compose the Destination.
Source Code:[/src/unit/music.js](https://github.com/chvin/react-tetris/blob/master/src/unit/music.js). To achieve ajax loading mp3, and to WAA, control the playback process.
`WAA` is supported in the latest 2 versions of each browser([CanIUse](http://caniuse.com/#search=webaudio))
![browser compatibility](https://img.alicdn.com/tps/TB15z4VOVXXXXahaXXXXXXXXXXX-679-133.png)
IE and Android lack support though.
Web Audio Api learning materials:
* [Web audio concepts and usage| MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)
* [Getting Started with Web Audio API](http://www.html5rocks.com/en/tutorials/webaudio/intro/)
----
## 4. Game on the experience of optimization
* Experience:
* Press the arrow keys to move vertically and horizontally. The trigger frequency is different, the game can define the trigger frequency, instead of the original event frequency, the source code:[/src/unit/event.js](https://github.com/chvin/react-tetris/blob/master/src/unit/event.js) ;
* Left and right to move the delay can drop the speed, but when moving in the wall smaller delay; in the speed of 6 through the delay will ensure a complete horizontal movement in a row;
* The `touchstart` and `mousedown` events are also registered for the button for responsive games. When `touchstart` occurs, `mousedown` is not triggered, and when `mousedown` occurs, the `mouseup` simulator `mouseup` will also be listened to as `mouseup`, since the mouse-removed event element can not fire. Source Code:[/src/components/keyboard/index.js](https://github.com/chvin/react-tetris/blob/master/src/components/keyboard/index.js);
* The `visibilitychange` event, when the page is hidden\switch, the game will not proceed, switch back and it will continue, the `focus` state has also been written into the Redux. So when playing with the phone and the phone has a `call`, the progress of the game will be saved; PC open the game do not hear any other gameover, which is a bit like `ios` application switch;
* In the game `any` time you refresh the page, (such as the closing the tab or the end of the game) can restore the current state;
* The only pictures used in the game are ![image](https://img.alicdn.com/tps/TB1qq7kNXXXXXacXFXXXXXXXXXX-400-186.png), all the rest is CSS;
* Game compatible with Chrome, Firefox, IE9 +, Edge, etc .;
* Rules:
* You can specify the initial board (ten levels) and speed (six levels) before the start of the game;
* 100 points for 1 line, 300 points for 2 lines, 700 points for 3 lines, 1500 points for 4 lines;
* The drop speed of the box increases with the number of rows eliminated (one level for every 20 lines);
----
## 5. Experience in Development
* `shouldComponentUpdate` is written for all react components, which on the phone causes a significant performance improvement. For Large and medium-sized applications when facing performance problems, try writing your own `shouldComponentUpdate`, it will most probably help you.
* `Stateless Functional Components`([Stateless Functional Components](https://medium.com/@joshblack/stateless-components-in-react-0-14-f9798f8b992d#.xjqnbfx4e)) has no lifecycle hooks. And because all components need to write the life cycle hook `shouldComponentUpdate`, they are not used.
* In the `webpack.config.js` `devServer` attribute is written `host: '0.0.0.0'`, but you can be use in the development any other ip, not limited to localhost;
* Redux in the `store` not only connect to the method passed to `container`, you can jump out of the component, in other documents out to do flow control (dispatch), the source code:[/src/control/states.js](https://github.com/chvin/react-tetris/blob/master/src/control/states.js)
* Dealing with persistence in React + Redux is very convenient, as long as the redux state of storage, reducers do read in each of the initialization time.
* By configuring `.eslintrc.js` and `webpack.config.js`, the `ESLint` test is integrated in the project. Using ESLint allows coding to be written to specifications, effectively controlling code quality. Code that does not conform to the specifications can be found through the IDE and the console at development time (or build time). reference:[Airbnb React/JSX Style Guide](https://github.com/airbnb/javascript/tree/master/react)
----
## 6. Summary
* As a React hand application, in the realization of the process found a small "box" or a lot of details can be optimized and polished, this time is a test of a front-end engineers and the skill of the time carefully.
* Optimization of the direction of both React itself, such as what state is stored by the Redux, which states to the state of the component like; and out of the framework of the product can have a lot of features to play, in order to meet your needs, these will be natural propulsion technology development of.
* An application from scratch, the function slowly accumulate bit by bit, it will build into a high-rise, do not fear it difficult to have the idea to knock on it. ^_^
----
## 7. Flowchart
![Flowchart](https://img.alicdn.com/tfs/TB1B6ODRXXXXXXHaFXXXXXXXXXX-1920-1080.png)
----
## 8. Development
### Install
```
npm install
```
### Run
```
npm start
```
The browser will go to [http://127.0.0.1:8080/](http://127.0.0.1:8080/)
### multi-language
In the [i18n.json](https://github.com/chvin/react-tetris/blob/master/i18n.json) is the configuration for the multi-language environment. You can change the language by passing the url parameter `lan` like this: `https://chvin.github.io/react-tetris/?lan=en`
### Build
```
npm run build
```
Will build the application in the build folder.
因为 它太大了无法显示 source diff 。你可以改为 查看blob
此差异已折叠。
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}
/*# sourceMappingURL=css-1.0.1.css.map*/
\ No newline at end of file
{"version":3,"sources":[],"names":[],"mappings":"","file":"css-1.0.1.css","sourceRoot":""}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="renderer" content="webkit">
<meta name="description" content="使用React、Redux、Immutable制作的俄罗斯方块" />
<meta name="keywords" content="俄罗斯方块,Tetris,React,Redux,Immuatble,JavaScript">
<meta name="format-detection" content="telephone=no"/>
<script>;(function(){var w=parseInt(window.screen.width),s=w/640,u=navigator.userAgent.toLowerCase(),m='<meta name="viewport" content="width=640,';if(/android (\d+\.\d+)/.test(u)){if(parseFloat(RegExp.$1)>2.3)m+='minimum-scale='+s+',maximum-scale='+s+',';}else{m+='user-scalable=no,';}m+='target-densitydpi=device-dpi">';document.write(m);}());</script>
<meta charset="UTF-8">
<title>俄罗斯方块</title>
<link href="./loader.css" rel="stylesheet" />
<link href="css-1.0.1.css" rel="stylesheet"></head>
<body>
<div id="root">
<div class="load">
<div class="loader">加载中...</div>
</div>
</div>
<script type="text/javascript" src="app-1.0.1.js"></script></body>
</html>
\ No newline at end of file
body{
background: #009688;
padding: 0;
margin: 0;
}
.load{
width:240px;
height:240px;
position:absolute;
top:50%;
left:50%;
margin:-120px 0 0 -120px;
color:#efcc19;
-webkit-animation:fadeIn 2s infinite ease-in-out;
animation:fadeIn 2s infinite ease-in-out;
-webkit-animation-delay:2s;
animation-delay:2s;
opacity:0;
}
.load .loader,.load .loader:before,.load .loader:after{
background:#efcc19;
-webkit-animation:load 1s infinite ease-in-out;
animation:load 1s infinite ease-in-out;
width:1em;
height:4em
}
.load .loader:before,.load .loader:after{
position:absolute;
top:0;
content:''
}
.load .loader:before{
left:-1.5em;
-webkit-animation-delay:-0.32s;
animation-delay:-0.32s
}
.load .loader{
text-indent:-9999em;
margin:8em auto;
position:relative;
font-size:11px;
-webkit-animation-delay:-0.16s;
animation-delay:-0.16s
}
.load .loader:after{
left:1.5em
}
@-webkit-keyframes load{
0%,80%,100%{
box-shadow:0 0 #efcc19;
height:4em
}
40%{
box-shadow:0 -2em #efcc19;height:5em
}
}
@keyframes load{
0%,80%,100%{
box-shadow:0 0 #efcc19;
height:4em
}
40%{
box-shadow:0 -2em #efcc19;
height:5em
}
}
@-webkit-keyframes fadeIn{
0%{
opacity:0;
}
100%{
opacity:1;
}
}
@keyframes fadeIn{
0%{
opacity:0;
}
100%{
opacity:1;
}
}
\ No newline at end of file
文件已添加
{
"lan": ["cn", "en", "fr", "fa"],
"default": "cn",
"data": {
"title": {
"cn": "俄罗斯方块",
"en": "T E T R I S",
"fr": "T E T R I S",
"fa": "خانه سازی"
},
"github": {
"cn": "GitHub",
"en": "GitHub",
"fr": "GitHub",
"fa": "گیت‌هاب"
},
"linkTitle": {
"cn": "查看源代码",
"en": "View data source",
"fr": "Afficher la source des données",
"fa": "مشاهده سورس پروژه"
},
"QRCode":{
"cn": "二维码",
"en": "QR code",
"fr": "QR code",
"fa": "کیوآر کد"
},
"titleCenter": {
"cn": "俄罗斯方块<br />TETRIS",
"en": "TETRIS",
"fr": "TETRIS",
"fa": "خانه سازی"
},
"point": {
"cn": "得分",
"en": "Point",
"fr": "Score",
"fa": "امتیاز"
},
"highestScore": {
"cn": "最高分",
"en": "Max",
"fr": "Max",
"fa": "حداکثر"
},
"lastRound": {
"cn": "上轮得分",
"en": "Last Round",
"fr": "Dernier Tour",
"fa": "آخرین دور"
},
"cleans": {
"cn": "消除行",
"en": "Cleans",
"fr": "Lignes",
"fa": "پاک کرد"
},
"level": {
"cn": "级别",
"en": "Level",
"fr": "Difficulté",
"fa": "سطح"
},
"startLine": {
"cn": "起始行",
"en": "Start Line",
"fr": "Ligne Départ",
"fa": "خط شروع"
},
"next": {
"cn": "下一个",
"en": "Next",
"fr": "Prochain",
"fa": "بعدی"
},
"pause": {
"cn": "暂停",
"en": "Pause",
"fr": "Pause",
"fa": "مکث"
},
"sound": {
"cn": "音效",
"en": "Sound",
"fr": "Sonore",
"fa": "صدا"
},
"reset": {
"cn": "重玩",
"en": "Reset",
"fr": "Réinitialiser",
"fa": "ریست"
},
"rotation": {
"cn": "旋转",
"en": "Rotation",
"fr": "Rotation",
"fa": "چرخش"
},
"left": {
"cn": "左移",
"en": "Left",
"fr": "Gauche",
"fa": "چپ"
},
"right": {
"cn": "右移",
"en": "Right",
"fr": "Droite",
"fa": "راست"
},
"down": {
"cn": "下移",
"en": "Down",
"fr": "Bas",
"fa": "پایین"
},
"drop": {
"cn": "掉落",
"en": "Drop",
"fr": "Tomber",
"fa": "سقوط"
}
}
}
{
"name": "react-tetris",
"version": "1.0.1",
"description": "使用React、Redux、Immutable编写「俄罗斯方块」。Use Tetact, Redux, Immutable to coding \"Tetris\".",
"scripts": {
"start": "webpack-dev-server --progress",
"build": "rm -rf ./docs/* && webpack --config ./webpack.production.config.js --progress && ls ./docs"
},
"repository": {
"type": "git",
"url": "git+https://github.com/chvin/react-tetris.git"
},
"keywords": [
"Tetris",
"React",
"Redux",
"Immutable",
"俄罗斯方块"
],
"author": "Chvin",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/chvin/react-tetris/issues"
},
"homepage": "https://github.com/chvin/react-tetris#readme",
"devDependencies": {
"autoprefixer": "^6.7.2",
"babel-core": "^6.13.2",
"babel-loader": "^6.2.4",
"babel-plugin-react-transform": "^2.0.2",
"babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.16.0",
"copy-webpack-plugin": "^3.0.1",
"css-loader": "^0.23.1",
"eslint": "^3.3.1",
"eslint-config-airbnb": "^10.0.1",
"eslint-loader": "^1.6.1",
"eslint-plugin-import": "^1.13.0",
"eslint-plugin-jsx-a11y": "^2.1.0",
"eslint-plugin-react": "^6.1.1",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"html-webpack-plugin": "^2.22.0",
"json-loader": "^0.5.4",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"open-browser-webpack-plugin": "0.0.3",
"postcss": "^5.2.12",
"postcss-loader": "^1.2.2",
"precss": "^1.4.0",
"react-transform-hmr": "^1.0.4",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"
},
"dependencies": {
"classnames": "^2.2.5",
"immutable": "^3.8.1",
"prop-types": "^15.5.10",
"qrcode": "^1.2.0",
"react": "^15.3.0",
"react-dom": "^15.3.0",
"react-redux": "^4.4.5",
"redux": "^3.5.2",
"redux-immutable": "^3.0.8"
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="renderer" content="webkit">
<meta name="description" content="使用React、Redux、Immutable制作的俄罗斯方块" />
<meta name="keywords" content="俄罗斯方块,Tetris,React,Redux,Immuatble,JavaScript">
<meta name="format-detection" content="telephone=no"/>
<script>;(function(){var w=parseInt(window.screen.width),s=w/640,u=navigator.userAgent.toLowerCase(),m='<meta name="viewport" content="width=640,';if(/android (\d+\.\d+)/.test(u)){if(parseFloat(RegExp.$1)>2.3)m+='minimum-scale='+s+',maximum-scale='+s+',';}else{m+='user-scalable=no,';}m+='target-densitydpi=device-dpi">';document.write(m);}());</script>
<meta charset="UTF-8">
<title>俄罗斯方块</title>
<link href="./loader.css" rel="stylesheet" />
<link href="./css.css" rel="stylesheet">
</head>
<body>
<div id="root">
<div class="load">
<div class="loader">加载中...</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="renderer" content="webkit">
<meta name="description" content="使用React、Redux、Immutable制作的俄罗斯方块" />
<meta name="keywords" content="俄罗斯方块,Tetris,React,Redux,Immuatble,JavaScript">
<meta name="format-detection" content="telephone=no"/>
<script>;(function(){var w=parseInt(window.screen.width),s=w/640,u=navigator.userAgent.toLowerCase(),m='<meta name="viewport" content="width=640,';if(/android (\d+\.\d+)/.test(u)){if(parseFloat(RegExp.$1)>2.3)m+='minimum-scale='+s+',maximum-scale='+s+',';}else{m+='user-scalable=no,';}m+='target-densitydpi=device-dpi">';document.write(m);}());</script>
<meta charset="UTF-8">
<title>俄罗斯方块</title>
<link href="./loader.css" rel="stylesheet" />
</head>
<body>
<div id="root">
<div class="load">
<div class="loader">加载中...</div>
</div>
</div>
</body>
</html>
\ No newline at end of file
import { getNextType } from '../unit';
import * as reducerType from '../unit/reducerType';
import Block from '../unit/block';
import keyboard from './keyboard';
function nextBlock(next = getNextType()) {
return {
type: reducerType.NEXT_BLOCK,
data: next,
};
}
function moveBlock(option) {
return {
type: reducerType.MOVE_BLOCK,
data: option.reset === true ? null : new Block(option),
};
}
function speedStart(n) {
return {
type: reducerType.SPEED_START,
data: n,
};
}
function speedRun(n) {
return {
type: reducerType.SPEED_RUN,
data: n,
};
}
function startLines(n) {
return {
type: reducerType.START_LINES,
data: n,
};
}
function matrix(data) {
return {
type: reducerType.MATRIX,
data,
};
}
function lock(data) {
return {
type: reducerType.LOCK,
data,
};
}
function clearLines(data) {
return {
type: reducerType.CLEAR_LINES,
data,
};
}
function points(data) {
return {
type: reducerType.POINTS,
data,
};
}
function max(data) {
return {
type: reducerType.MAX,
data,
};
}
function reset(data) {
return {
type: reducerType.RESET,
data,
};
}
function drop(data) {
return {
type: reducerType.DROP,
data,
};
}
function pause(data) {
return {
type: reducerType.PAUSE,
data,
};
}
function music(data) {
return {
type: reducerType.MUSIC,
data,
};
}
function focus(data) {
return {
type: reducerType.FOCUS,
data,
};
}
export default {
nextBlock,
moveBlock,
speedStart,
speedRun,
startLines,
matrix,
lock,
clearLines,
points,
reset,
max,
drop,
pause,
keyboard,
music,
focus,
};
import * as reducerType from '../unit/reducerType';
function drop(data) {
return {
type: reducerType.KEY_DROP,
data,
};
}
function down(data) {
return {
type: reducerType.KEY_DOWN,
data,
};
}
function left(data) {
return {
type: reducerType.KEY_LEFT,
data,
};
}
function right(data) {
return {
type: reducerType.KEY_RIGHT,
data,
};
}
function rotate(data) {
return {
type: reducerType.KEY_ROTATE,
data,
};
}
function reset(data) {
return {
type: reducerType.KEY_RESET,
data,
};
}
function music(data) {
return {
type: reducerType.KEY_MUSIC,
data,
};
}
function pause(data) {
return {
type: reducerType.KEY_PAUSE,
data,
};
}
export default {
drop,
down,
left,
right,
rotate,
reset,
music,
pause,
};
import React from 'react';
import cn from 'classnames';
import { i18n, lan } from '../../unit/const';
import style from './index.less';
export default class Decorate extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
return (
<div className={style.decorate}>
<div className={style.topBorder}>
<span className={cn(['l', style.mr])} style={{ width: 40 }} />
<span className={cn(['l', style.mr])} />
<span className={cn(['l', style.mr])} />
<span className={cn(['l', style.mr])} />
<span className={cn(['l', style.mr])} />
<span className={cn(['r', style.ml])} style={{ width: 40 }} />
<span className={cn(['r', style.ml])} />
<span className={cn(['r', style.ml])} />
<span className={cn(['r', style.ml])} />
<span className={cn(['r', style.ml])} />
</div>
<h1>{i18n.title[lan]}</h1>
<div className={style.view}>
<b className="c" />
<div className="clear" />
<b className="c" />
<b className="c" />
<div className="clear" />
<em />
<b className="c" />
<p />
<em />
<b className="c" />
<div className="clear" />
<b className="c" />
<b className="c" />
<div className="clear" />
<em />
<b className="c" />
<p />
<b className="c" />
<b className="c" />
<b className="c" />
<b className="c" />
<p />
<b className="c" />
<div className="clear" />
<b className="c" />
<b className="c" />
<div className="clear" />
<b className="c" />
<p />
<b className="c" />
<b className="c" />
<div className="clear" />
<b className="c" />
<div className="clear" />
<b className="c" />
<p />
<em />
<b className="c" />
<div className="clear" />
<em />
<b className="c" />
<div className="clear" />
<em />
<b className="c" />
<div className="clear" />
<em />
<b className="c" />
</div>
<div className={cn([style.view, style.l])}>
<em />
<b className="c" />
<div className="clear" />
<b className="c" />
<b className="c" />
<div className="clear" />
<b className="c" />
<p />
<b className="c" />
<div className="clear" />
<b className="c" />
<b className="c" />
<div className="clear" />
<b className="c" />
<p />
<b className="c" />
<b className="c" />
<b className="c" />
<b className="c" />
<p />
<em />
<b className="c" />
<div className="clear" />
<b className="c" />
<b className="c" />
<div className="clear" />
<em />
<b className="c" />
<p />
<b className="c" />
<b className="c" />
<div className="clear" />
<em />
<b className="c" />
<div className="clear" />
<em />
<b className="c" />
<p />
<b className="c" />
<div className="clear" />
<b className="c" />
<div className="clear" />
<b className="c" />
<div className="clear" />
<b className="c" />
</div>
</div>
);
}
}
.decorate{
h1{
position: absolute;
width: 100%;
text-align: center;
font-weight: normal;
top: -12px;
left: 0;
margin: 0;
padding: 0;
font-size: 30px;
}
.topBorder{
position:absolute;
height:10px;
width:100%;
position:absolute;
top:0px;
left:0px;
overflow:hidden;
span{
display:block;
width:10px;
height:10px;
overflow:hidden;
background:#000;
&.mr{
margin-right:10px;
}
&.ml{
margin-left:10px;
}
}
}
.view{
position: absolute;
right: -70px;
top: 20px;
width: 44px;
em {
display: block;
width: 22px;
height: 22px;
overflow: hidden;
float: left;
}
p {
height: 22px;
clear: both;
}
&.l{
right: auto;
left: -70px;
}
}
}
\ No newline at end of file
import React from 'react';
import QRCode from 'qrcode';
import style from './index.less';
import { transform, i18n, lan } from '../../unit/const';
import { isMobile } from '../../unit';
export default class Guide extends React.Component {
constructor() {
super();
this.state = {
isMobile: isMobile(),
QRCode: '',
};
}
componentWillMount() {
if (this.state.isMobile) {
return;
}
QRCode.toDataURL(location.href, { margin: 1 })
.then(dataUrl => this.setState({ QRCode: dataUrl }));
}
shouldComponentUpdate(state) {
if (state.QRCode === this.state.QRCode) {
return false;
}
return true;
}
render() {
if (this.state.isMobile) {
return (
null
);
}
return (
<div style={{ display: this.state.isMobile ? 'none' : 'block' }}>
<div className={`${style.guide} ${style.right}`}>
<div className={style.up}>
<em style={{ [transform]: 'translate(0,-3px) scale(1,2)' }} />
</div>
<div className={style.left}>
<em style={{ [transform]: 'translate(-7px,3px) rotate(-90deg) scale(1,2)' }} />
</div>
<div className={style.down}>
<em style={{ [transform]: 'translate(0,9px) rotate(180deg) scale(1,2)' }} /></div>
<div className={style.right}>
<em style={{ [transform]: 'translate(7px,3px)rotate(90deg) scale(1,2)' }} />
</div>
</div>
<div className={`${style.guide} ${style.left}`}>
<p>
<a href="https://github.com/chvin/react-tetris" rel="noopener noreferrer" target="_blank" title={i18n.linkTitle[lan]}>{`${i18n.github[lan]}:`}</a><br />
<iframe
src="https://ghbtns.com/github-btn.html?user=chvin&repo=react-tetris&type=star&count=true"
frameBorder="0"
scrolling="0"
width="170px"
height="20px"
style={{ [transform]: 'scale(1.68)', [`${transform}Origin`]: 'center left' }}
/>
<br />
<iframe
src="https://ghbtns.com/github-btn.html?user=chvin&repo=react-tetris&type=fork&count=true"
frameBorder="0"
scrolling="0"
width="170px"
height="20px"
style={{ [transform]: 'scale(1.68)', [`${transform}Origin`]: 'center left' }}
/>
</p>
<div className={style.space}>SPACE</div>
</div>
{ this.state.QRCode !== '' ? (
<div className={`${style.guide} ${style.qr}`}>
<img
src={this.state.QRCode}
alt={i18n.QRCode[lan]}
/>
</div>
) : null }
</div>
);
}
}
.background(@from, @to){
background: (@from + @to)/2;
background: -webkit-gradient(linear, left top, left bottom, from(@from), to(@to));
background: -moz-linear-gradient(top, @from, @from);
// IE9使用filter背景渐变, 但因为使用了borader-radius, 所以不兼容, IE9使用中和的背景色即可
//filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@{from}', endColorstr='@{to}');
}
.guide {
position: absolute;
left: 115%;
right: 115%;
text-align:center;
line-height: 1;
white-space: nowrap;
&.right{
right: auto;
bottom: 5%;
}
&.left{
left: auto;
bottom: 5%;
}
p{
text-align: left;
margin-bottom: 350px;
opacity: .5;
iframe{
margin-top: 20px;
}
}
a{
color:#005850;
font-size:30px;
position:relative;
z-index:1;
cursor: alias;
text-decoration: none;
}
&.qr{
left: auto;
top: 5%;
width: 250px;
height:250px;
opacity: .5;
text-align: right;
cursor: none;
&:hover{
img{
width: 100%;
height: 100%;
}
}
img{
width: 38px;
height: 38px;
}
}
&>div{
width: 100px;
height: 54px;
background: /*#404040*/#364d4b;
display:inline-block;
border-radius: 4px;
position: relative;
color: #acc3c1;
font-size: 16px;
em {
display: block;
width: 0;
height: 0;
border: 6px solid;
border-color: transparent transparent /*#ccc*/#acc3c1;
position: absolute;
top: 50%;
left: 50%;
margin: -9px 0 0 -6px;
}
&:before, &:after{
content: '';
display: block;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius:4px;
box-shadow: 0 5px 10px rgba(255, 255, 255, .15) inset;
}
&:before{
box-shadow: 0 -5px 10px rgba(0, 0, 0, .15) inset;
}
}
.up{
height: 60px;
display:block;
margin:0 auto 2px;
}
.down{
margin:0 10px;
}
.space{
left: auto;
bottom: 5%;
height: 80px;
width: 400px;
line-height: 80px;
letter-spacing: 2px;
}
}
\ No newline at end of file
import React from 'react';
import cn from 'classnames';
import propTypes from 'prop-types';
import style from './index.less';
import { transform } from '../../../unit/const';
export default class Button extends React.Component {
shouldComponentUpdate(nextProps) {
return nextProps.active !== this.props.active;
}
render() {
const {
active, color, size, top, left, label, position, arrow,
} = this.props;
return (
<div
className={cn({ [style.button]: true, [style[color]]: true, [style[size]]: true })}
style={{ top, left }}
>
<i
className={cn({ [style.active]: active })}
ref={(c) => { this.dom = c; }}
/>
{ size === 's1' && <em
style={{
[transform]: `${arrow} scale(1,2)`,
}}
/> }
<span className={cn({ [style.position]: position })}>{label}</span>
</div>
);
}
}
Button.propTypes = {
color: propTypes.string.isRequired,
size: propTypes.string.isRequired,
top: propTypes.number.isRequired,
left: propTypes.number.isRequired,
label: propTypes.string.isRequired,
position: propTypes.bool,
arrow: propTypes.string,
active: propTypes.bool.isRequired,
};
.background(@from, @to){
background: (@from + @to)/2;
background: -webkit-gradient(linear, left top, left bottom, from(@from), to(@to));
background: -moz-linear-gradient(top, @from, @from);
// IE9使用filter背景渐变, 但因为使用了borader-radius, 所以不兼容, IE9使用中和的背景色即可
//filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='@{from}', endColorstr='@{to}');
}
.button{
position: absolute;
text-align: center;
color: #111;
position: absolute;
white-space: nowrap;
line-height: 1.6;
&.s2{
font-size: 16px;
}
span.position{
position: absolute;
top: 5px;
left: 102px;
}
i{
display: block;
position: relative;
border: 1px solid #000;
border-radius: 50%;
&:before, &:after{
content: '';
display: block;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius:50%;
box-shadow: 0 5px 10px rgba(255, 255, 255, .8) inset;
}
&:after{
box-shadow: 0 -5px 10px rgba(0, 0, 0, .8) inset;
}
&.active{
&:before{
box-shadow: 0 -3px 6px rgba(255, 255, 255, .6) inset;
}
&:after{
box-shadow: 0 5px 5px rgba(0, 0, 0, .6) inset;
}
}
box-shadow: 0 3px 3px rgba(0, 0, 0, .2);
}
&.blue i{
.background(#6e77ef, #4652f3);
}
&.green i{
.background(#4bc441, #0ec400);
}
&.red i{
.background(#dc3333, #de0000);
}
&.s0 i{
width: 160px;
height: 160px;
}
&.s1 i{
width: 100px;
height: 100px;
}
&.s2 i{
width: 52px;
height: 52px;
&:before, &:after{
box-shadow: 0px 3px 6px rgba(255, 255, 255, .8) inset;
}
&:after{
box-shadow: 0px -3px 6px rgba(0, 0, 0, .8) inset;
}
&.active{
&:before{
box-shadow: 0px -1px 2px rgba(255, 255, 255, .6) inset;
}
&:after{
box-shadow: 0px 3px 3px rgba(0, 0, 0, .7) inset;
}
}
box-shadow: 1px 1px 1px rgba(0, 0, 0, .2);
}
&.s1{
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;
}
}
}
\ No newline at end of file
import React from 'react';
import Immutable from 'immutable';
import propTypes from 'prop-types';
import style from './index.less';
import Button from './button';
import store from '../../store';
import todo from '../../control/todo';
import { i18n, lan } from '../../unit/const';
export default class Keyboard extends React.Component {
componentDidMount() {
const touchEventCatch = {}; // 对于手机操作, 触发了touchstart, 将作出记录, 不再触发后面的mouse事件
// 在鼠标触发mousedown时, 移除元素时可以不触发mouseup, 这里做一个兼容, 以mouseout模拟mouseup
const mouseDownEventCatch = {};
document.addEventListener('touchstart', (e) => {
if (e.preventDefault) {
e.preventDefault();
}
}, true);
// 解决issue: https://github.com/chvin/react-tetris/issues/24
document.addEventListener('touchend', (e) => {
if (e.preventDefault) {
e.preventDefault();
}
}, true);
// 阻止双指放大
document.addEventListener('gesturestart', (e) => {
if (e.preventDefault) {
event.preventDefault();
}
});
document.addEventListener('mousedown', (e) => {
if (e.preventDefault) {
e.preventDefault();
}
}, true);
Object.keys(todo).forEach((key) => {
this[`dom_${key}`].dom.addEventListener('mousedown', () => {
if (touchEventCatch[key] === true) {
return;
}
todo[key].down(store);
mouseDownEventCatch[key] = true;
}, true);
this[`dom_${key}`].dom.addEventListener('mouseup', () => {
if (touchEventCatch[key] === true) {
touchEventCatch[key] = false;
return;
}
todo[key].up(store);
mouseDownEventCatch[key] = false;
}, true);
this[`dom_${key}`].dom.addEventListener('mouseout', () => {
if (mouseDownEventCatch[key] === true) {
todo[key].up(store);
}
}, true);
this[`dom_${key}`].dom.addEventListener('touchstart', () => {
touchEventCatch[key] = true;
todo[key].down(store);
}, true);
this[`dom_${key}`].dom.addEventListener('touchend', () => {
todo[key].up(store);
}, true);
});
}
shouldComponentUpdate({ keyboard, filling }) {
return !Immutable.is(keyboard, this.props.keyboard) || filling !== this.props.filling;
}
render() {
const keyboard = this.props.keyboard;
return (
<div
className={style.keyboard}
style={{
marginTop: 20 + this.props.filling,
}}
>
<Button
color="blue"
size="s1"
top={0}
left={374}
label={i18n.rotation[lan]}
arrow="translate(0, 63px)"
position
active={keyboard.get('rotate')}
ref={(c) => { this.dom_rotate = c; }}
/>
<Button
color="blue"
size="s1"
top={180}
left={374}
label={i18n.down[lan]}
arrow="translate(0,-71px) rotate(180deg)"
active={keyboard.get('down')}
ref={(c) => { this.dom_down = c; }}
/>
<Button
color="blue"
size="s1"
top={90}
left={284}
label={i18n.left[lan]}
arrow="translate(60px, -12px) rotate(270deg)"
active={keyboard.get('left')}
ref={(c) => { this.dom_left = c; }}
/>
<Button
color="blue"
size="s1"
top={90}
left={464}
label={i18n.right[lan]}
arrow="translate(-60px, -12px) rotate(90deg)"
active={keyboard.get('right')}
ref={(c) => { this.dom_right = c; }}
/>
<Button
color="blue"
size="s0"
top={100}
left={52}
label={`${i18n.drop[lan]} (SPACE)`}
active={keyboard.get('drop')}
ref={(c) => { this.dom_space = c; }}
/>
<Button
color="red"
size="s2"
top={0}
left={196}
label={`${i18n.reset[lan]}(R)`}
active={keyboard.get('reset')}
ref={(c) => { this.dom_r = c; }}
/>
<Button
color="green"
size="s2"
top={0}
left={106}
label={`${i18n.sound[lan]}(S)`}
active={keyboard.get('music')}
ref={(c) => { this.dom_s = c; }}
/>
<Button
color="green"
size="s2"
top={0}
left={16}
label={`${i18n.pause[lan]}(P)`}
active={keyboard.get('pause')}
ref={(c) => { this.dom_p = c; }}
/>
</div>
);
}
}
Keyboard.propTypes = {
filling: propTypes.number.isRequired,
keyboard: propTypes.object.isRequired,
};
.keyboard{
width: 580px;
height: 330px;
margin: 20px auto 0;
position:relative;
}
\ No newline at end of file
import React from 'react';
import cn from 'classnames';
import propTypes from 'prop-types';
import style from './index.less';
import { i18n, lan } from '../../unit/const';
export default class Logo extends React.Component {
constructor() {
super();
this.state = {
style: style.r1,
display: 'none',
};
}
componentWillMount() {
this.animate(this.props);
}
componentWillReceiveProps(nextProps) {
if ( // 只有在游戏进入开始, 或结束时 触发改变
(
[this.props.cur, nextProps.cur].indexOf(false) !== -1 &&
(this.props.cur !== nextProps.cur)
) ||
(this.props.reset !== nextProps.reset)
) {
this.animate(nextProps);
}
}
shouldComponentUpdate({ cur, reset }) {
return cur !== this.props.cur || reset !== this.props.reset || !cur;
}
animate({ cur, reset }) {
clearTimeout(Logo.timeout);
this.setState({
style: style.r1,
display: 'none',
});
if (cur || reset) {
this.setState({ display: 'none' });
return;
}
let m = 'r'; // 方向
let count = 0;
const set = (func, delay) => {
if (!func) {
return;
}
Logo.timeout = setTimeout(func, delay);
};
const show = (func) => { // 显示
set(() => {
this.setState({
display: 'block',
});
if (func) {
func();
}
}, 150);
};
const hide = (func) => { // 隐藏
set(() => {
this.setState({
display: 'none',
});
if (func) {
func();
}
}, 150);
};
const eyes = (func, delay1, delay2) => { // 龙在眨眼睛
set(() => {
this.setState({ style: style[m + 2] });
set(() => {
this.setState({ style: style[m + 1] });
if (func) {
func();
}
}, delay2);
}, delay1);
};
const run = (func) => { // 开始跑步啦!
set(() => {
this.setState({ style: style[m + 4] });
set(() => {
this.setState({ style: style[m + 3] });
count++;
if (count === 10 || count === 20 || count === 30) {
m = m === 'r' ? 'l' : 'r';
}
if (count < 40) {
run(func);
return;
}
this.setState({ style: style[m + 1] });
if (func) {
set(func, 4000);
}
}, 100);
}, 100);
};
const dra = () => {
count = 0;
eyes(() => {
eyes(() => {
eyes(() => {
this.setState({ style: style[m + 2] });
run(dra);
}, 150, 150);
}, 150, 150);
}, 1000, 1500);
};
show(() => { // 忽隐忽现
hide(() => {
show(() => {
hide(() => {
show(() => {
dra(); // 开始运动
});
});
});
});
});
}
render() {
if (this.props.cur) {
return null;
}
return (
<div className={style.logo} style={{ display: this.state.display }}>
<div className={cn({ bg: true, [style.dragon]: true, [this.state.style]: true })} />
<p dangerouslySetInnerHTML={{ __html: i18n.titleCenter[lan] }} />
</div>
);
}
}
Logo.propTypes = {
cur: propTypes.bool,
reset: propTypes.bool.isRequired,
};
Logo.statics = {
timeout: null,
};
.logo {
width: 224px;
height: 200px;
position: absolute;
top: 100px;
left: 12px;
text-align: center;
overflow: hidden;
p {
position: absolute;
width: 100%;
line-height: 1.4;
top: 100px;
left: 0;
font-family: initial;
letter-spacing: 6px;
text-shadow: 1px 1px 1px rgba(255, 255, 255,.35);
}
.dragon {
width: 80px;
height: 86px;
margin: 0 auto;
background-position: 0 -100px;
&.r1,&.l1 {
background-position: 0 -100px;
}
&.r2,&.l2 {
background-position: -100px -100px;
}
&.r3,&.l3 {
background-position: -200px -100px;
}
&.r4,&.l4 {
background-position: -300px -100px;
}
&.l1,&.l2,&.l3,&.l4{
transform: scale(-1, 1);
-webkit-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
}
}
}
\ No newline at end of file
import React from 'react';
import immutable, { List } from 'immutable';
import classnames from 'classnames';
import propTypes from 'prop-types';
import style from './index.less';
import { isClear } from '../../unit/';
import { fillLine, blankLine } from '../../unit/const';
import states from '../../control/states';
const t = setTimeout;
export default class Matrix extends React.Component {
constructor() {
super();
this.state = {
clearLines: false,
animateColor: 2,
isOver: false,
overState: null,
};
}
componentWillReceiveProps(nextProps = {}) {
const clears = isClear(nextProps.matrix);
const overs = nextProps.reset;
this.setState({
clearLines: clears,
isOver: overs,
});
if (clears && !this.state.clearLines) {
this.clearAnimate(clears);
}
if (!clears && overs && !this.state.isOver) {
this.over(nextProps);
}
}
shouldComponentUpdate(nextProps = {}) { // 使用Immutable 比较两个List 是否相等
const props = this.props;
return !(
immutable.is(nextProps.matrix, props.matrix) &&
immutable.is(
(nextProps.cur && nextProps.cur.shape),
(props.cur && props.cur.shape)
) &&
immutable.is(
(nextProps.cur && nextProps.cur.xy),
(props.cur && props.cur.xy)
)
) || this.state.clearLines
|| this.state.isOver;
}
getResult(props = this.props) {
const cur = props.cur;
const shape = cur && cur.shape;
const xy = cur && cur.xy;
let matrix = props.matrix;
const clearLines = this.state.clearLines;
if (clearLines) {
const animateColor = this.state.animateColor;
clearLines.forEach((index) => {
matrix = matrix.set(index, List([
animateColor,
animateColor,
animateColor,
animateColor,
animateColor,
animateColor,
animateColor,
animateColor,
animateColor,
animateColor,
]));
});
} else if (shape) {
shape.forEach((m, k1) => (
m.forEach((n, k2) => {
if (n && xy.get(0) + k1 >= 0) { // 竖坐标可以为负
let line = matrix.get(xy.get(0) + k1);
let color;
if (line.get(xy.get(1) + k2) === 1 && !clearLines) { // 矩阵与方块重合
color = 2;
} else {
color = 1;
}
line = line.set(xy.get(1) + k2, color);
matrix = matrix.set(xy.get(0) + k1, line);
}
})
));
}
return matrix;
}
clearAnimate() {
const anima = (callback) => {
t(() => {
this.setState({
animateColor: 0,
});
t(() => {
this.setState({
animateColor: 2,
});
if (typeof callback === 'function') {
callback();
}
}, 100);
}, 100);
};
anima(() => {
anima(() => {
anima(() => {
t(() => {
states.clearLines(this.props.matrix, this.state.clearLines);
}, 100);
});
});
});
}
over(nextProps) {
let overState = this.getResult(nextProps);
this.setState({
overState,
});
const exLine = (index) => {
if (index <= 19) {
overState = overState.set(19 - index, List(fillLine));
} else if (index >= 20 && index <= 39) {
overState = overState.set(index - 20, List(blankLine));
} else {
states.overEnd();
return;
}
this.setState({
overState,
});
};
for (let i = 0; i <= 40; i++) {
t(exLine.bind(null, i), 40 * (i + 1));
}
}
render() {
let matrix;
if (this.state.isOver) {
matrix = this.state.overState;
} else {
matrix = this.getResult();
}
return (
<div className={style.matrix}>{
matrix.map((p, k1) => (<p key={k1}>
{
p.map((e, k2) => <b
className={classnames({
c: e === 1,
d: e === 2,
})}
key={k2}
/>)
}
</p>))
}
</div>
);
}
}
Matrix.propTypes = {
matrix: propTypes.object.isRequired,
cur: propTypes.object,
reset: propTypes.bool.isRequired,
};
.matrix{
border:2px solid #000;
padding:3px 1px 1px 3px;
width:228px;
p{
width:220px;
height:22px;
}
}
\ No newline at end of file
import React from 'react';
import cn from 'classnames';
import propTypes from 'prop-types';
import style from './index.less';
export default class Music extends React.Component {
shouldComponentUpdate({ data }) {
return data !== this.props.data;
}
render() {
return (
<div
className={cn(
{
bg: true,
[style.music]: true,
[style.c]: !this.props.data,
}
)}
/>
);
}
}
Music.propTypes = {
data: propTypes.bool.isRequired,
};
.music{
width: 25px;
height: 21px;
background-position: -175px -75px;
position: absolute;
top: 2px;
left: -12px;
&.c{
background-position: -150px -75px;
}
}
\ No newline at end of file
import React from 'react';
import propTypes from 'prop-types';
import style from './index.less';
import { blockShape } from '../../unit/const';
const xy = { // 方块在下一个中的坐标
I: [1, 0],
L: [0, 0],
J: [0, 0],
Z: [0, 0],
S: [0, 0],
O: [0, 1],
T: [0, 0],
};
const empty = [
[0, 0, 0, 0],
[0, 0, 0, 0],
];
export default class Next extends React.Component {
constructor() {
super();
this.state = {
block: empty,
};
}
componentWillMount() {
this.build(this.props.data);
}
componentWillReceiveProps(nextProps) {
this.build(nextProps.data);
}
shouldComponentUpdate(nextProps) {
return nextProps.data !== this.props.data;
}
build(type) {
const shape = blockShape[type];
const block = empty.map(e => ([...e]));
shape.forEach((m, k1) => {
m.forEach((n, k2) => {
if (n) {
block[k1 + xy[type][0]][k2 + xy[type][1]] = 1;
}
});
});
this.setState({ block });
}
render() {
return (
<div className={style.next}>
{
this.state.block.map((arr, k1) => (
<div key={k1}>
{
arr.map((e, k2) => (
<b className={e ? 'c' : ''} key={k2} />
))
}
</div>
))
}
</div>
);
}
}
Next.propTypes = {
data: propTypes.string,
};
.next{
div{
height: 22px;
width: 88px;
float: right;
}
}
\ No newline at end of file
import React from 'react';
import cn from 'classnames';
import propTypes from 'prop-types';
import style from './index.less';
const render = (data) => (
<div className={style.number}>
{
data.map((e, k) => (
<span className={cn(['bg', style[`s_${e}`]])} key={k} />
))
}
</div>
);
const formate = (num) => (
num < 10 ? `0${num}`.split('') : `${num}`.split('')
);
export default class Number extends React.Component {
constructor() {
super();
this.state = {
time_count: false,
time: new Date(),
};
}
componentWillMount() {
if (!this.props.time) {
return;
}
const clock = () => {
const count = +Number.timeInterval;
Number.timeInterval = setTimeout(() => {
this.setState({
time: new Date(),
time_count: count, // 用来做 shouldComponentUpdate 优化
});
clock();
}, 1000);
};
clock();
}
shouldComponentUpdate({ number }) {
if (this.props.time) { // 右下角时钟
if (this.state.time_count !== Number.time_count) {
if (this.state.time_count !== false) {
Number.time_count = this.state.time_count; // 记录clock上一次的缓存
}
return true;
}
return false; // 经过判断这次的时间已经渲染, 返回false
}
return this.props.number !== number;
}
componentWillUnmount() {
if (!this.props.time) {
return;
}
clearTimeout(Number.timeInterval);
}
render() {
if (this.props.time) { // 右下角时钟
const now = this.state.time;
const hour = formate(now.getHours());
const min = formate(now.getMinutes());
const sec = now.getSeconds() % 2;
const t = hour.concat(sec ? 'd' : 'd_c', min);
return (render(t));
}
const num = `${this.props.number}`.split('');
for (let i = 0, len = this.props.length - num.length; i < len; i++) {
num.unshift('n');
}
return (render(num));
}
}
Number.statics = {
timeInterval: null,
time_count: null,
};
Number.propTypes = {
number: propTypes.number,
length: propTypes.number,
time: propTypes.bool,
};
Number.defaultProps = {
length: 6,
};
.number{
height:24px;
font-size:14px;
float:right;
span{
float:left;
width:14px;
height:24px;
}
.s_0{background-position:-75px -25px;}
.s_1{background-position:-89px -25px;}
.s_2{background-position:-103px -25px;}
.s_3{background-position:-117px -25px;}
.s_4{background-position:-131px -25px;}
.s_5{background-position:-145px -25px;}
.s_6{background-position:-159px -25px;}
.s_7{background-position:-173px -25px;}
.s_8{background-position:-187px -25px;}
.s_9{background-position:-201px -25px;}
.s_n{background-position:-215px -25px;}
.s_d{background-position:-243px -25px;}
.s_d_c{background-position:-229px -25px;}
}
\ No newline at end of file
import React from 'react';
import cn from 'classnames';
import propTypes from 'prop-types';
import style from './index.less';
export default class Pause extends React.Component {
constructor() {
super();
this.state = { // 控制显示状态
showPause: false,
};
}
componentDidMount() {
this.setShake(this.props.data);
}
componentWillReceiveProps({ data }) {
this.setShake(data);
}
shouldComponentUpdate({ data }) {
if (data) { // 如果暂停了, 不会有太多的dispatch, 考虑到闪烁效果, 直接返回true
return true;
}
return data !== this.props.data;
}
setShake(bool) { // 根据props显示闪烁或停止闪烁
if (bool && !Pause.timeout) {
Pause.timeout = setInterval(() => {
this.setState({
showPause: !this.state.showPause,
});
}, 250);
}
if (!bool && Pause.timeout) {
clearInterval(Pause.timeout);
this.setState({
showPause: false,
});
Pause.timeout = null;
}
}
render() {
return (
<div
className={cn(
{
bg: true,
[style.pause]: true,
[style.c]: this.state.showPause,
}
)}
/>
);
}
}
Pause.statics = {
timeout: null,
};
Pause.propTypes = {
data: propTypes.bool.isRequired,
};
Pause.defaultProps = {
data: false,
};
.pause {
width: 20px;
height: 18px;
background-position: -100px -75px;
position: absolute;
top: 3px;
left: 18px;
&.c {
background-position: -75px -75px;
}
}
\ No newline at end of file
import React from 'react';
import propTypes from 'prop-types';
import Number from '../number';
import { i18n, lan } from '../../unit/const';
const DF = i18n.point[lan];
const ZDF = i18n.highestScore[lan];
const SLDF = i18n.lastRound[lan];
export default class Point extends React.Component {
constructor() {
super();
this.state = {
label: '',
number: 0,
};
}
componentWillMount() {
this.onChange(this.props);
}
componentWillReceiveProps(nextProps) {
this.onChange(nextProps);
}
shouldComponentUpdate({ cur, point, max }) {
const props = this.props;
return cur !== props.cur || point !== props.point || max !== props.max || !props.cur;
}
onChange({ cur, point, max }) {
clearInterval(Point.timeout);
if (cur) { // 在游戏进行中
this.setState({
label: point >= max ? ZDF : DF,
number: point,
});
} else { // 游戏未开始
const toggle = () => { // 最高分与上轮得分交替出现
this.setState({
label: SLDF,
number: point,
});
Point.timeout = setTimeout(() => {
this.setState({
label: ZDF,
number: max,
});
Point.timeout = setTimeout(toggle, 3000);
}, 3000);
};
if (point !== 0) { // 如果为上轮没玩, 也不用提示了
toggle();
} else {
this.setState({
label: ZDF,
number: max,
});
}
}
}
render() {
return (
<div>
<p>{ this.state.label }</p>
<Number number={this.state.number} />
</div>
);
}
}
Point.statics = {
timeout: null,
};
Point.propTypes = {
cur: propTypes.bool,
max: propTypes.number.isRequired,
point: propTypes.number.isRequired,
};
import React from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import propTypes from 'prop-types';
import style from './index.less';
import Matrix from '../components/matrix';
import Decorate from '../components/decorate';
import Number from '../components/number';
import Next from '../components/next';
import Music from '../components/music';
import Pause from '../components/pause';
import Point from '../components/point';
import Logo from '../components/logo';
import Keyboard from '../components/keyboard';
import Guide from '../components/guide';
import { transform, lastRecord, speeds, i18n, lan } from '../unit/const';
import { visibilityChangeEvent, isFocus } from '../unit/';
import states from '../control/states';
class App extends React.Component {
constructor() {
super();
this.state = {
w: document.documentElement.clientWidth,
h: document.documentElement.clientHeight,
};
}
componentWillMount() {
window.addEventListener('resize', this.resize.bind(this), true);
}
componentDidMount() {
if (visibilityChangeEvent) { // 将页面的焦点变换写入store
document.addEventListener(visibilityChangeEvent, () => {
states.focus(isFocus());
}, false);
}
if (lastRecord) { // 读取记录
if (lastRecord.cur && !lastRecord.pause) { // 拿到上一次游戏的状态, 如果在游戏中且没有暂停, 游戏继续
const speedRun = this.props.speedRun;
let timeout = speeds[speedRun - 1] / 2; // 继续时, 给予当前下落速度一半的停留时间
// 停留时间不小于最快速的速度
timeout = speedRun < speeds[speeds.length - 1] ? speeds[speeds.length - 1] : speedRun;
states.auto(timeout);
}
if (!lastRecord.cur) {
states.overStart();
}
} else {
states.overStart();
}
}
resize() {
this.setState({
w: document.documentElement.clientWidth,
h: document.documentElement.clientHeight,
});
}
render() {
let filling = 0;
const size = (() => {
const w = this.state.w;
const h = this.state.h;
const ratio = h / w;
let scale;
let css = {};
if (ratio < 1.5) {
scale = h / 960;
} else {
scale = w / 640;
filling = (h - (960 * scale)) / scale / 3;
css = {
paddingTop: Math.floor(filling) + 42,
paddingBottom: Math.floor(filling),
marginTop: Math.floor(-480 - (filling * 1.5)),
};
}
css[transform] = `scale(${scale})`;
return css;
})();
return (
<div
className={style.app}
style={size}
>
<div className={classnames({ [style.rect]: true, [style.drop]: this.props.drop })}>
<Decorate />
<div className={style.screen}>
<div className={style.panel}>
<Matrix
matrix={this.props.matrix}
cur={this.props.cur}
reset={this.props.reset}
/>
<Logo cur={!!this.props.cur} reset={this.props.reset} />
<div className={style.state}>
<Point cur={!!this.props.cur} point={this.props.points} max={this.props.max} />
<p>{ this.props.cur ? i18n.cleans[lan] : i18n.startLine[lan] }</p>
<Number number={this.props.cur ? this.props.clearLines : this.props.startLines} />
<p>{i18n.level[lan]}</p>
<Number
number={this.props.cur ? this.props.speedRun : this.props.speedStart}
length={1}
/>
<p>{i18n.next[lan]}</p>
<Next data={this.props.next} />
<div className={style.bottom}>
<Music data={this.props.music} />
<Pause data={this.props.pause} />
<Number time />
</div>
</div>
</div>
</div>
</div>
<Keyboard filling={filling} keyboard={this.props.keyboard} />
<Guide />
</div>
);
}
}
App.propTypes = {
music: propTypes.bool.isRequired,
pause: propTypes.bool.isRequired,
matrix: propTypes.object.isRequired,
next: propTypes.string.isRequired,
cur: propTypes.object,
dispatch: propTypes.func.isRequired,
speedStart: propTypes.number.isRequired,
speedRun: propTypes.number.isRequired,
startLines: propTypes.number.isRequired,
clearLines: propTypes.number.isRequired,
points: propTypes.number.isRequired,
max: propTypes.number.isRequired,
reset: propTypes.bool.isRequired,
drop: propTypes.bool.isRequired,
keyboard: propTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
pause: state.get('pause'),
music: state.get('music'),
matrix: state.get('matrix'),
next: state.get('next'),
cur: state.get('cur'),
speedStart: state.get('speedStart'),
speedRun: state.get('speedRun'),
startLines: state.get('startLines'),
clearLines: state.get('clearLines'),
points: state.get('points'),
max: state.get('max'),
reset: state.get('reset'),
drop: state.get('drop'),
keyboard: state.get('keyboard'),
});
export default connect(mapStateToProps)(App);
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;
}
:global{
.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;
-khtml-user-select: none;
user-select: none;
}
.app{
width: 640px;
padding-top: 42px;
box-shadow: 0 0 10px #fff inset;
border-radius: 20px;
position: absolute;
top: 50%;
left: 50%;
margin: -480px 0 0 -320px;
background: #efcc19;
:global{
b {
display: block;
width: 20px;
height: 20px;
padding: 2px;
border: 2px solid #879372;
margin: 0 2px 2px 0;
float: left;
&:after {
content: '';
display: block;
width: 12px;
height: 12px;
background: #879372;
overflow: hidden;
}
&.c {
border-color: #000;
&:after {
background: #000;
}
}
&.d {
border-color: #560000;
&:after {
background: #560000;
}
}
}
}
}
.rect{
width: 480px;
padding: 45px 0 35px;
border: #000 solid;
border-width: 0 10px 10px;
margin: 0 auto;
position: relative;
&.drop{ -webkit-transform:translateY(5px);transform:translateY(5px); }
}
.screen{
width: 390px;
height: 478px;
border: solid 5px;
border-color: #987f0f #fae36c #fae36c #987f0f;
margin: 0 auto;
position: relative;
.panel{
width: 380px;
height: 468px;
margin: 0 auto;
background: #9ead86;
padding: 8px;
border: 2px solid #494536;
}
}
.state {
width: 108px;
position: absolute;
top: 0;
right: 15px;
p {
line-height: 47px;
height: 57px;
padding: 10px 0 0;
white-space: nowrap;
clear: both;
}
.bottom {
position: absolute;
width: 114px;
top: 426px;
left: 0;
}
}
\ No newline at end of file
.load{
@-webkit-keyframes loads{
0%,80%,100%{
box-shadow:0 0 #efcc19;
height:4em}
40%{
box-shadow:0 -2em #efcc19;
height:5em
}
}
@keyframes loads{
0%,80%,100%{
box-shadow:0 0 #efcc19;
height:4em
}
40%{
box-shadow:0 -2em #efcc19;
height:5em
}
}
width:240px;
height:240px;
float:left;
position:relative;
color:#fff;
text-align:center;
position:absolute;
top:50%;
left:50%;
margin:-120px 0 0 -120px;
p{
position:absolute;
bottom:0;
left:-25%;
width:150%;
white-space:nowrap;
display:none;
}
.loader{
&,&:before,&:after{
background:#efcc19;
-webkit-animation:loads 1s infinite ease-in-out;
animation:loads 1s infinite ease-in-out;
width:1em;
height:4em
}
&:before,&:after{
position:absolute;top:0;content:''
}
&:before{
left:-1.5em;
-webkit-animation-delay:-0.32s;
animation-delay:-0.32s
}
text-indent:-9999em;
margin:8em auto;
position:relative;
font-size:11px;
-webkit-animation-delay:-0.16s;
animation-delay:-0.16s;
&:after{
left:1.5em
}
}
}
import store from '../store';
import todo from './todo';
const keyboard = {
37: 'left',
38: 'rotate',
39: 'right',
40: 'down',
32: 'space',
83: 's',
82: 'r',
80: 'p',
};
let keydownActive;
const boardKeys = Object.keys(keyboard).map(e => parseInt(e, 10));
const keyDown = (e) => {
if (e.metaKey === true || boardKeys.indexOf(e.keyCode) === -1) {
return;
}
const type = keyboard[e.keyCode];
if (type === keydownActive) {
return;
}
keydownActive = type;
todo[type].down(store);
};
const keyUp = (e) => {
if (e.metaKey === true || boardKeys.indexOf(e.keyCode) === -1) {
return;
}
const type = keyboard[e.keyCode];
if (type === keydownActive) {
keydownActive = '';
}
todo[type].up(store);
};
document.addEventListener('keydown', keyDown, true);
document.addEventListener('keyup', keyUp, true);
import { List } from 'immutable';
import store from '../store';
import { want, isClear, isOver } from '../unit/';
import actions from '../actions';
import { speeds, blankLine, blankMatrix, clearPoints, eachLines } from '../unit/const';
import { music } from '../unit/music';
const getStartMatrix = (startLines) => { // 生成startLines
const getLine = (min, max) => { // 返回标亮个数在min~max之间一行方块, (包含边界)
const count = parseInt((((max - min) + 1) * Math.random()) + min, 10);
const line = [];
for (let i = 0; i < count; i++) { // 插入高亮
line.push(1);
}
for (let i = 0, len = 10 - count; i < len; i++) { // 在随机位置插入灰色
const index = parseInt(((line.length + 1) * Math.random()), 10);
line.splice(index, 0, 0);
}
return List(line);
};
let startMatrix = List([]);
for (let i = 0; i < startLines; i++) {
if (i <= 2) { // 0-3
startMatrix = startMatrix.push(getLine(5, 8));
} else if (i <= 6) { // 4-6
startMatrix = startMatrix.push(getLine(4, 9));
} else { // 7-9
startMatrix = startMatrix.push(getLine(3, 9));
}
}
for (let i = 0, len = 20 - startLines; i < len; i++) { // 插入上部分的灰色
startMatrix = startMatrix.unshift(List(blankLine));
}
return startMatrix;
};
const states = {
// 自动下落setTimeout变量
fallInterval: null,
// 游戏开始
start: () => {
if (music.start) {
music.start();
}
const state = store.getState();
states.dispatchPoints(0);
store.dispatch(actions.speedRun(state.get('speedStart')));
const startLines = state.get('startLines');
const startMatrix = getStartMatrix(startLines);
store.dispatch(actions.matrix(startMatrix));
store.dispatch(actions.moveBlock({ type: state.get('next') }));
store.dispatch(actions.nextBlock());
states.auto();
},
// 自动下落
auto: (timeout) => {
const out = (timeout < 0 ? 0 : timeout);
let state = store.getState();
let cur = state.get('cur');
const fall = () => {
state = store.getState();
cur = state.get('cur');
const next = cur.fall();
if (want(next, state.get('matrix'))) {
store.dispatch(actions.moveBlock(next));
states.fallInterval = setTimeout(fall, speeds[state.get('speedRun') - 1]);
} else {
let matrix = state.get('matrix');
const shape = cur && cur.shape;
const xy = cur && cur.xy;
shape.forEach((m, k1) => (
m.forEach((n, k2) => {
if (n && xy.get(0) + k1 >= 0) { // 竖坐标可以为负
let line = matrix.get(xy.get(0) + k1);
line = line.set(xy.get(1) + k2, 1);
matrix = matrix.set(xy.get(0) + k1, line);
}
})
));
states.nextAround(matrix);
}
};
clearTimeout(states.fallInterval);
states.fallInterval = setTimeout(fall,
out === undefined ? speeds[state.get('speedRun') - 1] : out);
},
// 一个方块结束, 触发下一个
nextAround: (matrix, stopDownTrigger) => {
clearTimeout(states.fallInterval);
store.dispatch(actions.lock(true));
store.dispatch(actions.matrix(matrix));
if (typeof stopDownTrigger === 'function') {
stopDownTrigger();
}
const addPoints = (store.getState().get('points') + 10) +
((store.getState().get('speedRun') - 1) * 2); // 速度越快, 得分越高
states.dispatchPoints(addPoints);
if (isClear(matrix)) {
if (music.clear) {
music.clear();
}
return;
}
if (isOver(matrix)) {
if (music.gameover) {
music.gameover();
}
states.overStart();
return;
}
setTimeout(() => {
store.dispatch(actions.lock(false));
store.dispatch(actions.moveBlock({ type: store.getState().get('next') }));
store.dispatch(actions.nextBlock());
states.auto();
}, 100);
},
// 页面焦点变换
focus: (isFocus) => {
store.dispatch(actions.focus(isFocus));
if (!isFocus) {
clearTimeout(states.fallInterval);
return;
}
const state = store.getState();
if (state.get('cur') && !state.get('reset') && !state.get('pause')) {
states.auto();
}
},
// 暂停
pause: (isPause) => {
store.dispatch(actions.pause(isPause));
if (isPause) {
clearTimeout(states.fallInterval);
return;
}
states.auto();
},
// 消除行
clearLines: (matrix, lines) => {
const state = store.getState();
let newMatrix = matrix;
lines.forEach(n => {
newMatrix = newMatrix.splice(n, 1);
newMatrix = newMatrix.unshift(List(blankLine));
});
store.dispatch(actions.matrix(newMatrix));
store.dispatch(actions.moveBlock({ type: state.get('next') }));
store.dispatch(actions.nextBlock());
states.auto();
store.dispatch(actions.lock(false));
const clearLines = state.get('clearLines') + lines.length;
store.dispatch(actions.clearLines(clearLines)); // 更新消除行
const addPoints = store.getState().get('points') +
clearPoints[lines.length - 1]; // 一次消除的行越多, 加分越多
states.dispatchPoints(addPoints);
const speedAdd = Math.floor(clearLines / eachLines); // 消除行数, 增加对应速度
let speedNow = state.get('speedStart') + speedAdd;
speedNow = speedNow > 6 ? 6 : speedNow;
store.dispatch(actions.speedRun(speedNow));
},
// 游戏结束, 触发动画
overStart: () => {
clearTimeout(states.fallInterval);
store.dispatch(actions.lock(true));
store.dispatch(actions.reset(true));
store.dispatch(actions.pause(false));
},
// 游戏结束动画完成
overEnd: () => {
store.dispatch(actions.matrix(blankMatrix));
store.dispatch(actions.moveBlock({ reset: true }));
store.dispatch(actions.reset(false));
store.dispatch(actions.lock(false));
store.dispatch(actions.clearLines(0));
},
// 写入分数
dispatchPoints: (point) => { // 写入分数, 同时判断是否创造最高分
store.dispatch(actions.points(point));
if (point > 0 && point > store.getState().get('max')) {
store.dispatch(actions.max(point));
}
},
};
export default states;
import { want } from '../../unit/';
import event from '../../unit/event';
import actions from '../../actions';
import states from '../states';
import { music } from '../../unit/music';
const down = (store) => {
store.dispatch(actions.keyboard.down(true));
if (store.getState().get('cur') !== null) {
event.down({
key: 'down',
begin: 40,
interval: 40,
callback: (stopDownTrigger) => {
const state = store.getState();
if (state.get('lock')) {
return;
}
if (music.move) {
music.move();
}
const cur = state.get('cur');
if (cur === null) {
return;
}
if (state.get('pause')) {
states.pause(false);
return;
}
const next = cur.fall();
if (want(next, state.get('matrix'))) {
store.dispatch(actions.moveBlock(next));
states.auto();
} else {
let matrix = state.get('matrix');
const shape = cur.shape;
const xy = cur.xy;
shape.forEach((m, k1) => (
m.forEach((n, k2) => {
if (n && xy.get(0) + k1 >= 0) { // 竖坐标可以为负
let line = matrix.get(xy.get(0) + k1);
line = line.set(xy.get(1) + k2, 1);
matrix = matrix.set(xy.get(0) + k1, line);
}
})
));
states.nextAround(matrix, stopDownTrigger);
}
},
});
} else {
event.down({
key: 'down',
begin: 200,
interval: 100,
callback: () => {
if (store.getState().get('lock')) {
return;
}
const state = store.getState();
const cur = state.get('cur');
if (cur) {
return;
}
if (music.move) {
music.move();
}
let startLines = state.get('startLines');
startLines = startLines - 1 < 0 ? 10 : startLines - 1;
store.dispatch(actions.startLines(startLines));
},
});
}
};
const up = (store) => {
store.dispatch(actions.keyboard.down(false));
event.up({
key: 'down',
});
};
export default {
down,
up,
};
import left from './left';
import right from './right';
import down from './down';
import rotate from './rotate';
import space from './space';
import s from './s';
import r from './r';
import p from './p';
export default {
left,
down,
rotate,
right,
space,
r,
p,
s,
};
import { want } from '../../unit/';
import event from '../../unit/event';
import actions from '../../actions';
import states from '../states';
import { speeds, delays } from '../../unit/const';
import { music } from '../../unit/music';
const down = (store) => {
store.dispatch(actions.keyboard.left(true));
event.down({
key: 'left',
begin: 200,
interval: 100,
callback: () => {
const state = store.getState();
if (state.get('lock')) {
return;
}
if (music.move) {
music.move();
}
const cur = state.get('cur');
if (cur !== null) {
if (state.get('pause')) {
states.pause(false);
return;
}
const next = cur.left();
const delay = delays[state.get('speedRun') - 1];
let timeStamp;
if (want(next, state.get('matrix'))) {
next.timeStamp += parseInt(delay, 10);
store.dispatch(actions.moveBlock(next));
timeStamp = next.timeStamp;
} else {
cur.timeStamp += parseInt(parseInt(delay, 10) / 1.5, 10); // 真实移动delay多一点,碰壁delay少一点
store.dispatch(actions.moveBlock(cur));
timeStamp = cur.timeStamp;
}
const remain = speeds[state.get('speedRun') - 1] - (Date.now() - timeStamp);
states.auto(remain);
} else {
let speed = state.get('speedStart');
speed = speed - 1 < 1 ? 6 : speed - 1;
store.dispatch(actions.speedStart(speed));
}
},
});
};
const up = (store) => {
store.dispatch(actions.keyboard.left(false));
event.up({
key: 'left',
});
};
export default {
down,
up,
};
import event from '../../unit/event';
import states from '../states';
import actions from '../../actions';
const down = (store) => {
store.dispatch(actions.keyboard.pause(true));
event.down({
key: 'p',
once: true,
callback: () => {
const state = store.getState();
if (state.get('lock')) {
return;
}
const cur = state.get('cur');
const isPause = state.get('pause');
if (cur !== null) { // 暂停
states.pause(!isPause);
} else { // 新的开始
states.start();
}
},
});
};
const up = (store) => {
store.dispatch(actions.keyboard.pause(false));
event.up({
key: 'p',
});
};
export default {
down,
up,
};
import event from '../../unit/event';
import states from '../states';
import actions from '../../actions';
const down = (store) => {
store.dispatch(actions.keyboard.reset(true));
if (store.getState().get('lock')) {
return;
}
if (store.getState().get('cur') !== null) {
event.down({
key: 'r',
once: true,
callback: () => {
states.overStart();
},
});
} else {
event.down({
key: 'r',
once: true,
callback: () => {
if (store.getState().get('lock')) {
return;
}
states.start();
},
});
}
};
const up = (store) => {
store.dispatch(actions.keyboard.reset(false));
event.up({
key: 'r',
});
};
export default {
down,
up,
};
import { want } from '../../unit/';
import event from '../../unit/event';
import actions from '../../actions';
import states from '../states';
import { speeds, delays } from '../../unit/const';
import { music } from '../../unit/music';
const down = (store) => {
store.dispatch(actions.keyboard.right(true));
event.down({
key: 'right',
begin: 200,
interval: 100,
callback: () => {
const state = store.getState();
if (state.get('lock')) {
return;
}
if (music.move) {
music.move();
}
const cur = state.get('cur');
if (cur !== null) {
if (state.get('pause')) {
states.pause(false);
return;
}
const next = cur.right();
const delay = delays[state.get('speedRun') - 1];
let timeStamp;
if (want(next, state.get('matrix'))) {
next.timeStamp += parseInt(delay, 10);
store.dispatch(actions.moveBlock(next));
timeStamp = next.timeStamp;
} else {
cur.timeStamp += parseInt(parseInt(delay, 10) / 1.5, 10); // 真实移动delay多一点,碰壁delay少一点
store.dispatch(actions.moveBlock(cur));
timeStamp = cur.timeStamp;
}
const remain = speeds[state.get('speedRun') - 1] - (Date.now() - timeStamp);
states.auto(remain);
} else {
let speed = state.get('speedStart');
speed = speed + 1 > 6 ? 1 : speed + 1;
store.dispatch(actions.speedStart(speed));
}
},
});
};
const up = (store) => {
store.dispatch(actions.keyboard.right(false));
event.up({
key: 'right',
});
};
export default {
down,
up,
};
import { want } from '../../unit/';
import event from '../../unit/event';
import actions from '../../actions';
import states from '../states';
import { music } from '../../unit/music';
const down = (store) => {
store.dispatch(actions.keyboard.rotate(true));
if (store.getState().get('cur') !== null) {
event.down({
key: 'rotate',
once: true,
callback: () => {
const state = store.getState();
if (state.get('lock')) {
return;
}
if (state.get('pause')) {
states.pause(false);
}
const cur = state.get('cur');
if (cur === null) {
return;
}
if (music.rotate) {
music.rotate();
}
const next = cur.rotate();
if (want(next, state.get('matrix'))) {
store.dispatch(actions.moveBlock(next));
}
},
});
} else {
event.down({
key: 'rotate',
begin: 200,
interval: 100,
callback: () => {
if (store.getState().get('lock')) {
return;
}
if (music.move) {
music.move();
}
const state = store.getState();
const cur = state.get('cur');
if (cur) {
return;
}
let startLines = state.get('startLines');
startLines = startLines + 1 > 10 ? 0 : startLines + 1;
store.dispatch(actions.startLines(startLines));
},
});
}
};
const up = (store) => {
store.dispatch(actions.keyboard.rotate(false));
event.up({
key: 'rotate',
});
};
export default {
down,
up,
};
import event from '../../unit/event';
import actions from '../../actions';
const down = (store) => {
store.dispatch(actions.keyboard.music(true));
if (store.getState().get('lock')) {
return;
}
event.down({
key: 's',
once: true,
callback: () => {
if (store.getState().get('lock')) {
return;
}
store.dispatch(actions.music(!store.getState().get('music')));
},
});
};
const up = (store) => {
store.dispatch(actions.keyboard.music(false));
event.up({
key: 's',
});
};
export default {
down,
up,
};
import { want } from '../../unit/';
import event from '../../unit/event';
import actions from '../../actions';
import states from '../states';
import { music } from '../../unit/music';
const down = (store) => {
store.dispatch(actions.keyboard.drop(true));
event.down({
key: 'space',
once: true,
callback: () => {
const state = store.getState();
if (state.get('lock')) {
return;
}
const cur = state.get('cur');
if (cur !== null) { // 置底
if (state.get('pause')) {
states.pause(false);
return;
}
if (music.fall) {
music.fall();
}
let index = 0;
let bottom = cur.fall(index);
while (want(bottom, state.get('matrix'))) {
bottom = cur.fall(index);
index++;
}
let matrix = state.get('matrix');
bottom = cur.fall(index - 2);
store.dispatch(actions.moveBlock(bottom));
const shape = bottom.shape;
const xy = bottom.xy;
shape.forEach((m, k1) => (
m.forEach((n, k2) => {
if (n && xy[0] + k1 >= 0) { // 竖坐标可以为负
let line = matrix.get(xy[0] + k1);
line = line.set(xy[1] + k2, 1);
matrix = matrix.set(xy[0] + k1, line);
}
})
));
store.dispatch(actions.drop(true));
setTimeout(() => {
store.dispatch(actions.drop(false));
}, 100);
states.nextAround(matrix);
} else {
states.start();
}
},
});
};
const up = (store) => {
store.dispatch(actions.keyboard.drop(false));
event.up({
key: 'space',
});
};
export default {
down,
up,
};
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './containers/';
import './unit/const';
import './control';
import { subscribeRecord } from './unit';
subscribeRecord(store); // 将更新的状态记录到localStorage
render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root')
);
import * as reducerType from '../../unit/reducerType';
import { lastRecord } from '../../unit/const';
let initState = lastRecord && !isNaN(parseInt(lastRecord.clearLines, 10)) ?
parseInt(lastRecord.clearLines, 10) : 0;
if (initState < 0) {
initState = 0;
}
const clearLines = (state = initState, action) => {
switch (action.type) {
case reducerType.CLEAR_LINES:
return action.data;
default:
return state;
}
};
export default clearLines;
import { List } from 'immutable';
import * as reducerType from '../../unit/reducerType';
import { lastRecord } from '../../unit/const';
import Block from '../../unit/block';
const initState = (() => {
if (!lastRecord || !lastRecord.cur) { // 无记录 或 有记录 但方块为空, 返回 null
return null;
}
const cur = lastRecord.cur;
const option = {
type: cur.type,
rotateIndex: cur.rotateIndex,
shape: List(cur.shape.map(e => List(e))),
xy: cur.xy,
};
return new Block(option);
})();
const cur = (state = initState, action) => {
switch (action.type) {
case reducerType.MOVE_BLOCK:
return action.data;
default:
return state;
}
};
export default cur;
import * as reducerType from '../../unit/reducerType';
import { lastRecord } from '../../unit/const';
const initState = lastRecord && lastRecord.drop !== undefined ? !!lastRecord.drop : false;
const drop = (state = initState, action) => {
switch (action.type) {
case reducerType.DROP:
return action.data;
default:
return state;
}
};
export default drop;
import * as reducerType from '../../unit/reducerType';
import { isFocus } from '../../unit/';
const initState = isFocus();
const focus = (state = initState, action) => {
switch (action.type) {
case reducerType.FOCUS:
return action.data;
default:
return state;
}
};
export default focus;
import { combineReducers } from 'redux-immutable';
import pause from './pause';
import music from './music';
import matrix from './matrix';
import next from './next';
import cur from './cur';
import startLines from './startLines';
import max from './max';
import points from './points';
import speedStart from './speedStart';
import speedRun from './speedRun';
import lock from './lock';
import clearLines from './clearLines';
import reset from './reset';
import drop from './drop';
import keyboard from './keyboard';
import focus from './focus';
const rootReducer = combineReducers({
pause,
music,
matrix,
next,
cur,
startLines,
max,
points,
speedStart,
speedRun,
lock,
clearLines,
reset,
drop,
keyboard,
focus,
});
export default rootReducer;
import * as reducerType from '../../unit/reducerType';
const initState = false;
const reducer = (state = initState, action) => {
switch (action.type) {
case reducerType.KEY_DOWN:
return action.data;
default:
return state;
}
};
export default reducer;
import * as reducerType from '../../unit/reducerType';
const initState = false;
const reducer = (state = initState, action) => {
switch (action.type) {
case reducerType.KEY_DROP:
return action.data;
default:
return state;
}
};
export default reducer;
import { combineReducers } from 'redux-immutable';
import drop from './drop';
import down from './down';
import left from './left';
import right from './right';
import rotate from './rotate';
import reset from './reset';
import music from './music';
import pause from './pause';
const keyboardReducer = combineReducers({
drop,
down,
left,
right,
rotate,
reset,
music,
pause,
});
export default keyboardReducer;
import * as reducerType from '../../unit/reducerType';
const initState = false;
const reducer = (state = initState, action) => {
switch (action.type) {
case reducerType.KEY_LEFT:
return action.data;
default:
return state;
}
};
export default reducer;
import * as reducerType from '../../unit/reducerType';
const initState = false;
const reducer = (state = initState, action) => {
switch (action.type) {
case reducerType.KEY_MUSIC:
return action.data;
default:
return state;
}
};
export default reducer;
import * as reducerType from '../../unit/reducerType';
const initState = false;
const reducer = (state = initState, action) => {
switch (action.type) {
case reducerType.KEY_PAUSE:
return action.data;
default:
return state;
}
};
export default reducer;
import * as reducerType from '../../unit/reducerType';
const initState = false;
const reducer = (state = initState, action) => {
switch (action.type) {
case reducerType.KEY_RESET:
return action.data;
default:
return state;
}
};
export default reducer;
import * as reducerType from '../../unit/reducerType';
const initState = false;
const reducer = (state = initState, action) => {
switch (action.type) {
case reducerType.KEY_RIGHT:
return action.data;
default:
return state;
}
};
export default reducer;
import * as reducerType from '../../unit/reducerType';
const initState = false;
const reducer = (state = initState, action) => {
switch (action.type) {
case reducerType.KEY_ROTATE:
return action.data;
default:
return state;
}
};
export default reducer;
import * as reducerType from '../../unit/reducerType';
import { lastRecord } from '../../unit/const';
const initState = lastRecord && lastRecord.lock !== undefined ? !!lastRecord.lock : false;
const lock = (state = initState, action) => {
switch (action.type) {
case reducerType.LOCK:
return action.data;
default:
return state;
}
};
export default lock;
import { List } from 'immutable';
import * as reducerType from '../../unit/reducerType';
import { blankMatrix, lastRecord } from '../../unit/const';
const initState = lastRecord && Array.isArray(lastRecord.matrix) ?
List(lastRecord.matrix.map(e => List(e))) : blankMatrix;
const matrix = (state = initState, action) => {
switch (action.type) {
case reducerType.MATRIX:
return action.data;
default:
return state;
}
};
export default matrix;
import * as reducerType from '../../unit/reducerType';
import { lastRecord, maxPoint } from '../../unit/const';
let initState = lastRecord && !isNaN(parseInt(lastRecord.max, 10)) ?
parseInt(lastRecord.max, 10) : 0;
if (initState < 0) {
initState = 0;
} else if (initState > maxPoint) {
initState = maxPoint;
}
const parse = (state = initState, action) => {
switch (action.type) {
case reducerType.MAX:
return action.data > 999999 ? 999999 : action.data; // 最大分数
default:
return state;
}
};
export default parse;
import * as reducerType from '../../unit/reducerType';
import { lastRecord } from '../../unit/const';
import { hasWebAudioAPI } from '../../unit/music';
let initState = lastRecord && lastRecord.music !== undefined ? !!lastRecord.music : true;
if (!hasWebAudioAPI.data) {
initState = false;
}
const music = (state = initState, action) => {
switch (action.type) {
case reducerType.MUSIC:
if (!hasWebAudioAPI.data) { // 若浏览器不支持 WebAudioApi, 将无法播放音效
return false;
}
return action.data;
default:
return state;
}
};
export default music;
import { getNextType } from '../../unit';
import * as reducerType from '../../unit/reducerType';
import { lastRecord, blockType } from '../../unit/const';
const initState = lastRecord && blockType.indexOf(lastRecord.next) !== -1 ?
lastRecord.next : getNextType();
const parse = (state = initState, action) => {
switch (action.type) {
case reducerType.NEXT_BLOCK:
return action.data;
default:
return state;
}
};
export default parse;
import * as reducerType from '../../unit/reducerType';
import { lastRecord } from '../../unit/const';
const initState = lastRecord && lastRecord.pause !== undefined ? !!lastRecord.pause : false;
const pause = (state = initState, action) => {
switch (action.type) {
case reducerType.PAUSE:
return action.data;
default:
return state;
}
};
export default pause;
import * as reducerType from '../../unit/reducerType';
import { lastRecord, maxPoint } from '../../unit/const';
let initState = lastRecord && !isNaN(parseInt(lastRecord.points, 10)) ?
parseInt(lastRecord.points, 10) : 0;
if (initState < 0) {
initState = 0;
} else if (initState > maxPoint) {
initState = maxPoint;
}
const points = (state = initState, action) => {
switch (action.type) {
case reducerType.POINTS:
return action.data > maxPoint ? maxPoint : action.data; // 最大分数
default:
return state;
}
};
export default points;
import * as reducerType from '../../unit/reducerType';
import { lastRecord } from '../../unit/const';
const initState = lastRecord && lastRecord.reset ? !!lastRecord.reset : false;
const reset = (state = initState, action) => {
switch (action.type) {
case reducerType.RESET:
return action.data;
default:
return state;
}
};
export default reset;
import * as reducerType from '../../unit/reducerType';
import { lastRecord } from '../../unit/const';
let initState = lastRecord && !isNaN(parseInt(lastRecord.speedRun, 10)) ?
parseInt(lastRecord.speedRun, 10) : 1;
if (initState < 1 || initState > 6) {
initState = 1;
}
const speedRun = (state = initState, action) => {
switch (action.type) {
case reducerType.SPEED_RUN:
return action.data;
default:
return state;
}
};
export default speedRun;
import * as reducerType from '../../unit/reducerType';
import { lastRecord } from '../../unit/const';
let initState = lastRecord && !isNaN(parseInt(lastRecord.speedStart, 10)) ?
parseInt(lastRecord.speedStart, 10) : 1;
if (initState < 1 || initState > 6) {
initState = 1;
}
const speedStart = (state = initState, action) => {
switch (action.type) {
case reducerType.SPEED_START:
return action.data;
default:
return state;
}
};
export default speedStart;
import * as reducerType from '../../unit/reducerType';
import { lastRecord } from '../../unit/const';
let initState = lastRecord && !isNaN(parseInt(lastRecord.startLines, 10)) ?
parseInt(lastRecord.startLines, 10) : 0;
if (initState < 0 || initState > 10) {
initState = 0;
}
const startLines = (state = initState, action) => {
switch (action.type) {
case reducerType.START_LINES:
return action.data;
default:
return state;
}
};
export default startLines;
body{
background: #009688;
padding: 0;
margin: 0;
}
.load{
width:240px;
height:240px;
position:absolute;
top:50%;
left:50%;
margin:-120px 0 0 -120px;
color:#efcc19;
-webkit-animation:fadeIn 2s infinite ease-in-out;
animation:fadeIn 2s infinite ease-in-out;
-webkit-animation-delay:2s;
animation-delay:2s;
opacity:0;
}
.load .loader,.load .loader:before,.load .loader:after{
background:#efcc19;
-webkit-animation:load 1s infinite ease-in-out;
animation:load 1s infinite ease-in-out;
width:1em;
height:4em
}
.load .loader:before,.load .loader:after{
position:absolute;
top:0;
content:''
}
.load .loader:before{
left:-1.5em;
-webkit-animation-delay:-0.32s;
animation-delay:-0.32s
}
.load .loader{
text-indent:-9999em;
margin:8em auto;
position:relative;
font-size:11px;
-webkit-animation-delay:-0.16s;
animation-delay:-0.16s
}
.load .loader:after{
left:1.5em
}
@-webkit-keyframes load{
0%,80%,100%{
box-shadow:0 0 #efcc19;
height:4em
}
40%{
box-shadow:0 -2em #efcc19;height:5em
}
}
@keyframes load{
0%,80%,100%{
box-shadow:0 0 #efcc19;
height:4em
}
40%{
box-shadow:0 -2em #efcc19;
height:5em
}
}
@-webkit-keyframes fadeIn{
0%{
opacity:0;
}
100%{
opacity:1;
}
}
@keyframes fadeIn{
0%{
opacity:0;
}
100%{
opacity:1;
}
}
\ No newline at end of file
import { createStore } from 'redux';
import rootReducer from '../reducers';
const store = createStore(rootReducer, window.devToolsExtension && window.devToolsExtension());
export default store;
import { List } from 'immutable';
import { blockShape, origin } from './const';
class Block {
constructor(option) {
this.type = option.type;
if (!option.rotateIndex) {
this.rotateIndex = 0;
} else {
this.rotateIndex = option.rotateIndex;
}
if (!option.timeStamp) {
this.timeStamp = Date.now();
} else {
this.timeStamp = option.timeStamp;
}
if (!option.shape) { // init
this.shape = List(blockShape[option.type].map(e => List(e)));
} else {
this.shape = option.shape;
}
if (!option.xy) {
switch (option.type) {
case 'I': // I
this.xy = List([0, 3]);
break;
case 'L': // L
this.xy = List([-1, 4]);
break;
case 'J': // J
this.xy = List([-1, 4]);
break;
case 'Z': // Z
this.xy = List([-1, 4]);
break;
case 'S': // S
this.xy = List([-1, 4]);
break;
case 'O': // O
this.xy = List([-1, 4]);
break;
case 'T': // T
this.xy = List([-1, 4]);
break;
default:
break;
}
} else {
this.xy = List(option.xy);
}
}
rotate() {
const shape = this.shape;
let result = List([]);
shape.forEach(m => m.forEach((n, k) => {
const index = m.size - k - 1;
if (result.get(index) === undefined) {
result = result.set(index, List([]));
}
const tempK = result.get(index).push(n);
result = result.set(index, tempK);
}));
const nextXy = [
this.xy.get(0) + origin[this.type][this.rotateIndex][0],
this.xy.get(1) + origin[this.type][this.rotateIndex][1],
];
const nextRotateIndex = this.rotateIndex + 1 >= origin[this.type].length ?
0 : this.rotateIndex + 1;
return {
shape: result,
type: this.type,
xy: nextXy,
rotateIndex: nextRotateIndex,
timeStamp: this.timeStamp,
};
}
fall(n = 1) {
return {
shape: this.shape,
type: this.type,
xy: [this.xy.get(0) + n, this.xy.get(1)],
rotateIndex: this.rotateIndex,
timeStamp: Date.now(),
};
}
right() {
return {
shape: this.shape,
type: this.type,
xy: [this.xy.get(0), this.xy.get(1) + 1],
rotateIndex: this.rotateIndex,
timeStamp: this.timeStamp,
};
}
left() {
return {
shape: this.shape,
type: this.type,
xy: [this.xy.get(0), this.xy.get(1) - 1],
rotateIndex: this.rotateIndex,
timeStamp: this.timeStamp,
};
}
}
export default Block;
import { List } from 'immutable';
import i18n from '../../i18n.json';
const blockShape = {
I: [
[1, 1, 1, 1],
],
L: [
[0, 0, 1],
[1, 1, 1],
],
J: [
[1, 0, 0],
[1, 1, 1],
],
Z: [
[1, 1, 0],
[0, 1, 1],
],
S: [
[0, 1, 1],
[1, 1, 0],
],
O: [
[1, 1],
[1, 1],
],
T: [
[0, 1, 0],
[1, 1, 1],
],
};
const origin = {
I: [[-1, 1], [1, -1]],
L: [[0, 0]],
J: [[0, 0]],
Z: [[0, 0]],
S: [[0, 0]],
O: [[0, 0]],
T: [[0, 0], [1, 0], [-1, 1], [0, -1]],
};
const blockType = Object.keys(blockShape);
const speeds = [800, 650, 500, 370, 250, 160];
const delays = [50, 60, 70, 80, 90, 100];
const fillLine = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
const blankLine = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const blankMatrix = (() => {
const matrix = [];
for (let i = 0; i < 20; i++) {
matrix.push(List(blankLine));
}
return List(matrix);
})();
const clearPoints = [100, 300, 700, 1500];
const StorageKey = 'REACT_TETRIS';
const lastRecord = (() => { // 上一把的状态
let data = localStorage.getItem(StorageKey);
if (!data) {
return false;
}
try {
if (window.btoa) {
data = atob(data);
}
data = decodeURIComponent(data);
data = JSON.parse(data);
} catch (e) {
if (window.console || window.console.error) {
window.console.error('读取记录错误:', e);
}
return false;
}
return data;
})();
const maxPoint = 999999;
const transform = (function () {
const trans = ['transform', 'webkitTransform', 'msTransform', 'mozTransform', 'oTransform'];
const body = document.body;
return trans.filter((e) => body.style[e] !== undefined)[0];
}());
const eachLines = 20; // 每消除eachLines行, 增加速度
const getParam = (param) => { // 获取浏览器参数
const r = new RegExp(`\\?(?:.+&)?${param}=(.*?)(?:&.*)?$`);
const m = window.location.toString().match(r);
return m ? decodeURI(m[1]) : '';
};
const lan = (() => {
let l = getParam('lan').toLowerCase();
l = i18n.lan.indexOf(l) === -1 ? i18n.default : l;
return l;
})();
document.title = i18n.data.title[lan];
module.exports = {
blockShape,
origin,
blockType,
speeds,
delays,
fillLine,
blankLine,
blankMatrix,
clearPoints,
StorageKey,
lastRecord,
maxPoint,
eachLines,
transform,
lan,
i18n: i18n.data,
};
const eventName = {};
const down = (o) => { // 键盘、手指按下
const keys = Object.keys(eventName);
keys.forEach(i => {
clearTimeout(eventName[i]);
eventName[i] = null;
});
if (!o.callback) {
return;
}
const clear = () => {
clearTimeout(eventName[o.key]);
};
o.callback(clear);
if (o.once === true) {
return;
}
let begin = o.begin || 100;
const interval = o.interval || 50;
const loop = () => {
eventName[o.key] = setTimeout(() => {
begin = null;
loop();
o.callback(clear);
}, begin || interval);
};
loop();
};
const up = (o) => { // 键盘、手指松开
clearTimeout(eventName[o.key]);
eventName[o.key] = null;
if (!o.callback) {
return;
}
o.callback();
};
const clearAll = () => {
const keys = Object.keys(eventName);
keys.forEach(i => {
clearTimeout(eventName[i]);
eventName[i] = null;
});
};
export default {
down,
up,
clearAll,
};
import { blockType, StorageKey } from './const';
const hiddenProperty = (() => { // document[hiddenProperty] 可以判断页面是否失焦
let names = [
'hidden',
'webkitHidden',
'mozHidden',
'msHidden',
];
names = names.filter((e) => (e in document));
return names.length > 0 ? names[0] : false;
})();
const visibilityChangeEvent = (() => {
if (!hiddenProperty) {
return false;
}
return hiddenProperty.replace(/hidden/i, 'visibilitychange'); // 如果属性有前缀, 相应的事件也有前缀
})();
const isFocus = () => {
if (!hiddenProperty) { // 如果不存在该特性, 认为一直聚焦
return true;
}
return !document[hiddenProperty];
};
const unit = {
getNextType() { // 随机获取下一个方块类型
const len = blockType.length;
return blockType[Math.floor(Math.random() * len)];
},
want(next, matrix) { // 方块是否能移到到指定位置
const xy = next.xy;
const shape = next.shape;
const horizontal = shape.get(0).size;
return shape.every((m, k1) => (
m.every((n, k2) => {
if (xy[1] < 0) { // left
return false;
}
if (xy[1] + horizontal > 10) { // right
return false;
}
if (xy[0] + k1 < 0) { // top
return true;
}
if (xy[0] + k1 >= 20) { // bottom
return false;
}
if (n) {
if (matrix.get(xy[0] + k1).get(xy[1] + k2)) {
return false;
}
return true;
}
return true;
})
));
},
isClear(matrix) { // 是否达到消除状态
const clearLines = [];
matrix.forEach((m, k) => {
if (m.every(n => !!n)) {
clearLines.push(k);
}
});
if (clearLines.length === 0) {
return false;
}
return clearLines;
},
isOver(matrix) { // 游戏是否结束, 第一行落下方块为依据
return matrix.get(0).some(n => !!n);
},
subscribeRecord(store) { // 将状态记录到 localStorage
store.subscribe(() => {
let data = store.getState().toJS();
if (data.lock) { // 当状态为锁定, 不记录
return;
}
data = JSON.stringify(data);
data = encodeURIComponent(data);
if (window.btoa) {
data = btoa(data);
}
localStorage.setItem(StorageKey, data);
});
},
isMobile() { // 判断是否为移动端
const ua = navigator.userAgent;
const android = /Android (\d+\.\d+)/.test(ua);
const iphone = ua.indexOf('iPhone') > -1;
const ipod = ua.indexOf('iPod') > -1;
const ipad = ua.indexOf('iPad') > -1;
const nokiaN = ua.indexOf('NokiaN') > -1;
return android || iphone || ipod || ipad || nokiaN;
},
visibilityChangeEvent,
isFocus,
};
module.exports = unit;
import store from '../store';
// 使用 Web Audio API
const AudioContext = (
window.AudioContext ||
window.webkitAudioContext ||
window.mozAudioContext ||
window.oAudioContext ||
window.msAudioContext
);
const hasWebAudioAPI = {
data: !!AudioContext && location.protocol.indexOf('http') !== -1,
};
const music = {};
(() => {
if (!hasWebAudioAPI.data) {
return;
}
const url = './music.mp3';
const context = new AudioContext();
const req = new XMLHttpRequest();
req.open('GET', url, true);
req.responseType = 'arraybuffer';
req.onload = () => {
context.decodeAudioData(req.response, (buf) => { // 将拿到的audio解码转为buffer
const getSource = () => { // 创建source源。
const source = context.createBufferSource();
source.buffer = buf;
source.connect(context.destination);
return source;
};
music.killStart = () => { // 游戏开始的音乐只播放一次
music.start = () => {};
};
music.start = () => { // 游戏开始
music.killStart();
if (!store.getState().get('music')) {
return;
}
getSource().start(0, 3.7202, 3.6224);
};
music.clear = () => { // 消除方块
if (!store.getState().get('music')) {
return;
}
getSource().start(0, 0, 0.7675);
};
music.fall = () => { // 立即下落
if (!store.getState().get('music')) {
return;
}
getSource().start(0, 1.2558, 0.3546);
};
music.gameover = () => { // 游戏结束
if (!store.getState().get('music')) {
return;
}
getSource().start(0, 8.1276, 1.1437);
};
music.rotate = () => { // 旋转
if (!store.getState().get('music')) {
return;
}
getSource().start(0, 2.2471, 0.0807);
};
music.move = () => { // 移动
if (!store.getState().get('music')) {
return;
}
getSource().start(0, 2.9088, 0.1437);
};
},
(error) => {
if (window.console && window.console.error) {
window.console.error(`音频: ${url} 读取错误`, error);
hasWebAudioAPI.data = false;
}
});
};
req.send();
})();
module.exports = {
hasWebAudioAPI,
music,
};
export const PAUSE = 'PAUSE';
export const MUSIC = 'MUSIC';
export const MATRIX = 'MATRIX';
export const NEXT_BLOCK = 'NEXT_BLOCK';
export const MOVE_BLOCK = 'MOVE_BLOCK';
export const START_LINES = 'START_LINES';
export const MAX = 'MAX';
export const POINTS = 'POINTS';
export const SPEED_START = 'SPEED_START';
export const SPEED_RUN = 'SPEED_RUN';
export const LOCK = 'LOCK';
export const CLEAR_LINES = 'CLEAR_LINES';
export const RESET = 'RESET';
export const DROP = 'DROP';
export const KEY_DROP = 'KEY_DROP';
export const KEY_DOWN = 'KEY_DOWN';
export const KEY_LEFT = 'KEY_LEFT';
export const KEY_RIGHT = 'KEY_RIGHT';
export const KEY_ROTATE = 'KEY_ROTATE';
export const KEY_RESET = 'KEY_RESET';
export const KEY_MUSIC = 'KEY_MUSIC';
export const KEY_PAUSE = 'KEY_PAUSE';
export const FOCUS = 'FOCUS';
var webpack = require('webpack');
var OpenBrowserPlugin = require('open-browser-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var precss = require('precss');
var autoprefixer = require('autoprefixer');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var version = require('./package.json').version;
// 程序入口
var entry = __dirname + '/src/index.js';
// 输出文件
var output = {
filename: 'page/[name]/index.js',
chunkFilename: 'chunk/[name].[chunkhash:5].chunk.js',
};
// 生成source-map追踪js错误
var devtool = 'source-map';
// eslint
var eslint = {
configFile: __dirname + '/.eslintrc.js',
}
// loader
var loaders = [
{
test: /\.(json)$/,
exclude: /node_modules/,
loader: 'json',
},
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel!eslint-loader',
},
{
test: /\.(?:png|jpg|gif)$/,
loader: 'url?limit=8192', //小于8k,内嵌;大于8k生成文件
},
{
test: /\.less/,
loader: ExtractTextPlugin.extract('style', 'css?modules&localIdentName=[hash:base64:4]!postcss!less'),
}
];
// dev plugin
var devPlugins = [
new CopyWebpackPlugin([
{ from: './src/resource/music/music.mp3' },
{ from: './src/resource/css/loader.css' },
]),
// 热更新
new webpack.HotModuleReplacementPlugin(),
// 允许错误不打断程序, 仅开发模式需要
new webpack.NoErrorsPlugin(),
// 打开浏览器页面
new OpenBrowserPlugin({
url: 'http://127.0.0.1:8080/'
}),
// css打包
new ExtractTextPlugin('css.css', {
allChunks: true
}),
]
// production plugin
var productionPlugins = [
// 定义生产环境
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
}),
// 复制
new CopyWebpackPlugin([
{ from: './src/resource/music/music.mp3' },
{ from: './src/resource/css/loader.css' },
]),
// HTML 模板
new HtmlWebpackPlugin({
template: __dirname + '/server/index.tmpl.html'
}),
// JS压缩
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}}
),
// css打包
new ExtractTextPlugin('css-' + version + '.css', {
allChunks: true
}),
];
// dev server
var devServer = {
contentBase: './server',
colors: true,
historyApiFallback: false,
port: 8080, // defaults to "8080"
hot: true, // Hot Module Replacement
inline: true, // Livereload
host: '0.0.0.0',
disableHostCheck: true
};
module.exports = {
entry: entry,
devtool: devtool,
output: output,
loaders: loaders,
devPlugins: devPlugins,
productionPlugins: productionPlugins,
devServer: devServer,
postcss: function () {
return [precss, autoprefixer];
},
version: version
};
var config = require('./w.config');
// dev环境配置
module.exports = {
devtool: config.devtool,
entry: config.entry,
output: {
path: __dirname + '/server',
filename: 'app.js',
},
eslint: config.eslint,
module: {
loaders: config.loaders
},
plugins: config.devPlugins,
devServer: config.devServer,
postcss: config.postcss
};
var config = require('./w.config');
// production环境配置
module.exports = {
devtool: config.devtool,
entry: config.entry,
output: {
path: __dirname + '/docs',
filename: 'app-' + config.version+'.js',
},
eslint: config.eslint,
module: {
loaders: config.loaders
},
plugins: config.productionPlugins,
postcss: config.postcss
};
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册