2010-08-26

對戰吧~GWT 踩地雷! [下]

跟電腦(web server)對戰的故事大概是這樣的:
  1. 跟 server 要求開始一場遊戲
  2. 跟 server 取得遊戲資訊
  3. 根據遊戲資訊繪製畫面
  4. 將玩家踩的地點傳送給 server
    1. 命中地雷→更新遊戲資訊
    2. 沒有命中→輪到 AI 踩地雷→更新遊戲資訊
  5. 檢查是否有某方獲勝?
    1. 有→結束。
    2. 沒有→回到步驟 3
粗體的部份是跟 server 有關的部份。
那麼,以 RPC、或說以程式的講法
server 只需要提供兩個 method
  • startGame():負責開遊戲 
    • 回傳:該場 game 的 uid
  • shoot():傳送玩家踩的地點
    • 參數:uid, x, y
    • 回傳:GameInfo 物件
因為懶得多設計一堆有的沒的
所以開場之後讓 client 端程式自動踩 (-1,-1) 這個非法位置
就會進入到步驟 2~4 的循環當中 [逃]

好了,終於要進入到 GWT RPC 的部份了。
這裡打算跳過理論、架構的部份
直接以程式碼來說明一切...

首先,是要有一個 MineService 的 interface
繼承自 com.google.gwt.user.client.rpc.RemoteService
裡頭就是宣告上頭說得那兩個 method
@RemoteServiceRelativePath("mineRPC")
public interface MineService extends RemoteService {
  public String startGame();
  public GameInfo shoot(String id, int x, int y) throws Exception;
}
至於那個 annotation 先跳過,後頭會解釋

再來是一個對應的 interface,名稱通常是後頭補 Async
public interface MineServiceAsync {
  void startGame(AsyncCallback<String> callback);
  void shoot(String id, int x, int y, AsyncCallback<GameInfo> callback);
}

這兩個 class 必須放在 gwt compiler 會處理的目錄下(例如 client)

有 interface 自然有實做的 class
MineService 對應實做 class 通常叫做 MineServiceImpl
會長成這樣子:
public class MineServiceImpl extends RemoteServiceServlet implements MineService {
  private AI_Interface ai = new RandomAI();  //FIXME change your ai here!
  
  @Override
  public String startGame() {
    String id = UUID.randomUUID().toString();
    MineGM setting = new MineGM();
    setServer(id, setting);
    return id+",Random";
  }
    
  private void setServer(String id, MineGM setting){
    this.getThreadLocalRequest().getSession().setAttribute(id+"ID", setting);
  }
  
  private MineGM getServer(String id) throws Exception{
    String name = id+"ID";
    if(this.getThreadLocalRequest().getSession().getAttribute(name)!=null){
      return (MineGM) this.getThreadLocalRequest().getSession().getAttribute(name);
    }else{
      throw new Exception("還沒開局");
    }
  }
  
  @Override
  public GameInfo shoot(String id, int x, int y) throws Exception{
    MineGM server = getServer(id);
    if(x==-1 || y==-1 || server.getMap()[x][y]!=-1){
      return MineGM.toGameInfo(server);
    }
    
    if(!server.shoot(x, y, MineGM.USER)){
      int[] xy = new int[2];
      do{
        ai.guess(MineGM.toGameInfo(server), xy);
      }while(server.shoot(xy[0], xy[1], MineGM.AI));
    }
    
    setServer(id, server);
    return MineGM.toGameInfo(server);
  }
}

這個 class 還會繼承 RemoteServiceServlet
往上追溯,parent 是 HttpServlet
也就是說,MineServiceImpl 也是一個 HttpServlet
雖然 GWT RPC 很神奇地包裝好許多東西
但終究還是 base on JSP
所以寫這個 class 時,就不用管 GWT 的重重限制
只要 web.xml 有設定正確就好

講到 web.xml,回頭講一下 MineService 的 annotation
用 RemoteServiceRelativePath 設定 servlet-mapping 會比較方便
<!-- in web.xml -->
<servlet-mapping>
 <servlet-name>mineRPC</servlet-name>
 <url-pattern>/_mine/mineRPC</url-pattern>
</servlet-mapping>

url-pattern 的值,前半段 _mine 是在 gwt.xml 中
設定 <module rename-to='_mine' >
後半段 mineRPC 就是在 MineService 設定的值
(沒有用這個 annotation,得要多好幾行煩死人的 code)

至於遊戲資訊,我選擇塞在 session 當中
在 RemoteServiceServlet 要取得 session 比較囉唆一點
得要這樣才能取得 session
this.getThreadLocalRequest().getSession()
其餘的程式碼... 嗯... 不在 GWT 的範圍當中,跳過 XD

