Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

initial import, the code is compilable under opa 0.9.0 ~ 0.9.4.

  • Loading branch information...
commit c46d0493dd0b063d9996e7730fc97b32fd423300 1 parent e54c88a
@winbomb authored
Showing with 4,633 additions and 0 deletions.
  1. +47 −0 Makefile
  2. +7 −0 plugins/Makefile
  3. +267 −0 plugins/engine2d.js
  4. BIN  resources/arrow.png
  5. BIN  resources/board.png
  6. BIN  resources/button.wav
  7. BIN  resources/countfan.wav
  8. BIN  resources/da.wav
  9. BIN  resources/dialog.png
  10. BIN  resources/dragon.jpg
  11. BIN  resources/eg_jong.png
  12. BIN  resources/eg_kong.png
  13. BIN  resources/eg_pung.png
  14. BIN  resources/eg_shun.png
  15. BIN  resources/eswn.png
  16. BIN  resources/game_bg.png
  17. BIN  resources/home_bg.jpg
  18. +211 −0 resources/how_to_play.html
  19. BIN  resources/loading.gif
  20. BIN  resources/login_bg.png
  21. +120 −0 resources/main.css
  22. BIN  resources/menu_bar.png
  23. BIN  resources/numbers.png
  24. BIN  resources/offline.png
  25. +61 −0 resources/page.css
  26. BIN  resources/page_bg.jpg
  27. BIN  resources/player_frame_h.png
  28. BIN  resources/player_frame_v.png
  29. BIN  resources/portrait.jpg
  30. BIN  resources/result.png
  31. BIN  resources/start.png
  32. BIN  resources/start.wav
  33. +153 −0 resources/style.css
  34. BIN  resources/table_bg.png
  35. BIN  resources/tiles.png
  36. BIN  resources/tiles_small.png
  37. BIN  resources/ting.png
  38. BIN  resources/tray.wav
  39. BIN  resources/win.wav
  40. BIN  resources/yellow_bar.png
  41. +666 −0 src/board.opa
  42. +739 −0 src/game.opa
  43. +98 −0 src/input.opa
  44. +170 −0 src/login.opa
  45. +640 −0 src/mahjong.opa
  46. +137 −0 src/main.opa
  47. +96 −0 src/network.opa
  48. +123 −0 src/page.opa
  49. +965 −0 src/render.opa
  50. +133 −0 src/testutil.opa
