# [程式筆記]太鼓達人-2.前後端連接與紀錄 接續前篇已經做好的極簡風太鼓達人遊戲,本篇要用websocket連結前後端,將後端的 python server當作AI玩家,前端就像播報員不斷將遊戲狀態報給玩家,而server不斷給出動作指令,當然目前AI玩家還不會學習,只會隨機的給出動作指令,並記下遊戲每個瞬間的變化,累積資料以供之後學習。 ### 往返通訊 前端與後端之間的通訊就像在打桌球,從頭到尾都是一顆球打來打去,只是前端永遠都是發球方,後端只能先最好準備等球打來在回擊。 先假設server端已經準備好了,當前端建立起websocket的連線之後,觸發onopen事件裡面的send函數就會做第一次發球,直接把剛建好的s0、a、r、s1等初始內容發給玩家,(為了方便初始值是隨便給的,之後訓練時略過第一筆數據就好)。 等AI玩家打新的action指令回來後,前端就被觸發了onmessage事件,計算好這個動作帶來的得分後,例如此處動作"o"就代表打鼓觸發beat函數,接著就將當下的遊戲狀態作為新的s1,原本的s1丟進s0,重新把s0、a、r、s1再打給server,如此建立起不斷往返的通訊迴路。 另外,設定了pause變數作為前端是否繼續播報當下狀況判斷,當遊戲有特殊狀況發生時就可以中斷通訊。 ``` javascript=70 var ws = new WebSocket("ws://localhost:9999/echo"); var t=0,s0=0,a=0,r=0,s1=0 var pause=1 ws.onopen = function(){ pause=0 s1=red.x()-100 ws.send([t,s0,a,r,s1].join()) }; ws.onmessage = function (evt){ a = evt.data; r=0 if( a=='o' ){ beat() } s0=s1 s1=red.x()-100 if(!pause){ ws.send([t,s0.toFixed(2),a,r.toFixed(2),s1.toFixed(2)].join()) } }; </script> ``` ### 隨機玩家 即便是強化學習AI的在累積足夠經驗前,基本上就是先以隨機的方式在挑選動作,但每個動作的機率也不能隨便設,在此我先設定每個瞬間只有0.05的機率回做出敲鼓的動作,盡量讓每種遊戲狀況都能發生到,也避免一直亂敲學到了一堆扣分的動作。 在下一步的動作把回傳給前端後,就順便將剛才收到的s0,a,r,s1等訊息記在本本(txt檔)裡,記滿10000筆資料後就收工。 ```python=1 import asyncio import websockets import random n=0 f = open("tempoRecord.txt", "w") async def echo(websocket, path): async for msg in websocket: global n n+=1 f.write(msg+"\n") a=random.random() if a>0.95 : action='o' else : action='x' await asyncio.sleep(.02) await websocket.send(action) if n%100==0 : print("record "+str(n)+" data to tempoRecord.txt") if n>10000: f.close() asyncio.get_event_loop().stop() asyncio.get_event_loop().close() asyncio.get_event_loop().run_until_complete( websockets.serve(echo,'localhost', 9999)) asyncio.get_event_loop().run_forever() ``` 這裡值得特別注意的是,為甚麼在隨機決定好動作後到send給前端前,要故意sleep耽擱個0.02秒, 簡而言之是要模擬控制延遲,例如人機對戰為求公平,會將這個延遲秒數近似於人類的反射弧,往更深一點講這關乎於回合遊戲與即時遊戲的差異性。 ### 特殊狀況 ```javascript=38 function run(){ t+=1 red.x(red.x()-1) if( red.x()<100 ){ pause=1 r=-10 red.x(150+Math.random()*150) s0=s1 s1=red.x()-100 ws.send([t,s0,a,r,s1].join()) pause=0 } } setInterval('run()',10) function beat(){ end.strokeWidth(5) end.stroke('yellow') setTimeout(() => { end.strokeWidth(2) end.stroke('black') }, 100); if( red.x()<150 ){ r=10-(red.x()-100)/5 red.x(150+Math.random()*150) }else{ r=-1 } // console.log(r) } document.addEventListener("keydown", (event) => { if(event.key=='a'){ // beat() } }); ```