server 端的程式碼解決了,現在來看 client 端
client 端必須透過 MineServiceAsync 來呼叫 RPC
不過用法有點奇怪...... Orz

首先要先這樣寫,取得一個 MineServiceAsync 的 object
MineServiceAsync msa = GWT.create(MineService.class);
然後就可以用 msa.shoot() 來告訴 server 要踩哪個位置
但是事情還沒完,除了標準的 parameter
得要傳一個為 AsyncCallback 的 parameter
這是讓 server 端處理完畢後可以 callback 的一個手段
初期通常都會用 anonymous class 來解決
所以程式碼會長得像這樣:
msa.shoot(this.gameID, hitX, hitY, new AsyncCallback<GameInfo>(){
      @Override
      public void onFailure(Throwable caught) {
        Window.alert("shoot : "+caught.getLocalizedMessage());
      }

      @Override
      public void onSuccess(GameInfo result) {
        setShootResult(result);
      }
    });

這邊要注意兩件事情

其一,RPC 傳遞/回傳的 class(及其 field)
除了 primitive data type 外
一定得 implements IsSerializable
還必須是 GWT 允許的 class
此外,自訂的 class 還必須讓在 GWT compiler 會處理的目錄下
不然實際跑起來就會有(很難看懂的)錯誤訊息

其二,呼叫完 msa.shoot() 之後
下一步並不會執行 onSuccess()/onFailure()
更正確來說,在寫 client 端程式碼的時候
並不會知道 onSuccess()/onFailure() 什麼時候會呼叫到
端看 server 處理以及網路傳輸的速度... etc
這是 callback 的特性,在踩地雷的 case 當中並不會造成困擾
但在其他實務上,如果發現怎麼 RPC 回傳值都不正確
那大概就是忘記這個性質所導致的...... Orz

喔對... 都忘記講一個大前提了
要用 GWT RPC,server 必須是 JSP container

同時也來講講 GWT RPC 的好處 \囧/
一言以蔽之就是「通通傳便便
client 不用組 query string 或 post 內容
server 端不用準備對應的 url
client/server 都不需要剖析傳遞的資料
甚至可以傳遞(客製化的) exception
在 client 端的 onFailure() 可以分門別類處理......
程式碼看起來、寫起來都很 OO、都很 Java
以一個 Java Programmer 來說,有什麼比這個更快樂的事情呢? XD
(連 xml 都沒有用到呢! [握拳])

好了,「對抗電腦版的踩地雷」拆解到這裡
只剩下地雷區要設定 click 的 handler
收到 GameInfo 之後要更新畫面
以及電腦 AI 設計這些功能
相信你一定可以自己寫的很開心的,就跳過不細談了

如果你想偷懶想拿寫好的程式碼來執行看看,
可以到這裡下載。也歡迎投稿你的 AI 設計

Enjoy GWT and have fun! \囧/

2010-08-23

對戰吧~GWT 踩地雷! [上]

上一次教完如何寫一個生命遊戲(好久以前啊 [遠目])
這次的題目同樣是陣列系的踩地雷
不過,如果單機自己玩也太無聊了點
所以呢... 這次的目標是「對抗電腦版的踩地雷」!
這個題目有點大,讓我們一步一步慢慢來......

首先是先弄出一個 MineGM 的物件
來負責創造地雷世界、運作規則邏輯
所以 MineGM 必須要有這些 field
public static final int UNKNOW = -1;

private int x;
private int y;
private int total;  //總共幾個地雷
private int remainder;  //剩下幾個地雷
private boolean[][] answer; //地雷分佈圖
private int[][] map;  //玩家看到的地圖
private int[] playerHit = new int[2]; //分別踩了幾個

一開始就用亂數把 answer 準備好
至於 map 的內容一開始都是 UNKNOWN,表示還不知道是啥狀況
map 當中還可能出現:
  • 0~8:九宮格內出現的地雷數
  • 9:玩家踩到的地雷
  • -9:電腦踩到的地雷
而 MineGM 還需要有一個 public 的 method「shoot()」
負責接受玩家的輸入、然後回報是否命中
public boolean shoot(int hitX, int hitY, boolean who){
  map[hitX][hitY] = count(hitX, hitY);

  //踩到空地的連鎖反應
  if(map[hitX][hitY]==0){
    for(int i=-1; i<2; i++){
      if(hitX+i==x || hitX+i<0){continue;}
      for(int j=-1; j<2; j++){
        if(hitY+j==y || hitY+j<0){continue;}
        if(map[hitX+i][hitY+j] != -1){
          continue;
        }else{
          shoot(hitX+i, hitY+j, who);
        }
      }
    }
  }

  //不同人踩到地雷要給不同值
  if(map[hitX][hitY]==9){
    remainder--;
    if(who){
      playerHit[0]++;
    }else{
      map[hitX][hitY]=-9;
      playerHit[1]++;
    }
  }
  
  return Math.abs(map[hitX][hitY])==9;      
}