View
47 Makefile
@@ -0,0 +1,47 @@
+S=@
+OPA=opa
+CONFIG_FILE=mahjong.conf
+LOGS=error.log access.log
+BUILD_DIRS=_build _tracks *.opx *.opx.broken
+PLUGINS_DIR=plugins
+PLUGINS=-I {plugins} engine2d.opp
+
+SRC_DIR=src
+SRC_FILES=$(wildcard $(SRC_DIR)/*.opa)
+
+BIN_DIR=bin
+EXEC_NAME=./mahjong.exe
+RUN_OPTS= --verbose 2 --db-remote:mahjong localhost:27017
+DEBUG_NAME=./mahjong.exe --verbose 8
+EXEC=$(BIN_DIR)/$(EXEC_NAME)
+
+INSTALL_DIR=/usr/bin/mahjong
+
+default:
+ $(S) clear
+ $(OPA) $(PLUGINS) $(SRC_FILES) -o $(EXEC)
+
+all:
+ $(S) clear
+ $(S) make clean
+ $(S) make plugin
+ $(OPA) $(PLUGINS) $(SRC_FILES) -o $(EXEC)
+
+plugin:
+ cd plugins && make clean && make all && cd -
+
+install:
+ $(S) install $(EXEC) $(INSTALL_DIR)
+
+run:
+ $(S) cd $(BIN_DIR) && $(EXEC_NAME) $(RUN_OPTS) && cd -
+
+debug:
+ $(S) cd $(BIN_DIR) && $(DEBUG_NAME) $(RUN_OPTS) && cd -
+
+clean:
+ $(S) rm -fvr $(BUILD_DIRS)
+ $(S) rm -fvr $(BIN_DIR)/*.log
+ $(S) rm -fvr $(BIN_DIR)/*.out
+ $(S) rm -fvr $(PLUGINS_DIR)/*.opp
+ $(S) rm -fvr $(EXEC)
View
7 plugins/Makefile
@@ -0,0 +1,7 @@
+OPA_PLUGIN=opa-plugin-builder
+
+all:
+ $(OPA_PLUGIN) --js-validator-off engine2d.js -o engine2d
+
+clean:
+ $(S) rm -fvr engine2d.opp
View
267 plugins/engine2d.js
@@ -0,0 +1,267 @@
+##opa-type list('a)
+##opa-type Game.obj
+##extern-type Image.image
+
+var IMG_CACHE = {} //图像的缓存
+var AUD_CACHE = {} //声音的缓存
+
+var game;
+##register set_game: opa[Game.obj] -> void
+##args(gm)
+{
+ game = gm;
+}
+
+##register get_game: -> opa[Game.obj]
+##args()
+{
+ return game;
+}
+
+##register preload: opa[list(string)],opa[list(string)],( -> void) -> void
+##args(imgIdents,audIdents,callback)
+{
+ AUD_CACHE = {};
+ var images = list2js(imgIdents);
+ var audios = list2js(audIdents);
+ var countLoaded = 0;
+ var countTotal = 0;
+
+ function incrementLoaded() {
+ countLoaded++;
+ info = document.getElementById("loading_info");
+ if(!!info){
+ info.innerHTML = "loading game resource... [ "+countLoaded+" / "+countTotal+" ]"
+ }
+ if (countLoaded >= countTotal) {
+ callback();
+ }
+ }
+
+ function getProgress() {
+ return countTotal > 0 ? countLoaded / countTotal : 1;
+ }
+
+ function imgSuccessHandler() {
+ IMG_CACHE[this.key] = this;
+ incrementLoaded();
+ }
+
+ function audSuccessHandler() {
+ //不知到为什么Firefox会触发两次canplay事件,
+ //如果不做判断,会出现countLoaded大于countTotal的事情。
+ if(!AUD_CACHE[this.key]){
+ AUD_CACHE[this.key] = this;
+ incrementLoaded();
+ }
+ }
+
+ function errorHandler() {
+ incrementLoaded();
+ throw new Error('Error loading ' + this.src);
+ }
+
+ for (var i=0;i<images.length;i++) {
+ var key = images[i]
+ if (key.indexOf('png') == -1 &&
+ key.indexOf('jpg') == -1 &&
+ key.indexOf('gif') == -1) {
+ continue;
+ }
+
+ var img = new Image();
+ countTotal++;
+ img.addEventListener('load', imgSuccessHandler, true);
+ img.addEventListener('error', errorHandler, true);
+ img.src = key;
+ img.key = key;
+ }
+
+ if(window.HTMLAudioElement){
+ try{
+ var audio = document.createElement("audio");
+ if(audio != null && audio.canPlayType && audio.canPlayType("audio/wav")){
+ for( var i=0;i<audios.length;i++){
+ var key = audios[i]
+ if(key.indexOf('wav') == -1) continue;
+
+ var audio = new Audio();
+ countTotal++;
+ audio.addEventListener('canplay', audSuccessHandler, true);
+ audio.addEventListener('error', errorHandler, true);
+ audio.src = key;
+ audio.key = key;
+ audio.load();
+ }
+ }
+ }catch(e){
+ alert("Error: "+e);
+ window.console.error("Error"+e);
+ }
+ }
+
+ /** if(!!(a.canPlayType && a.canPlayType('audio/mpeg;').replace(/no/, ''))){
+ for( var i=0;i<audios.length;i++){
+ var key = audios[i]
+ if(key.indexOf('wav') == -1 &&
+ key.indexOf('ogg') == -1) {
+ continue;
+ }
+
+ var audio = new Audio();
+ countTotal++;
+ audio.addEventListener('canplay', audSuccessHandler, true);
+ audio.addEventListener('error', errorHandler, true);
+ audio.src = key;
+ audio.key = key;
+ audio.load();
+ }
+ }*/
+}
+
+##register get: string -> Image.image
+##args(key)
+{
+ var img = IMG_CACHE[key];
+ if (!img) {
+ throw new Error('Missing "' + key + '", preload() all images before trying to load them.');
+ }
+ return img;
+}
+
+var counter;
+
+##register start_timer: -> void
+##args()
+{
+ var ctx = document.getElementById("gmcanvas").getContext("2d");
+ counter = 11;
+ var loop = function(){
+ return function(){
+ if(counter > 0){
+ counter = counter - 1;
+ ctx.clearRect(330+28,237+28,24,24);
+ ctx.restore();
+ ctx.save(ctx);
+ ctx.fillStyle = "#efea3a";
+ ctx.fillRect(330+28,237+28,24,24);
+ ctx.fillStyle = "red";
+ ctx.font = "normal bold 24px serif";
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText(counter,370,277);
+ ctx.restore();
+
+ window.setTimeout(arguments.callee,1000);
+ }
+ }
+ }
+ loop.call(this)();
+}
+
+##register stop_timer: -> void
+##args()
+{
+ counter = 0;
+}
+
+##register show_menu: int -> void
+##args(opt_flag)
+{
+ var ctx = document.getElementById("gmcanvas").getContext("2d");
+ var arr = [8,4,2,1]
+ var arr2 = new Array(4);
+ for(var i = 0; i < arr.length; i++){
+ if(opt_flag >= arr[i]){
+ arr2[i] = true;
+ opt_flag = opt_flag - arr[i];
+ }else {
+ arr2[i] = false;
+ }
+ }
+
+ /** 这是带动画的方式,现在先使用不带动画的
+ var frame = 0;
+ var img = get_img("menu_bar.png");
+ var loop = function(){
+ return function(){
+ if(frame < 25){
+ ctx.clearRect(540,481,210,50);
+ ctx.restore();
+ for(var i = 0,x = 730-8*frame; i < arr2.length; i++){
+ if(arr2[i]){
+ ctx.drawImage(img,50*i,0,50,50,x+50*i,481,50,50);
+ }else{
+ ctx.drawImage(img,50*i,50,50,50,x+50*i,481,50,50);
+ }
+ }
+ frame++;
+
+ window.setTimeout(arguments.callee,40);
+ }
+ }
+ }
+ loop.call(this)(); */
+
+ var img = get_img("menu_bar.png");
+ for(var i=0, x=490; i<arr2.length; i++){
+ if(arr2[i]){
+ ctx.drawImage(img,60*i,0,60,60,x+60*i,475,60,60);
+ }else{
+ ctx.drawImage(img,60*i,60,60,60,x+60*i,475,60,60);
+ }
+ }
+}
+
+##register hide_menu: -> void
+##args()
+{
+ var ctx = document.getElementById("gmcanvas").getContext("2d");
+ ctx.clearRect(540,481,210,50);
+}
+
+var get_img = function(key){
+ var img = IMG_CACHE["/resources/"+key];
+ if (!img) {
+ throw new Error('Missing "' + key + '", preload() all images before trying to load them.');
+ }
+ return img;
+}
+
+##register play_sound: string -> void
+##args(key)
+{
+ var snd = AUD_CACHE[key];
+ if(snd){
+ snd.play();
+ }
+}
+
+//两个参数,一个是cookie的名子,一个是值
+##register set_cookie: string,string -> void
+##args(name,value)
+{
+ var Days = 30; //此 cookie 将被保存 30 天
+ var exp = new Date(); //new Date("December 31, 9998");
+ exp.setTime(exp.getTime() + Days*24*60*60*1000);
+ document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString();
+}
+
+//取cookies函数
+##register get_cookie: string -> string
+##args(name)
+{
+ var arr = document.cookie.match(new RegExp("(^| )"+name+"=([^;]*)(;|$)"));
+ if(arr != null) return unescape(arr[2]);
+ return "";
+}
+
+//删除cookie
+##register del_cookie: string -> void
+##args(name)
+{
+ var exp = new Date();
+ exp.setTime(exp.getTime() - 1);
+ var cval=getCookie(name);
+ if(cval!=null) document.cookie= name + "="+cval+";expires="+exp.toGMTString();
+}
View
BIN  resources/arrow.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/board.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/button.wav
Binary file not shown
View
BIN  resources/countfan.wav
Binary file not shown
View
BIN  resources/da.wav
Binary file not shown
View
BIN  resources/dialog.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/dragon.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/eg_jong.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/eg_kong.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/eg_pung.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/eg_shun.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/eswn.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/game_bg.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/home_bg.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
211 resources/how_to_play.html
@@ -0,0 +1,211 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>How to play</title>
+<style type="text/css">
+ body {
+ background:url("/resources/page_bg.jpg") repeat #1c2eff
+ }
+
+ #main {
+ width: 700px;
+ margin: 0px auto;
+ background:#FFF;
+ padding:1px;
+ }
+
+ #tile_bing {
+ position:absolute;
+ margin-top: 10px;
+ clip:rect(0px,360px,50px,0px);
+ }
+
+ #tile_tiao {
+ position:absolute;
+ clip:rect(50px,360px,100px,0px);
+ margin-top: -40px;
+ }
+
+ #tile_wan {
+ position:absolute;
+ clip:rect(100px,360px,150px,0px);
+ margin-top: -90px;
+ }
+
+ #tile_wan2 {
+ position:absolute;
+ clip:rect(150px,360px,200px,0px);
+ margin-top: -140px;
+ }
+
+ #act_pung {
+ position:absolute;
+ clip:rect(0px,60px,60px,0px);
+ }
+
+ #act_kong {
+ position:absolute;
+ clip:rect(0px,120px,60px,60px);
+ margin-left: -60px;
+ }
+
+ #act_hooo {
+ position:absolute;
+ clip:rect(0px,180px,60px,120px);
+ margin-left: -120px;
+ }
+ .rule {
+ height: 70px;
+}
+.action_name {
+ margin-left: 70px;
+ padding-top: 40px;
+}
+.action_expr {
+ margin-left: 70px;
+ background-color: #CCC;
+}
+
+ul {
+ margin-top: 10px;
+}
+.indent_1 {
+ margin-left:40px;
+}
+
+.margin_40 {
+ margin-top:40px;
+}
+
+.how_to_play {
+ background:#90C;
+ padding:5px;
+ font-size:xx-large;
+ text-align:center;
+}
+
+#content {
+ padding:5px;
+}
+</style>
+</head>
+<body>
+<div id="main">
+<div class="how_to_play">
+ How to paly Mahjong
+</div>
+<div id="content">
+<p><strong>1. Introduction</strong><br />
+ <strong>Mahjong (NOT Mahjong Solitaire)</strong> is a game that originated in China. It is widely played in China and some Asian areas. Mahjong is a game of skill, strategy and calculation and involves a certain degree of chance. </p>
+<p>There are many variants of Mahjong with different rules. To make it easy, this game use the most simple one. Any one who never touched Mahjong would learn to play it in one minutes. Besides, we redesigned some tiles to make it easy to recognize for people who don't know Chinese characters in Mahjong tiles.</p>
+<p><strong>2. Mahjong Set</strong></p>
+<p> Consit of 108 tiles in total and are divided into 3 suits:</p>
+<ul >
+ <li>Circles (Number 1 to 9, 4 tiles each)</li>
+ <img src="/resources/tiles.png" id="tile_bing"/>
+</ul>
+<p>&nbsp;</p>
+<ul class="margin_40">
+ <li>Bamboos(Number 1 to 9, 4 tiles each)</li>
+ <img src="/resources/tiles.png" id="tile_tiao"/>
+</ul>
+<p>&nbsp;</p>
+<ul class="margin_40">
+ <li>Thousands(Number 1 to 9, 4 tiles each)</li>
+ <ul>
+ <li>Classical: </li>
+ <img src="/resources/tiles.png" id="tile_wan"/>
+ </ul>
+</ul>
+<p>&nbsp;</p>
+<ul class="margin_40">
+ <ul>
+ <li>Redesigned: As classical thousand tiles contain Chinese characters that are hard for foreign people to recognize and remember. We redesign thousand tiles as follow </li>
+ <img src="/resources/tiles.png" id="tile_wan2"/>
+ </ul>
+</ul>
+<p>&nbsp;</p>
+<p>&nbsp;</p>
+<p><strong>3. How to win</strong></p>
+<p> Collect 4 sets of either &quot;Pung&quot;, &quot;Sheung&quot; or &quot;Kong&quot; and must have a pair of same tiles known as &quot;Jong&quot; to win, <font color="red"><strong>The &ldquo;Jong&rdquo; must have point of 2 or 5 or 8.</strong></font></p>
+<ul >
+ <li><strong>Pung</strong></a><strong> : </strong>Set of 3 identical tiles from any suits<strong> </strong></li>
+</ul>
+<p><img src="/resources/eg_pung.png" width="600" height="100" class="indent_1"/></p>
+<ul>
+ <li><strong>Sheung: </strong>A run of 3 tiles from same suit (9, 1, 2 is not permissible)<strong> </strong></li>
+</ul>
+<p><img src="/resources/eg_shun.png" width="600" height="100" class="indent_1" /></p>
+<ul>
+ <li><strong>Kong: </strong>4 identical tiles from any suits<strong> </strong></li>
+</ul>
+<p><img src="/resources/eg_kong.png" width="600" height="100" class="indent_1"/></p>
+<ul>
+ <li><strong>Jong: </strong>A pair of same tiles<strong>, must be of point 2, 5 or 8 </strong></li>
+</ul>
+<p><img src="/resources/eg_jong.png" width="600" height="100" class="indent_1"/></p>
+<p><strong>4. Game rules</strong></p>
+<p> The game will start when all four players are ready. </p>
+<ul>
+ <li><strong>Dealing</strong>: This is automatically done by game server. After dealing, the East ( first player who join the game ) has 14 tiles, the rest of the players have 13 tiles each. And it&rsquo;s turn for east to discard.</li>
+ <li><strong>Discard</strong>: The current player discard an unwanted tile face up into the middle, the tile may be useful for other players to make up a set. So other players may <strong>call</strong> this tile. If no one calls, the next player ( clockwise ) will pick up one tile from the Wall, and it&rsquo;s turn for him/her to discard.</li>
+ <li><strong>Call</strong>: If the unwanted tile is useful for one player to make up a set as in the following way:</li>
+ <ul>
+ <li class="rule">
+ <img src="/resources/menu_bar.png" id="act_pung"/>
+ <div class="action_name">Pung ( 3 identical tiles) </div>
+ </li>
+ </ul>
+</ul>
+<div class="action_expr">
+<p>If player has 2 identical tiles in hand which match the unwanted tile, &ldquo;Pung&rdquo; can be called.</p>
+</div>
+<ul >
+ <ul >
+ <li class="rule">
+ <img src="/resources/menu_bar.png" id="act_kong"/>
+ <div class="action_name">Kong ( 4 identical tiles) </div>
+ </li>
+ </ul>
+</ul>
+<div class="action_expr">
+<ul>
+ <li>If player has 3 identical tiles in hand which match the unwanted tile, &ldquo;Kong&rdquo; can be called. This is known as &ldquo;Soft Kong&rdquo;.</li>
+ <li> If player picks up an identical tile from the Wall that match the other 3 tiles in hand, &ldquo;Kong&rdquo; can be called. This is known as &ldquo;Hard Kong&rdquo;.</li>
+</ul>
+<p>Note: when &quot;Kong&quot; is shouted, player should pick up another tile from the tail of Wall (This is automatically done by game server).</p>
+</div>
+<ul >
+ <ul >
+ <li class="rule">
+ <img src="/resources/menu_bar.png" id="act_hooo"/>
+ <div class="action_name"> Hooo ( win the game)</div>
+ </li>
+ </ul>
+</ul>
+<div class="action_expr">
+ <ul>
+ <li>If player needs the unwanted tile to win, &ldquo;Hooo&rdquo; can be called and we say the player who discard the unwanted tile made a &quot;Fire&quot;.</li>
+ <li> If player picks up a tile and meet a win set, &ldquo;Hooo&rdquo; can be called. This is known as &quot;Self Pick&quot;.</li>
+ </ul>
+</div>
+<p><strong>5. Scores </strong></p>
+<p>Scores are automatically calculated by server, the rules are as follow:</p>
+<ol>
+ <li> If no one wins after tiles are used out, this round is draw, players&rsquo; score stay unchange. </li>
+ <li>If a player wins not by &quot;Self Pick&quot;, the player who made a &quot;Fire&quot; will give the winner 5 points. Note that winners may be more than one if serveral players need the same tile to win at the same time.</li>
+ <li>If a player wins by &quot;Self Pick&quot;, all three other players are losers and must give the him/her 5 points each.</li>
+ <li>If a player made a &quot;Soft Kong&quot;, the player who help him/her made this must give 5 points to him/her.</li>
+ <li>If a player made a &quot;Hard Kong&quot;, all three other players must give him/her 5 points each.</li>
+</ol>
+<p><strong>6. Tips</strong></p>
+<ul>
+ <li><strong>Show numbers on tile: </strong>If you want to show numbers on tiles, you can check the checkbox &quot;show number&quot; on top right corner.</li>
+ <li><strong>Show classical tiles:</strong> If you can recognize Chinese characters, you can check the checkbox &quot;classic tiles&quot; on top right corner.</li>
+</ul>
+<p><strong>Enjoy playing Mahjong now. </strong></p>
+</div>
+</div>
+</body>
+</html>
View
BIN  resources/loading.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/login_bg.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
120 resources/main.css
@@ -0,0 +1,120 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+div {
+ display: block;
+}
+
+body,html {
+margin: 0;
+padding: 0;
+font: 12px/1.5 arial;
+height:100%;
+}
+
+body {
+ background: #0c1e28;
+ color: #DDD;
+ font-size: 13px;
+ line-height: 120%;
+ text-align: center;
+ min-height: 580px0;
+ display: block;
+}
+
+input {
+ border: 0 none;
+ border-radius: 5px 5px 5px 5px;
+ box-shadow: 0 3px 5px #999999 inset;
+ font-size: 20px;
+ margin: 2px;
+ padding: 2px;
+}
+
+button {
+ background: -moz-linear-gradient(-90deg, #FCEE21, #B96E17) repeat scroll 0 0 transparent;
+ border-color: #333333;
+ border-radius: 0.3em 0.3em 0.3em 0.3em;
+ border-style: solid;
+ border-width: 1px;
+ box-shadow: 0 0 5px black, 0 25px 5px rgba(255, 255, 255, 0.1) inset;
+ color: #5D370C;
+ cursor: pointer;
+ display: inline-block;
+ font-size: 18px;
+ margin: 1px 2px;
+ padding: 10px 20px;
+ text-align: center;
+ text-decoration: none;
+ text-shadow: 0 1px 0 #FCEE21;
+ z-index: 100;
+}
+
+#container {
+ position: relative;
+ min-height:100%;
+}
+
+#content {
+ padding: 10px;
+ padding-bottom: 60px; /*(20px font-size x 2 line-height) + (2 * 10px padding) = 60px*/
+ min-height: 600px;
+ background: url("/resources/home_bg.jpg") no-repeat scroll center top #0c1e28;
+}
+
+#footer {
+ position: absolute;
+ bottom: 0;
+ padding: 10px 0;
+ background-color: #AAA;
+ width: 100%;
+}
+
+#login_box {
+ margin: 0 auto;
+ position: relative;
+ width:340px;
+ height: 230px;
+ top: 350px;
+ color: #ebebeb;
+ font: 12px Arial, Helvetica, sans-serif;
+ background: url("/resources/login_bg.png") no-repeat left top;
+}
+#login_box h1 {
+ text-align: left;
+ font-size: 24px;
+ padding-left: 10px;
+}
+
+#login_box input {
+ font-size: 24px;
+ margin: 5px auto;
+ width: 210px;
+ height: 32px;
+}
+
+#login_box button {
+ width: 170px;
+}
+
+#login_box p {
+ padding: 0px 10px;
+ color: orange;
+ text-align: left;
+}
+
+
+.how_to_play {
+ font-size: x-large;
+ margin: 0 auto;
+ position: relative;
+ top: 440px;
+}
+
+.notice {
+ position: relative;
+ top: 490px;
+ color: orange;
+}
View
BIN  resources/menu_bar.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/numbers.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/offline.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
61 resources/page.css
@@ -0,0 +1,61 @@
+* {
+ margin: 0px;
+ padding: 0px;
+}
+
+body {
+ background: url("/resources/page_bg.jpg") repeat #0c1e28;
+}
+
+.dragon_bg {
+ background: url("/resources/dragon.jpg");
+ margin: 0px auto;
+ width: 500px;
+ height: 200px;
+}
+
+#game_list {
+ background: #0c1e28;
+ width: 500px;
+ margin: 0px auto;
+ height: 460px;
+ border: 3px solid red;
+}
+
+.tb_game_list {
+ background: #d7d7d7;
+ width: 480px;
+ border: 1px solid black;
+}
+
+.game_list_pannel {
+ padding: 5px;
+ margin: 40px 5px 0;
+ padding: 10px 5px 0;
+ background: #fff;
+ height: 400px;
+}
+
+.title {
+ float: left;
+ margin: 5px;
+ color: white;
+}
+
+.quick-start {
+ float: right;
+ margin: 6px 10px 0 0;
+}
+
+.tb_game_list th, .tb_game_list td {
+ border-right: 1px solid black;
+ border-bottom: 1px solid black;
+ line-height: 18px;
+ padding: 4px;
+ text-align: left;
+ vertical-align: middle;
+}
+
+input {
+ margin-bottom: 0px;
+}
View
BIN  resources/page_bg.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/player_frame_h.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/player_frame_v.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/portrait.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/result.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/start.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/start.wav
Binary file not shown
View
153 resources/style.css
@@ -0,0 +1,153 @@
+* {
+ margin: 0
+ padding: 0
+}
+
+body {
+ background: url("/resources/page_bg.jpg") repeat scroll center top #0c1e28;
+ color: #DDD;
+ font-size: 13px;
+ line-height: 120%;
+ text-align: center;
+ height: 100%;
+ min-height: 640px;
+}
+
+
+.text {
+ /** background: url("/resources/input_field_left.png") top left no-repeat;
+ padding-left: 6px;
+ display: block;
+ height: 50px; */
+
+}
+
+#gmloader {
+ background: url("/resources/loading.gif") center center no-repeat;
+ position: absolute;
+ height: 620px;
+ width: 740px;
+}
+
+#loading_info {
+ position: relative;
+ top: 350px;
+}
+
+.game {
+ background: url("/resources/game_bg.png") top left no-repeat;
+ margin: 5px auto;
+ width: 990px;
+ height: 645px;
+ border-color: #333333;
+ border-radius: 0.3em 0.3em 0.3em 0.3em;
+ border-style: solid;
+ border-width: 2px;
+ box-shadow: 0 0 5px black, 0 25px 5px rgba(255, 255, 255, 0.1) inset;
+}
+
+.game .canvas {
+ margin: 10px;
+ float: left;
+}
+
+.game #gameinfo{
+ width: 230px;
+ height: 100%;
+ float: right;
+}
+
+.game #players {
+ margin-left: 8px;
+ width: 210px;
+ height: 100px;
+}
+
+.game #tb_players td {
+ max-width: 160px;
+ overflow: hidden;
+ height: 23px;
+ text-align: left;
+}
+
+.game #tb_players .self_row {
+ border: 1px;
+ background-color: red;
+ font-size: 14px;
+}
+
+.game .chat{
+ height: 410px
+}
+
+#chat_messages {
+ clear: both;
+ border: 0px;
+ height: 360px;
+ width: 207px;
+ margin: 20px 0px 8px 10px;
+ color: #ffebc7;
+ overflow-y: scroll;
+ text-shadow: black 1px 1px 1px;
+}
+
+#chat_messages li {
+ list-style: none;
+ word-wrap: break-word;
+ text-align: left;
+ margin: 3px 0px;
+ font-size: 13px;
+}
+
+#chat_messages .author {
+ clear: both;
+ float: left;
+ font-size: 12px;
+ font-weight: bold;
+ margin-right: 5px;
+}
+
+
+.game .chat .input {
+ width: 93%;
+ height: 40px;
+ margin: 0 5px;
+}
+
+.game .chat #entry {
+ float: left;
+ width: 150px;
+}
+
+.game .chat #post {
+ float: right;
+}
+
+#panel {
+ height: 40px;
+ margin-top: 3px;
+ text-align: left;
+}
+
+#scores {
+ height: 68px;
+ padding: 5px 10px;
+}
+
+.score_left {
+ float: left;
+ max-width: 105px;
+ overflow: hidden;
+ height: 35px;
+}
+
+.score_middle {
+ float: left;
+ padding-left: 3px;
+ padding-top: 14px;
+}
+
+.score_right {
+ float: right;
+ margin-right: 3px;
+}
View
BIN  resources/table_bg.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/tiles.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/tiles_small.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  resources/ting.png
Diff not rendered
View
BIN  resources/tray.wav
Binary file not shown
View
BIN  resources/win.wav
Binary file not shown
View
BIN  resources/yellow_bar.png
Diff not rendered
View
666 src/board.opa
@@ -0,0 +1,666 @@
+/*************************************************************************
+ * Mahjong: An html5 mahjong game built with opa.
+ * Copyright (C) 2012
+ * Author: winbomb
+ * Email: li.wenbo@whu.edu.cn
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ ************************************************************************/
+package mahjong
+
+/** 一对牌 */
+type Pair.t = {
+ Card.t card1,
+ Card.t card2
+}
+
+/** 一游牌 */
+type Group.t = {
+ Card.t card1,
+ Card.t card2,
+ Card.t card3
+}
+
+/**
+* 牌面的定义
+*/
+type Board.t = {
+ int start_pos, //开始起牌的位置
+ int curr_pos, //现在牌起到的位置
+ list(Card.t) card_pile, //牌堆
+ llarray(Discard.t) discards, //玩家的弃牌,依次为东/南/西/北
+ llarray(Deck.t) decks, //玩家的牌,依次为东/南/西/北
+ llarray(llarray(int)) pile_info, //玩家面前牌堆的情况
+}
+
+module Board {
+
+ PILE_BASE = [0,28,54,82]
+ DEBUG = {false}
+
+ /** 将的定义 */
+ GENERALS = [{point:2,suit:{Wan}},{point:5,suit: {Wan}},{point:8,suit: {Wan}},
+ {point:2,suit:{Tiao}},{point:5,suit:{Tiao}},{point:8,suit:{Tiao}},
+ {point:2,suit:{Bing}},{point:5,suit:{Bing}},{point:8,suit:{Bing}}]
+
+ /**
+ * 创建一个空的牌面
+ */
+ function Board.t create(){
+ suits = [{Bing},{Tiao},{Wan}];
+ points = [1,2,3,4,5,6,7,8,9];
+
+ //创建了一个空的108张牌
+ card_pile = List.foldi(function(i,suit,card_pile){
+ base_point = i*36
+ List.fold(function(point,card_pile){
+ id = base_point + (point - 1) * 4;
+ card_pile = {~point,~suit,id:id+1} +> card_pile;
+ card_pile = {~point,~suit,id:id+2} +> card_pile;
+ card_pile = {~point,~suit,id:id+3} +> card_pile;
+ {~point,~suit,id:id+4} +> card_pile;
+ },points,card_pile);
+ },suits,[]);
+
+ card_pile = shuffle(card_pile,100);
+
+ {start_pos: 0,
+ curr_pos: 0,
+ card_pile: card_pile,
+ discards: LowLevelArray.create(4,[]),
+ decks: LowLevelArray.create(4,Card.EMPTY_DECK),
+ pile_info: init_pile()
+ }
+ }
+
+ function init_pile(){
+ LowLevelArray.init(4)(function(n){
+ len = if(n == 0 || n == 2) 14 else 13
+ LowLevelArray.create(len,2);
+ });
+ }
+
+ /**
+ * 获得某个玩家的牌
+ * @place Place.t
+ */
+ function get_player_deck(board,player){
+ if(player.idx >= 0 && player.idx <= 3) LowLevelArray.get(board.decks,player.idx) else Card.EMPTY_DECK
+ }
+
+ /**
+ * 准备牌面
+ */
+ function prepare(board){
+ //先随机选一个开牌的点
+ //rand = Random.int(54) * 2;
+ rand = 0
+ board = {{board with start_pos: rand} with curr_pos: rand}
+
+ //每个人发13张牌,庄家发14张
+ if(not(DEBUG)){
+ board = deal_round_n(board,13);
+ deal_card(board,0) |> sort(_);
+ }else{
+ TestUtil.prepare(board) |> sort(_)
+ }
+ }
+
+ /**
+ * 判断一副牌能否胡
+ */
+ function can_hoo_card(deck,card){
+ cards = List.add(card,deck.handcards);
+ can_hoo(cards)
+ }
+
+ /**
+ * 判断是否听牌
+ */
+ both function is_ting(cards){
+ //遍历27张牌,还能不能胡,能胡的话就听了
+ points = [1,2,3,4,5,6,7,8,9]
+ suits = [{Bing},{Tiao},{Wan}]
+ List.fold(function(point,is_ting){
+ if(is_ting) {true} else {
+ List.fold(function(suit,is_ting){
+ if(is_ting) {true} else {
+ can_hoo({id: -1, ~point, ~suit} +> cards);
+ }
+ },suits,is_ting);
+ }
+ },points,{false});
+ }
+
+ /**
+ * 判断能否碰
+ * 如果自己牌面内有两张和card一样的牌,则返回true
+ */
+ function can_peng(deck,card){
+ find_card_count(deck,card) >= 2
+ }
+
+ /**
+ * 判断能否杠(杠别人打出的牌,不包括自己摸到牌后的动作)
+ */
+ function can_gang(deck,card){
+ find_card_count(deck,card) >= 3
+ }
+
+ /** 判断自己摸到牌后是否能杠 */
+ function can_gang_self(deck){
+ //检查自己的手牌中是否有跟已经做成的牌组成杠的机会
+ can_gang = List.fold(function(pattern,can_gang){
+ if(can_gang) can_gang else {
+ match(pattern.kind){
+ case {Peng}: {
+ card = List.head(pattern.cards);
+ List.exists(function(c){c.point == card.point && c.suit == card.suit},deck.handcards);
+ }
+ default: {false}
+ }
+ }
+ },deck.donecards,{false});
+
+ //检查自己的手牌中是否有4张一样的牌
+ if(can_gang) {true} else {
+ List.fold(function(card,self_gang){
+ if(self_gang) self_gang else {
+ //检查这张牌在牌堆中有几张
+ cards = List.filter(function(c){c.point == card.point && c.suit == card.suit},deck.handcards);
+ List.length(cards) == 4
+ }
+ },deck.handcards,{false});
+ }
+ }
+
+ /** 是否自摸 */
+ function can_hoo_self(deck){
+ can_hoo(deck.handcards);
+ }
+
+ /** 检查一副牌的手牌能否胡牌 */
+ function can_hoo(handcards){
+ //首先检查手牌中是否存在 2/5/8 的将
+ if(not(has_general(handcards))) {false} else {
+ //找到将
+ generals = find_general(handcards);
+ List.fold(function(general,can_hoo){
+ if(can_hoo) can_hoo else {
+ //cards为去掉两张将牌之后的牌
+ cards = filter_pair(handcards,general);
+
+ //检查去掉两张将牌之后的牌是否符合胡的模式
+ //wans,tiaos,bings本别表示万/条/饼的个数,它们必须为3的倍数才能胡(不胡7对)
+ wans = List.filter(function(c){c.suit == {Wan}},cards);
+ tiaos = List.filter(function(c){c.suit == {Tiao}},cards);
+ bings = List.filter(function(c){c.suit == {Bing}},cards);
+ if(mod(List.length(wans),3) == 0
+ && mod(List.length(tiaos),3) == 0 && mod(List.length(bings),3) == 0){
+ match_pattern(wans) && match_pattern(tiaos) && match_pattern(bings)
+ }else {false}
+ }
+ },generals,{false});
+ }
+ }
+
+ /** 检查一组牌(不包括将牌和已经成了的牌)是否符合胡的模式 */
+ function match_pattern(cards){
+ if(List.length(cards) == 0) {true} else {
+ cards = List.sort_with(Card.compare,cards);
+ min_card = List.head(cards);
+ match(find_xxx(min_card,cards)){
+ case ~{some}: match_pattern(filter_group(cards,some))
+ case {none}: {
+ match(find_xyz(min_card,cards)){
+ case ~{some}: match_pattern(filter_group(cards,some))
+ case {none}: {false}
+ }
+ }
+ }
+ }
+ }
+
+ /** 从一组牌中找出一对将牌 */
+ function find_general(cards){
+ List.fold(function(general,result){
+ pair = List.fold(function(c,pair){
+ if(List.length(pair) == 2) pair else {
+ if(c.point == general.point && c.suit == general.suit) List.add(c,pair) else pair
+ }
+ },cards,[])
+
+ if(List.length(pair) != 2) result else {
+ {card1: Option.get(List.get(0,pair)), card2: Option.get(List.get(1,pair))} +> result
+ }
+ },GENERALS,[]);
+ }
+
+ /** 从一组牌cards中找到以card为底的xxx的模式 例如:3万/3万/3万 */
+ function find_xxx(card,cards){
+ result = List.fold(function(c,result){
+ if(c.suit == card.suit && c.point == card.point){
+ c +> result
+ } else result
+ },cards,[]);
+
+ if(List.length(result) <= 2) {none} else {
+ group = {card1: Option.get(List.get(0,result)),
+ card2: Option.get(List.get(1,result)),
+ card3: Option.get(List.get(2,result))}
+ some(group);
+ }
+ }
+
+ /** 从一组牌cards中找到以card为底的xyz的模式, 例如: 3万/4万/5万 */
+ function find_xyz(card,cards){
+ card1 = card
+ card2 = List.find(function(c){c.suit == card.suit && c.point == card.point + 1},cards);
+ card3 = List.find(function(c){c.suit == card.suit && c.point == card.point + 2},cards);
+
+ match((card2,card3)){
+ case ({some:card2},{some:card3}): some(~{card1,card2,card3})
+ default: {none}
+ }
+ }
+
+ /**
+ * 找到一副手牌中可以杠的选择,返回一个pattern的数组
+ */
+ function find_gangs(deck){
+ //首先找到可以与已碰牌组成杠的机会
+ gangs = List.fold(function(done,gangs){
+ if(done.kind != {Peng}) gangs else {
+ //如果手牌中存在能和已经碰的牌形成杠的牌的话
+ gang_card = List.head(done.cards);
+ card = List.find(function(c){
+ c.point == gang_card.point && c.suit == gang_card.suit
+ },deck.handcards)
+
+ match(card){
+ case {none}: gangs
+ case ~{some}: {
+ pattern = {
+ kind: {Soft_Gang},
+ cards: some +> done.cards,
+ source: done.source
+ }
+ pattern +> gangs
+ }
+ }
+ }
+ },deck.donecards,[]);
+
+ //然后找到手牌中的杠
+ List.fold(function(card,gangs){
+ cards = List.filter(function(c){
+ c.point == card.point && c.suit == card.suit && c.id >= card.id
+ },deck.handcards);
+
+ if(List.length(cards) != 4) gangs else {
+ pattern = {
+ kind: {Hard_Gang},
+ cards: cards,
+ source: -1
+ }
+ pattern +> gangs
+ }
+ },deck.handcards,gangs);
+ }
+
+ private function filter_group(cards,group){
+ List.filter(function(c){
+ c.id != group.card1.id && c.id != group.card2.id && c.id != group.card3.id
+ },cards);
+ }
+
+ private function filter_pair(cards,pair){
+ List.filter(function(c){
+ c.id != pair.card1.id && c.id != pair.card2.id
+ },cards);
+ }
+
+ function has_general(cards){
+ List.fold(function(general,has_general){
+ if(has_general) has_general else {
+ count = List.fold(function(c,count){
+ if(c.point == general.point && c.suit == general.suit) count + 1 else count
+ },cards,0);
+ count >= 2
+ }
+ },GENERALS,{false});
+ }
+
+ function find_card_count(deck,card){
+ List.fold(function(c,count){
+ if(c.point == card.point && c.suit == card.suit) count + 1 else count
+ },deck.handcards,0);
+ }
+
+ /**
+ * 发n圈牌
+ */
+ function deal_round_n(board,n){
+ match(n){
+ case 0: board;
+ case x: {
+ board = deal_round(board);
+ deal_round_n(board,x-1);
+ }
+ }
+ }
+
+ /**
+ * 发一圈牌
+ */
+ function Board.t deal_round(Board.t board){
+ LowLevelArray.foldi(function(i,_,board){
+ deal_card(board,i);
+ },board.decks,board);
+ }
+
+ /**
+ * 给某个方位的玩家发一张牌
+ */
+ function deal_card(board,idx){
+ deck = LowLevelArray.get(board.decks,idx);
+
+ card = List.head(board.card_pile);
+ handcards = List.rev(deck.handcards) |> List.add(card,_) |> List.rev(_); //把这张牌添加到牌尾
+ deck = {deck with handcards: handcards}
+ LowLevelArray.set(board.decks,idx,deck);
+
+ board = {board with pile_info: update_pile(board.pile_info,board.curr_pos)}
+ board = {board with curr_pos: mod(board.curr_pos + 1,108)}
+ {board with card_pile: List.drop(1,board.card_pile)}
+ }
+
+ /**
+ * 从杠头取一张牌给某个玩家
+ */
+ function deal_card_backwards(board,idx){
+ deck = LowLevelArray.get(board.decks,idx);
+
+ n = List.length(board.card_pile);
+ card = Option.get(List.get(n-1,board.card_pile));
+ handcards = List.rev(deck.handcards) |> List.add(card,_) |> List.rev(_);
+ deck = {deck with handcards: handcards}
+ LowLevelArray.set(board.decks,idx,deck);
+
+ board = {board with pile_info: update_pile(board.pile_info,mod(board.curr_pos + n,108))}
+ {board with card_pile: List.remove(card,board.card_pile)}
+ }
+
+ function update_pile(pile_info,pos){
+ //首先获得要更新谁的牌面
+ idx = List.fold(function(n,idx){
+ if(pos >= n) idx + 1 else idx
+ },PILE_BASE,-1);
+
+ pile_num = (pos - Option.get(List.get(idx,PILE_BASE))) / 2;
+
+ info = LowLevelArray.get(pile_info,idx);
+ new_val = LowLevelArray.get(info,pile_num) - 1;
+ LowLevelArray.set(info,pile_num,new_val);
+
+ pile_info
+ }
+
+ /**
+ * 从手牌中弃掉一张牌
+ */
+ server function discard(board,idx,card){
+ deck = discard_card(LowLevelArray.get(board.decks,idx),card)
+ LowLevelArray.set(board.decks,idx,deck);
+ board
+ }
+
+ /**
+ * 将弃牌加入到玩家的弃牌区
+ */
+ server function add_to_discards(board,idx,card){
+ discard = LowLevelArray.get(board.discards,idx);
+ LowLevelArray.set(board.discards,idx,card +> discard);
+ board
+ }
+
+ /**
+ * 从Deck中弃掉一张牌,如果牌不在deck中,则返回原来的deck
+ */
+ both function discard_card(deck,card){
+ handcards = List.filter(function(c){c.id != card.id},deck.handcards);
+ if(List.length(handcards) == List.length(deck.handcards)){
+ Log.warning("Board","Try to discard an unexist card {card} from deck {deck.handcards}");
+ }
+ {deck with handcards: List.sort_with(Card.compare,handcards)}
+ }
+
+ /**
+ * 玩家碰牌
+ */
+ function peng(game,idx,card){
+ deck = LowLevelArray.get(game.board.decks,idx);
+ peng_cards = List.fold(function(c,peng_cards){
+ if(List.length(peng_cards) == 3) peng_cards else {
+ if(c.point == card.point && c.suit == card.suit){
+ c +> peng_cards
+ }else peng_cards
+ }
+ },deck.handcards,[card]);
+
+ pattern = create_pattern({Peng},peng_cards,game.curr_turn);
+ deck = {deck with donecards: pattern +> deck.donecards}
+
+ handcards = List.filter(function(c){
+ not(List.exists(function(d){d.id == c.id},peng_cards));
+ },deck.handcards);
+ deck = {deck with handcards: handcards}
+
+ LowLevelArray.set(game.board.decks,idx,deck)
+ game.board
+ }
+
+ function gang(game,idx,card){
+ deck = LowLevelArray.get(game.board.decks,idx);
+ gang_cards = List.fold(function(c,gang_cards){
+ if(List.length(gang_cards) == 4) gang_cards else {
+ if(c.point == card.point && c.suit == card.suit){
+ c +> gang_cards
+ }else gang_cards
+ }
+ },deck.handcards,[card]);
+
+ pattern = create_pattern({Soft_Gang},gang_cards,game.curr_turn);
+ deck = {deck with donecards: pattern +> deck.donecards}
+
+ handcards = List.filter(function(c){
+ not(List.exists(function(d){d.id == c.id},gang_cards));
+ },deck.handcards);
+
+ //TODO:从杠头上取一张加到handcards中
+ deck = {deck with handcards: handcards}
+ LowLevelArray.set(game.board.decks,idx,deck)
+ deal_card_backwards(game.board,idx);
+ }
+
+ function create_pattern(kind,cards,source){
+ ~{kind,cards,source}
+ }
+
+ /**
+ * 打乱牌的顺序
+ */
+ function list(Card.t) shuffle(list(Card.t) l,int n)
+ {
+ match(n){
+ case 0: l
+ case x: {
+ l = s(l);
+ shuffle(l,x-1);
+ }
+ }
+ }
+
+ private function list s(list l){
+ head = List.head(l);
+ l = List.drop(1,l);
+ int r = Random.int(List.length(l)+1);
+ List.insert_at(head,r,l);
+ }
+
+ /**
+ * 排序某个牌面所有玩家的手牌
+ */
+ function sort(board){
+ LowLevelArray.foldi(function(i,deck,board){
+ deck = {deck with handcards: List.sort_with(Card.compare,deck.handcards)}
+ LowLevelArray.set(board.decks,i,deck);
+ board
+ },board.decks,board);
+ }
+
+ /** 获得4个玩家以作成的牌 */
+ /** function get_donecards(board){
+ LowLevelArray.init(4)(function(n){
+ deck = LowLevelArray.get(board.decks,n);
+ deck.donecards
+ });
+ }*/
+
+ /**
+ * 获得
+ */
+ function get_decks(board,flag){
+ LowLevelArray.init(4)(function(n){
+ deck = LowLevelArray.get(board.decks,n);
+ if(flag == {true}) deck else{
+ {deck with handcards: []}
+ }
+ });
+ }
+
+ /**
+ * 获得前一个玩家的坐标
+ */
+ function get_prev_idx(idx){
+ mod(idx + 3,4)
+ }
+
+ /**
+ * 获得下一个玩家的坐标
+ */
+ function get_next_idx(idx){
+ mod(idx + 1,4)
+ }
+
+ /**
+ * 获得对家的坐标
+ */
+ function get_oppt_idx(idx){
+ mod(idx + 2,4)
+ }
+
+ /**
+ * 获得tg_player相对与src_player的位置
+ * 0:自己 1:左边 2:对家 3:右边
+ */
+ function get_rel_pos(src_idx,tg_idx){
+ mod(src_idx - tg_idx + 4,4)
+ }
+
+ function idx_to_place(idx){
+ match(idx){
+ case 0: "East"
+ case 1: "South"
+ case 2: "West"
+ case 3: "North"
+ case _: "Unset"
+ }
+ }
+}
+
+
+/**
+* 花色的定义
+*/
+type Suit.t = {Bing} //饼
+ or {Tiao} //条
+ or {Wan} //万
+
+/**
+* 麻将牌的定义
+*/
+type Card.t = {
+ int point, //点数:1 ~ 9
+ Suit.t suit, //花色
+ int id, //编号,用于排序
+}
+
+/**
+* 可以成为一游的模式的定义
+*/
+type Pattern.kind = {Straight} //顺, 例如 3/4/5万
+ or {Peng} //三张,例如 三个3万
+ or {Soft_Gang} //明杠
+ or {Hard_Gang} //暗杠
+
+type Pattern.t = {
+ Pattern.kind kind, //所属的模式
+ list(Card.t) cards, //牌
+ int source, //牌的来源(即是碰或杠哪家的牌)
+}
+
+/** 弃牌类型定义:就是一组Card.t */
+type Discard.t = list(Card.t)
+
+/** 已成牌的类型定义 */
+type Donecard.t = list(Pattern.t)
+
+/** 手牌的类型定义:也是一组Card.t */
+type Handcard.t = list(Card.t)
+
+/**
+* 玩家手中所拥有的一组牌的定义
+*/
+type Deck.t = {
+ Handcard.t handcards, //手牌
+ Donecard.t donecards //已经成了的牌
+}
+
+/**
+* 麻将牌模块
+*/
+module Card {
+ /** 空的手牌定义 */
+ EMPTY_DECK = {handcards:[],donecards:[]}
+
+ function compare(card1,card2){
+ if(card1.id != -1 && card2.id != -1){
+ if(card1.id >= card2.id) {gt} else {lt}
+ } else {
+ if(card1.point >= card2.point) {gt} else {lt}
+ }
+ }
+
+ function to_string(card){
+ point = "{card.point}"
+ suit = match(card.suit){
+ case {Wan}: "W"
+ case {Tiao}: "T"
+ case {Bing}: "B"
+ }
+ point ^ suit ^ "#{card.id}"
+ }
+}
View
739 src/game.opa
@@ -0,0 +1,739 @@
+/*************************************************************************
+ * Mahjong: An html5 mahjong game built with opa.
+ * Copyright (C) 2012
+ * Author: winbomb
+ * Email: li.wenbo@whu.edu.cn
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ ************************************************************************/
+package mahjong
+import-plugin engine2d
+
+type Action.t = {no_act} //不进行动作
+ or {peng} //碰
+ or {gang} //杠
+ or {gang_self} //自己杠
+ or {hoo} //胡
+ or {Card.t discard} //弃牌
+ or {set_ready} //准备好
+ or {set_ok} //关闭结算界面
+ or {quit} //离开游戏
+ or {none} //尚未选择
+
+type Game.t = {
+ string id, //游戏id
+ Status.t status, //游戏状态
+ int curr_turn, //当前玩家的位置
+ int ready_flags, //用于表示玩家是否准备好(1 + 2 + 4 + 8)
+ int ok_flags, //用户表示玩家是否关闭了结算界面
+ int last_act, //最后一次出手时间,用于判断超时
+ bool change_flag, //标志这个游戏自上次广播之后状态(游戏人数,准备状态等)是否变化
+ list(int) winners, //游戏胜利玩家
+ option(Card.t) curr_card, //当前牌面上打出的牌
+ llarray(option(Player.t)) players, //游戏玩家
+ llarray(option(ThreadContext.client)) clients,
+ llarray(Action.t) actions, //玩家回合内的动作
+ Board.t board, //牌面的情况
+ Network.network(Game_msg.t) game_channel,
+ Network.network(Chat_msg.t) chat_channel
+}
+
+/**
+* 游戏信息的定义
+* 即在游戏大厅显示游戏列表时的信息
+*/
+type Game.info = {
+ string id, //游戏Id
+ bool in_progress, //游戏是否在进行中
+ int player_cnt, //玩家个数
+ int ready_cnt, //准备好的个数
+}
+
+type Game.ctx = {
+ string game_id, //游戏名称
+ Player.t player, //玩家名称
+ Network.network(Game_msg.t) game_channel, //游戏通道
+ Network.network(Chat_msg.t) chat_channel //聊天通道
+};
+
+set_cookie = %%engine2d.set_cookie%%
+get_cookie = %%engine2d.get_cookie%%
+
+DEFAULT_COINS = 1000; //默认的金币数量
+ALL_IS_READY = 15; //表示所有玩家都准备好了(1+2+4+8)
+ALL_IS_OK = 15;
+FLAGS = [8,4,2,1];
+
+public server gmMap = ServerReference.create(stringmap(Game.t) StringMap_empty);
+
+/** 测试第idx个玩家的某个flag是否为真 */
+public function test_flag(int flags,int idx){
+ if(flags <= 0 || flags >= 16) {false} else{
+ flag = List.foldi(function(i,f,flags){
+ if(flags >= f && i != 3-idx){
+ flags - f
+ }else flags
+ },FLAGS,flags);
+ flag != 0
+ }
+}
+
+/** 设置第idx个玩家的标志 */
+public function set_flag(int flags,int idx){
+ if(test_flag(flags,idx) || idx <= -1 || idx >= 4) flags else {
+ flags + Option.get(List.get(3-idx,FLAGS));
+ }
+}
+
+public function clear_flag(int flags,int idx){
+ if(not(test_flag(flags,idx)) || idx <= -1 || idx >= 4) flags else {
+ flags - Option.get(List.get(3-idx,FLAGS));
+ }
+}
+
+/** */
+public function get_flag_cnt(flags){
+ result = List.fold(function(f,r){
+ if(r.flags >= f) {r with flags: r.flags - f , cnt: r.cnt + 1} else r
+ },FLAGS,{~flags,cnt:0});
+ result.cnt;
+}
+
+module Game {
+
+ init_board = {
+ //初始化创建10个房间
+ ignore(for(0,function(i){
+ id = "game_{i}"
+ game = {
+ id: id,
+ status: {prepare},
+ winners: [],
+ curr_turn: 0,
+ last_act: 0,
+ ready_flags: 0,
+ ok_flags: 0,
+ change_flag: {false},
+ curr_card: {none},
+ players: LowLevelArray.create(4,{none}),
+ clients: LowLevelArray.create(4,{none}),
+ actions: LowLevelArray.create(4,{none}),
+ board: Board.create(),
+ game_channel: GameNetwork.memo(id),
+ chat_channel: ChatNetwork.memo(id)
+ };
+
+ ServerReference.update(gmMap,function(map){
+ StringMap_add(game.id,game,map)
+ });
+
+ i+1
+ }, _ <= 9))
+
+ //启动超时检查线程
+ Scheduler.timer(2000,function(){
+ timestamp = Date.in_milliseconds(Date.now());
+ Map.iter(function(_,game){
+ if(timestamp - game.last_act >= 12000){
+ match(game.status){
+ case {select_action}: Mahjong.default_action(game);
+ case {wait_for_resp}: Mahjong.do_action(game);
+ default: void
+ }
+ }
+ },ServerReference.get(gmMap));
+ });
+
+ //每隔2秒向大厅广播本次的游戏玩家变动
+ Scheduler.timer(2000,function(){
+ result = Map.fold(function(_,game,result){
+ if(game.change_flag){
+ game_info = {id:game.id,
+ rc: get_flag_cnt(game.ready_flags),
+ tc: get_player_cnt(game.players),
+ st: game.status != {prepare} && game.status != {game_over}
+ }
+ {msg: game_info} +> result
+ }else{ {unchanged} +> result}
+ },ServerReference.get(gmMap),[]);
+
+ //清除所有change_flag标志
+ ServerReference.update(gmMap,function(map){
+ Map.map(function(g){
+ {g with change_flag: {false}}
+ },map);
+ });
+
+ //只要有变化,就发送广播消息到大厅
+ b_changed = List.fold(function(r,b){
+ if(b) b else {
+ match(r){
+ case {unchanged}: {false}
+ case {msg:_}: {true}
+ }
+ }
+ },result,{false});
+ if(b_changed) Network.broadcast(result,hall);
+ });
+ }
+
+ /**
+ * 根据游戏的id获得游戏
+ */
+ function get(game_id){
+ Map.get(game_id,ServerReference.get(gmMap));
+ }
+
+ function with_game(game_id,(Game.t -> void) f){
+ match(get(game_id)){
+ case {none}: void
+ case ~{some}: f(some)
+ }
+ }
+
+ /** 获得可以加入的游戏的id(未开始,人数少于4) */
+ exposed function get_free_gameid(){
+ game_opt = Map.find(function(_,game){
+ if(game.status != {prepare} && game.status != {game_over}) {false} else {
+ if(get_player_cnt(game.players) >= 4) {false} else {true}
+ }
+ },ServerReference.get(gmMap));
+
+ match(game_opt){
+ case {none}: {none}
+ case {some:s}: some(s.val.id)
+ }
+ }
+
+ /** 获取游戏信息列表 */
+ public exposed function get_game_list(){
+ Map.fold(function(_,game,result){
+ game_info = {id:game.id,
+ rc: get_flag_cnt(game.ready_flags),
+ tc: get_player_cnt(game.players),
+ st: game.status != {prepare} && game.status != {game_over}
+ }
+ game_info +> result
+ },ServerReference.get(gmMap),[]);
+ }
+
+ public server function get_player_cnt(players){
+ LowLevelArray.fold(function(player,count){
+ if(player != {none}) count + 1 else count
+ },players,0)
+ }
+
+ server function get_online_cnt(players){
+ LowLevelArray.fold(function(player,count){
+ match(player){
+ case {none}: count;
+ case ~{some}: if(some.status == {online}) count+1 else count
+ }
+ },players,0);
+ }
+
+ public server function game_info(game){
+ {id: game.id,
+ in_progress: game.status != {prepare} && game.status != {game_over},
+ player_cnt: get_player_cnt(game.players),
+ ready_cnt: get_flag_cnt(game.ready_flags) }
+
+ }
+
+ function trans_pile_info(pile_info){
+ LowLevelArray.init(4)(function(i){
+ pile = LowLevelArray.get(pile_info,i);
+ LowLevelArray.fold(function(count,result){
+ match(count){
+ case 2: result ^ "2"
+ case 1: result ^ "1"
+ default: result ^ "0"
+ }
+ },pile,"");
+ });
+ }
+
+ function in_process(game){
+ (game.status == {draw_card} || game.status == {select_action} || game.status == {wait_for_resp});
+ }
+
+ /**
+ * 这个方法用于返回一个用于传递消息的Game.msg对象,为了保证在传输过程中
+ * 的数据量最小,尝试使用一些缩略。
+ */
+ function game_msg(game){
+ {id: game.id,
+ st: encode_status(game.status),
+ ct: game.curr_turn,
+ cc: game.curr_card,
+ rf: game.ready_flags,
+ pls: game.players,
+ dks: Board.get_decks(game.board,{true}),
+ dcs: game.board.discards,
+ pf: trans_pile_info(game.board.pile_info)
+ }
+ }
+
+ function game_obj(game,player){
+ { id: game.id,
+ status: game.status,
+ curr_turn: game.curr_turn,
+ curr_card: game.curr_card,
+ ready_flags: game.ready_flags,
+ players: game.players,
+ decks: Board.get_decks(game.board,{false}),
+ discards: game.board.discards,
+ pile_info: trans_pile_info(game.board.pile_info),
+ player: player,
+ idx: player.idx,
+ deck: Game.get_player_deck(game.id,player),
+ is_ting: {false},
+ is_ok: {false}
+ }
+ }
+
+ /**
+ * 更新服务器端游戏
+ */
+ function update(game){
+ ServerReference.update(gmMap,function(map){
+ Map.replace_or_add(game.id,function(_){
+ {game with last_act: Date.in_milliseconds(Date.now())}
+ },map);
+ });
+ Option.get(Map.get(game.id,ServerReference.get(gmMap)));
+ }
+
+ /** 更新玩家 */
+ exposed function update_player(game,player){
+ match(get(game.id)){
+ case {none}: game;
+ case {some:g}: {
+ players = LowLevelArray.mapi(g.players)(function(i,p){
+ match(p){
+ case {none}: {none}
+ case {some:p}:{
+ if(p.name == player.name && player.idx == i) some(player) else some(p)
+ }
+ }
+ });
+ {g with ~players} |> update(_);
+ }
+ }
+ }
+
+ /**
+ * 处理鼠标点击的事件
+ */
+ client function process(event){
+ game = get_game();
+ if(game.status == {prepare} || game.status == {wait_for_resp}
+ || game.status == {show_result} || game.curr_turn == game.idx){
+ // 获得鼠标点击事件在画布上的坐标
+ canvas_pos = Dom.get_position(#gmcanvas);
+ mouse_pos = event.mouse_position_on_page;
+ x = mouse_pos.x_px - canvas_pos.x_px;
+ y = mouse_pos.y_px - canvas_pos.y_px;
+ pos = ~{x,y}
+ action = Mahjong.get_action(pos,game);
+ match(action){
+ case {none}: void
+ default: {
+ Render.refresh();
+ Render.play_sound("button.wav");
+ Mahjong.request_action(game.id,game.idx,action);
+ }
+ }
+ }
+ }
+
+ /**
+ * 收到游戏消息后的处理函数
+ * @msg Game_msg.t 游戏消息
+ */
+ client function game_msg_received(msg){
+ match(msg){
+ case {GAME_REFRESH: game_msg}:{
+ Render.update(game_msg);
+ Render.update_deck();
+ Render.refresh();
+ }
+ case {GAME_START: game_msg}:{
+ Render.play_sound("start.wav");
+ Render.update(game_msg);
+ Render.update_deck();
+ Render.start_timer();
+ Render.refresh();
+
+ action_flag = Render.get_action_flag();
+ if(action_flag >= 2) Render.show_menu(action_flag);
+ }
+ case {GAME_RESTART: game_msg}:{
+ Render.update(game_msg);
+ Render.refresh();
+ refresh_players(game_msg.pls);
+ }
+ case {PLAYER_CHANGE: game_msg}:{
+ Render.update(game_msg);
+ Render.refresh();
+ refresh_players(game_msg.pls);
+ }
+ case {DISCARD_CARD: msg}:{ //玩家弃牌消息
+ Render.play_sound("da.wav");
+ Render.stop_timer()
+ Render.recv_discard_msg(msg);
+ Render.refresh();
+
+ //如果可以碰/杠/胡,则显示菜单
+ resp_flag = Render.get_resp_flag();
+ if(resp_flag >= 2) Render.show_menu(resp_flag);
+ }
+ case {NEXT_TURN: game_msg}:{
+ Render.update(game_msg);
+ if(game_msg.ct == get_game().idx) Render.update_deck();
+ Render.start_timer();
+ Render.refresh();
+
+ action_flag = Render.get_action_flag();
+ if(action_flag >= 2) Render.show_menu(action_flag);
+ }
+ case {NEXT_ACTION: game_msg, ACT: act}:{
+ Render.update(game_msg);
+ if(game_msg.ct == get_game().idx) Render.update_deck();
+ Render.start_timer();
+ Render.refresh();
+
+ action_flag = Render.get_action_flag();
+ if(action_flag >= 2) Render.show_menu(action_flag);
+
+ rel_pos = Board.get_rel_pos(get_game().idx,game_msg.ct);
+ Render.draw_act(rel_pos,act);
+ }
+ case {HOO:winners}: {
+ set_game({get_game() with status: {game_over},is_ok:{false},is_ting:{false}});
+ Render.stop_timer();
+ Render.refresh();
+ player_idx = get_game().idx;
+ List.iter(function(win_idx){
+ if(player_idx == win_idx) Render.play_sound("win.wav")
+ Render.draw_win(Board.get_rel_pos(player_idx,win_idx))
+ },winners);
+ }
+ case {SHOW_RESULT: result}: {
+ game = {get_game() with status:{show_result}, is_ok: {false}}
+ match(result){
+ case {none}: {
+ set_game(game);
+ Render.refresh();
+ Render.show_draw_play(game,195,75);
+ }
+ case ~{some}: {
+ players = Mahjong.update_scores(game.players,some);
+ set_game({game with ~players});
+ refresh_players(players);
+
+ Render.play_sound("countfan.wav");
+ Render.refresh();
+ Render.show_result(game,some,195,75);
+ }
+ }
+ }
+ case {OFFLINE: player}: {
+ game = get_game();
+ players = LowLevelArray.mapi(game.players)(function(i,p){
+ match(p){
+ case {none}: {none}
+ case {some:p}: {
+ if(p.name == player.name && player.idx == i){
+ some({p with status: {offline}});
+ }else some(p)
+ }
+ }
+ });
+ set_game({game with ~players});
+ Render.refresh();
+ refresh_players(players);
+ }
+ case {PLAYER_READY: ready_flags}:{
+ game = {get_game() with ~ready_flags};
+ set_game(game);
+ if(game.status == {prepare} || (game.status == {show_result} && game.is_ok)){
+ Render.refresh();
+ }
+ }
+ default: jlog("msg: {msg}");
+ }
+ }
+
+ //收到聊天消息
+ client function user_update(msg){
+ line = <li><div class="author">{msg.author}: </div>{msg.text} </li>
+ #chat_messages =+ line
+ Dom.scroll_to_bottom(#chat_messages)
+ }
+
+ client function game_ready(game,player){
+ //加载资源
+ imgs = ["table_bg.png","board.png","result.png","arrow.png","win.png","menu_bar.png","ting.png","dialog.png",
+ "tiles.png","tiles_small.png","numbers.png","start.png","offline.png","player_frame_h.png","player_frame_v.png",
+ "portrait.jpg","eswn.png"];
+ auds = ["start.wav","da.wav","button.wav","tray.wav","countfan.wav","win.wav"];
+ Render.preload(imgs,auds,function(){
+ Dom.set_value(#loading_info,"prepare game...");
+ game_obs = Network.observe(game_msg_received,game.game_channel);
+ chat_obs = Network.observe(user_update,game.chat_channel);
+
+ ck_player = get_cookie("player");
+ ck_coins = get_cookie("coins");
+ coins = if(ck_player != player.name || String.is_empty(ck_coins)) DEFAULT_COINS else string_to_int(ck_coins);
+ player = {player with coins: coins};
+
+ game = update_player(game,player);
+ set_game(game_obj(game,player));
+ refresh_players(game.players);
+
+ //离开页面的提示(对Opera无效)
+ Dom.bind_beforeunload_confirmation(function(_){
+ {some: "Are you sure to quit?"}
+ });
+ Dom.bind_unload_confirmation(function(_){
+ Mahjong.quit(game.id,player.idx);
+ Network.unobserve(game_obs);
+ Network.unobserve(chat_obs);
+ {none}
+ });
+
+ //广播游戏信息
+ Network.broadcast({PLAYER_CHANGE: game_msg(game)},game.game_channel);
+
+ //去掉#gamecanvas的loading样式
+ Render.refresh();
+ Dom.remove(#gmloader);
+ });
+ }
+
+ /**
+ * 获得空的座位
+ */
+ function get_free_place_idx(game){
+ LowLevelArray.foldi(function(i,player,n){
+ if(n != -1) n else {
+ if(player == {none}) i else n
+ }
+ },game.players,-1);
+ }
+
+ /**
+ * 为player安排座位
+ * 返回:{none} or {some:int}
+ */
+ function assign_place(game,player){
+ clnt = match(ThreadContext.get({current}).key){
+ case {`client`:c}: c.client
+ default: ""
+ }
+
+ //先找其ctx.client与clnt一样(说明是同一个客户端的)的玩家,再找空位。
+ //否则返回{none},表示没有位置可以分配。
+ LowLevelArray.foldi(function(i,p,result){
+ match(result){
+ case {some:idx}: {
+ match(p){
+ case {none}: {some:idx}
+ case {some:p}:{
+ match(LowLevelArray.get(game.clients,i)){
+ case {none}: {some:idx}
+ case {some:ctx}:{
+ if(p.name == player.name && ctx.client == clnt) {some:i} else {some:idx}
+ }}
+ }}
+ }
+ case {none}: {
+ match(p){
+ case {none}: {some:i}
+ case {some:p}:{
+ match(LowLevelArray.get(game.clients,i)){
+ case {none}: {some:i}
+ case {some:ctx}:{
+ if(p.name == player.name && ctx.client == clnt) {some:i} else {none}
+ }}
+ }}
+ }}
+ },game.players,{none});
+ }
+
+ /**
+ * 游戏视图
+ */
+ function game_view(game,idx){
+ //更新第idx个玩家的client_ctx
+ player = Option.get(LowLevelArray.get(game.players,idx));
+ /** _ = Client.setTimeout(function(){
+ //如果ctx和game的第idx个client一致,说明这个玩家处于死链接状态,去除之。
+ with_game(game.id,function(game){
+ match(LowLevelArray.get(game.clients,idx)){
+ case {none}: void
+ case {some:c}:{
+ if(c.client == ctx.client && c.page == ctx.page){
+ Mahjong.quit(game.id,idx);
+ }
+ }
+ }
+ });
+ jlog("timeout");
+ },20);*/
+
+
+ Resource.styled_page("Mahjong",["/resources/style.css"],
+ <>
+ <div class="game" onready={function(_){game_ready(game,player) }}>
+ <div class="canvas">
+ <div id=#gmloader >
+ <p id=#loading_info>loading</p>
+ </div>
+ <canvas id=#gmcanvas width="740" height="625"
+ onmousedown={function(event){ process(event) }}>
+ "Your browser does not support html5 canvas element."
+ </canvas>
+ </div>
+ <div id=#gameinfo>
+ <div id=#panel>
+ <div style="float:left;text-align:left;width:45%">
+ <div>
+ <input type="checkbox" style="margin:0px" onclick={function(_){Render.show_or_hide_number()}}/>
+ <span>show number</span>
+ </div>
+ <div>
+ <input type="checkbox" style="margin:0px" onclick={function(_){Render.change_tile_style()}}/>
+ <span>classic tile</span>
+ </div>
+ </div>
+ <div style="float:right;margin:4px">
+ <input type="button" class="btn btn-info" value="Back" onclick={function(_){ Client.goto("/hall")} }/>
+ </div>
+ </div>
+ <div id=#scores>
+ <div class="score_left"><h2>{player.name}</h2></div>
+ <div class="score_middle">[{Board.idx_to_place(player.idx)}]</div>
+ <div class="score_right"><h2 id=#txt_score>{player.coins}</h2></div>
+ </div>
+ <div id=#players>
+ <table id=#tb_players></table>
+ </div>
+ <div class="chat">
+ <ul id="chat_messages"></ul>
+ <div class="input">
+ <input type="text" id=#entry class="input-large"
+ onnewline={function(_){post_chat_msg(player.name,game.chat_channel)}} placeholder="Your Message Here"/>
+ <input id=#post type="button" class="btn btn-primary" value="post"
+ onclick={function(_){post_chat_msg(player.name,game.chat_channel)}}/>
+ </div>
+ </div>
+ </div>
+ </div>
+ </>
+ );
+ /** Resource.styled_page("Mahjong",["/resources/style.css"],
+ <>
+ <h1>Hello Mahjong!</h1>
+ </>
+ );*/
+ }
+
+ @async function post_chat_msg(author,channel){
+ text = Dom.get_value(#entry);
+ if(not(String.is_empty(text))){
+ Dom.clear_value(#entry)
+ Network.broadcast(~{author,text},channel)
+ }
+ }
+
+ client function refresh_players(players){
+ Dom.remove_content(#tb_players);
+ LowLevelArray.iteri(function(i,player){
+ match(player){
+ case {none}: void
+ case ~{some}: {
+ self_idx = get_game().idx;
+ table_row = if(self_idx == i) {
+ //更新得分
+ set_cookie("player",some.name);
+ set_cookie("coins",int_to_string(some.coins));
+ #txt_player = "{some.name}"
+ #txt_score = "{some.coins}";
+ <tr class="self_row">
+ <td width="160px"> {some.name} </td>
+ <td width="80px"> {some.coins} </td>
+ </tr>
+ }else {
+ <tr>
+ <td width="160px"> {some.name} </td>
+ <td width="80px"> {some.coins} </td>
+ </tr>
+ }
+ #tb_players =+ table_row
+ }
+ }
+ },players);
+ }
+
+ /** 开始游戏 */
+ function start(game){
+ {game with
+ board: Board.prepare(game.board),
+ status: {select_action},
+ curr_turn: 0,
+ change_flag: {true},
+ ready_flags: 0,
+ ok_flags: 0,
+ actions: Mahjong.reset_actions(game)
+ }
+ }
+
+ /** 重新开始游戏
+ * ready: 是否需要玩家重现点ready
+ */
+ function restart(game,auto_ready){
+ game = {game with board: Board.create()}
+ game = match(auto_ready){
+ case {false}: {{game with board: Board.create()} with status: {prepare}}
+ case {true}: {{game with board: Board.prepare(Board.create())} with status: {select_action}}
+ }
+
+ //去除掉状态为offline的玩家,更新所有玩家的准备状态为{false}
+ players = LowLevelArray.mapi(game.players)(function(_,p){
+ match(p){
+ case {none}: {none}
+ case {some:p}: if(p.status == {offline}) {none} else some(p)
+ }
+ });
+
+ {game with
+ players: players,
+ ok_flags: 0,
+ curr_turn: 0,
+ change_flag: {true},
+ actions: Mahjong.reset_actions(game)
+ }
+ }
+
+ /** 获取某个玩家的deck */
+ exposed function get_player_deck(game_id,player){
+ match(get(game_id)){
+ case {none}: Card.EMPTY_DECK
+ case {some:game}: Board.get_player_deck(game.board,player);
+ }
+ }
+}
View
98 src/input.opa
@@ -0,0 +1,98 @@
+/*************************************************************************
+ * Mahjong: An html5 mahjong game built with opa.
+ * Copyright (C) 2012
+ * Author: winbomb
+ * Email: li.wenbo@whu.edu.cn
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.