count() 會計算周圍九宮格有幾個地雷
把回傳質設定到對應的 map 上
另外,因為我採取「answer 的周圍多一格空地」的作法
所以「踩到空地的連鎖反應」那段的迴圈可以比較好看一點

雖然已經有 MineGM 建立、維護地雷世界了
但是,我們還是需要另外一個 GameInfo 來包裝給玩家的資訊
不然如果玩家 or 電腦直接拿 MineGM 的 answer 來作弊怎麼辦? Orz
所以在 MineGM 當中弄了一個 static method 來轉換成 GameInfo
public static GameInfo toGameInfo(MineGM server) {
  GameInfo result = new GameInfo();
  result.setMap(server.getMap());
  result.setRemainder(server.remainder);
  result.setTotal(server.total);
  result.setPlayerHit(server.playerHit);
  return result;
}

至於其他的細節就留給大家慢慢寫了......

接下來處理 UI 的部份
這次使用 GWT 2.0 的 UiBinder 來處理排版
使用方法可以看官方文件痞子版的中文翻譯

預計的遊戲畫面長這樣:

上方是數據區
左右兩側是雙方的名字與分數,包了一個 PlayerInfo 來處理
其實很簡單,剛好適合拿來了解 UiBinder

PlayerInfo.ui.xml:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
  xmlns:g="urn:import:com.google.gwt.user.client.ui">
  <ui:style>
  .title{
    padding-left: 5px;  
  }
  </ui:style>
  <g:FlowPanel>
    <g:InlineLabel ui:field="title" styleName="{style.title}"></g:InlineLabel>
    <g:InlineLabel ui:field="hitCount"></g:InlineLabel>
  </g:FlowPanel>
</ui:UiBinder> 

PlayerInfo.java
public class PlayerInfo extends Composite {

  private static PlayerInfoUiBinder uiBinder = GWT.create(PlayerInfoUiBinder.class);

  interface PlayerInfoUiBinder extends UiBinder<Widget, PlayerInfo> {
  }

  @UiField Label title;
  @UiField Label hitCount;
  
  public PlayerInfo() {
    initWidget(uiBinder.createAndBindUi(this));
    setHitCount(0);
  }

  public void setHitCount(int i) {
    hitCount.setText(""+i);
  }

  public void setName(String name){
    title.setText(name+":");
  }
}

中間是還剩下多少地雷,用一個 Label 解決
下方的地雷區則是用 FlexTable 處理
這些東西都放在 MineMain 這個 class 當中
所以 MineMain.ui.xml 會長成這樣:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
  xmlns:g="urn:import:com.google.gwt.user.client.ui" xmlns:m="urn:import:org.psmonkey.product.client.mine">
  <ui:style>
  .playerInfo{
    width: 480px;
  }
  .cpu{
    width: 220px;
    background-color: red;
  }
  .player{
    width: 220px;
    background-color: #64A0C8;
  }
  .remainder{
    width: 40px;
    color: white;
    background-color: gray;
    text-align: center;
  }
  </ui:style>
  <g:VerticalPanel>
    <g:HorizontalPanel styleName="{style.playerInfo}">
      <m:PlayerInfo ui:field="cpu" styleName="{style.cpu}"></m:PlayerInfo>
      <g:Label ui:field="remainder" styleName="{style.remainder}"></g:Label>
      <m:PlayerInfo ui:field="player" styleName="{style.player}"></m:PlayerInfo>
    </g:HorizontalPanel>
    <g:FlexTable ui:field="map"></g:FlexTable>
  </g:VerticalPanel>
</ui:UiBinder> 

很懶惰地用 VerticalPanel 跟 HorizontalPanel 解決 XD

接下來,就要處理跟 web server 之間的溝通了! [待續]

2010-08-12

碎唸 ptt 上的管理

好了,文章開頭就直接自爆來意
以通俗的說法,就是:我是以小組長的身份來官官相護的

後頭的廢話懶得看的人,我可以把結論寫在前面:
除了執法尺度不一致、違反上層法規的申訴外
遇到版務爭議的預設值是支持版主

只要版主有確實執行,版規要定的多奇怪
我會支持,不會干涉

※   ※   ※

我相信這篇大多數人而言,沒啥建設性(破壞性倒是有?)
因為絕大多數是我管 CompScience 這幾年來的 murmur

※   ※   ※

在這個 thread,cleanwind 說了一個很妙的比喻
「CodeJob 像是接案方的家,卻像發案方的動物園。」

如果以 design pattern 的角度 [誤]
管理者跟看板使用者(以下簡稱鄉民)也還蠻適用這個譬喻
管理者像是動物園裡頭的動物
鄉民像是遊客,而且還不用買票進場

管理者的一言一行都要被檢視、要被批評
慘一點的會被批鬥、甚至要被思想改造
當然,這或許是管理者必須要付出的代價 or 義務 or whatever
畢竟權利跟義務通常是對等的
只是,如果以此標準把版主的權利跟義務放到天平上衡量
嗯... 大概版主幹個一年可以增加 2% 上天堂的機率這樣?

但是鄉民呢?

我不是說鄉民不能發表意見
而是,以經驗法則來說,有太高的比率
鄉民因為短暫的不爽,黑特、亂版、申訴、罷免
有付出什麼代價嗎?

保險一點的作法就是負責擾亂一池春水,誰也拿你沒轍
就像 F23ko 的推文,堪稱典範:

吃雞排,看熱鬧,等罷免。
我對這個版的好壞根本沒興趣,也沒心力去付出。
只是看這版主的個性,出狀況、起衝突是遲早的事。

激進一點的,養個帳號出來搞
反正另一個帳號 or 重新註冊又是一條好漢

但是管理者得付出多少代價來處理?
(答:經歷一次事件可以增加 0.5% 上天堂的機率)
版主搞掉幾個?
看板的品質被拖累了多少?

我們真的要理會這種人嗎?

※   ※   ※

回頭看一下版主上任的相關時間表

→2010.07.27 下午,我張貼徵版主的公告
→2010.08.03 凌晨,WolfLord 在 Sub_CS 張貼申請書
    當天,這裡的版標就已經改成
    「新版主政見已貼在 Sub_CS 版」(到現在還沒改是怎樣? [指])
→2010.08.09 半夜,WolfLord 正式就任

徵版主至今,將近兩個禮拜的時間
除了 NotOnSale 那篇不會被受理的文章跟推文
有任何人提出「我要當版主」的申請跟詢問嗎?

從 WolfLord 申請至今,將近一個禮拜的時間
有任何人提出質疑、提出管版理念不妥的地方嗎?

是因為資訊被隱藏起來嗎?
是因為不開放討論嗎?
是因為給的時間不夠多嗎?
是因為 WolfLord 申請書跟實際版規有出入嗎?

我想,這些答案都是「不是」,那麼
為甚麼在正式上任之前都沒有任何(反對)意見
而等到木已成舟之後,才抱怨這個、詰譙那個
還可以批評版主「自我感覺良好」?

有機會發言不發言、有機會掌權不掌權
然後結論確立之後才吵吵嚷嚷要翻案
別人執掌方法不合己意就指責人專制獨裁
這真的是為這個「組織」好而產生的努力嗎?
還是根本就是自我中心、自我感覺良好的作為?

我們真的要理會這種人嗎?

※   ※   ※

我超討厭推文一次超過三行的人
我超討厭連狀聲詞都不能用注音符號的規定
我超討厭案主發案不用寫基本預算

然後咧? 那又怎樣?

你超討厭噓文會被水桶
你超討厭發案之後要回來 update/delete 不然會被劣文
你超討厭水桶解除要寫悔過書

然後咧? 那又怎樣?

版規要怎麼定,那是版主的自由
精準地說,站規賦予版主的權利
從免費入園的遊客變成被觀賞的動物,換來的權利

從另一個角度來說,因為遊客(可能)來來去去
而必須始終都在的,是被觀賞的動物
而必須「面對音樂」,是被觀賞的動物
所以版規要怎麼定,是由版主作決定

需不需要聆聽採納鄉民的意見? 不需要

這就像 Java 跟 .net 哪個好,可以沒完沒了的爭論
你還是得決定這個案子要用哪一個
又或著說,今天說 Java 好的鄉民比較多,所以用 Java
明天說 .net 好的鄉民比較多,所以改寫成 .net
案子這樣搞能不出事嗎?
那為甚麼「管理」這檔事就可以這樣搞?

更進一步講,一個決策必須正反論點都考慮清楚
或是簡稱「要有配套措施」
壓根不是光看哪邊人比較多就能解決的事情

獨裁? 獨裁又怎樣?
這世界上不採取民主制度的組織團體多的是
如果一群腦袋不清楚、說話不用負責的人搞民主
能得到好的結果,只能解釋為神蹟

在 ptt 改站規 or 鄉民講話得付出代價之前
我會贊成、甚至鼓勵版主獨裁

※   ※   ※

日劇《奇蹟餐廳》
來用餐的客人都是國王
但是,不要忘了
國王,也是有被砍頭的
※   ※   ※

原文寫於 2010.08.12 ptt.cc 的 CodeJob 版。由於新版主 WolfLord 的治版理念十分特別,才剛上任就引發不少議論,因此碎念出這篇......