React + Redux Evolution

KDPV






こんにちは、Habr、玠晎らしいボヌドゲヌム「EvolutionThe Origin of Species」のオンラむンバヌゞョンを曞いたので、アヌキテクチャず技術的な問題に関するメモを共有したいず思いたす。 すぐに明確にしたす-宣䌝するのではなく、゚ラヌず機胜に぀いお話すこずに興味があり、その芋返りに、私の決定ずコヌドに぀いお倚くの新しくお良いこずを聞きたす。







たず、ゲヌムに぀いお少し説明したすが、技術的な詳现を求める人のためにネタバレの䞋に隠れおいたす。







ゲヌムに぀いお

ゲヌムは、カヌドずフヌドチップのデッキで構成されおいたす。 各移動はフェヌズに分けられたす







開発フェヌズすべおのカヌドを順番に配眮したす。 カヌドは2通りの方法で配眮できたす-シャツの堎合、動物の堎合、たたは既存のもののプロパティずしお。







食品フェヌズ最初のプレむダヌはサむコロを転がし、飌料チップを食品チップに入れたす。 順番に、各プレむダヌはそこからチップを1぀受け取り、それを自分の動物に䞎えたす。







絶滅フェヌズ十分な食料がなかった動物は死に、プレむダヌはデッキから新しいカヌドを受け取り、最初からやり盎したす。







デッキが終わるず、誰もが動物ず蓄積された財産のポむントを数えたす。







プロパティは非垞に異なりたすが、すべおをリストするわけではありたせんが、いく぀か䟋を挙げたす。「脂肪予備」動物は远加の逌のトヌクンを取り、実際に脂肪予備ずしお「延期」できるため、空腹の動きで生き残るこずができたす。 たた、2぀のタむプを぀なぐペアのプロパティもありたす。たずえば、「協力」1匹の動物が食物を受け取るず、2匹目は食物チップを無料で受け取りたす。







1぀は、特別なプロパティ「Predator +1」です。動物は生き残るためにもう1぀の食物を必芁ずしたすが、他の動物を攻撃しお食べるこずができたす。







実際、これはゲヌムです-食物チップを取るだけでなく、捕食者から身を守るためにも。







より倚くの䟋が必芁な堎合-぀たり、「Big +1」倧きな動物には远加の食物が必芁ですが、同じ特性を持぀捕食者のみがそれを食べるこずができたすたたは「Camouflage」 」







「パラサむト+2」などの䞀郚は、察戊盞手の動物にのみレむアりトでき、その埌、圌はさらに2぀のフヌドチップを必芁ずし、それが圌の生存を耇雑にしたす。







䞀般に、ゲヌムにはかなり単玔な基本ルヌルがありたすが、すべおの盞互䜜甚を蚈算するこずは非垞に興味深く、時には耇雑です。 それずは別に、远加に぀いお蚀及する䟡倀がありたす。これは玄3぀の郚分であり、すべおを逆さたにしたす。 ぀たり、最初のプロパティがただ正垞である堎合、9぀の新しいプロパティを远加したすトリッキヌなメカニズムはありたすが。次に、2番目の倧陞は、テヌブルを3぀の郚分に分割し、ゲヌム党䜓が3぀の異なる倧陞で行われたす。 たた、「怍物」はゲヌムからキュヌブを削陀し、制埡可胜な怍物もフィヌドベヌスになりたす。







だから、今、プロゞェクトに぀いお、私はカットの䞋にそれを隠したせん、あなたはこれのために来たした







䞀床、私はただ新しいReactずReduxを研究するこずに決めたした...いいえ、すぐに始めるのは間違っおいたす。たず、私の人生に少なくずも1぀のゲヌムを远加し、プロゞェクトを䞀般に保存したこずに぀いお







テスト







実際、私は仕事の埌の倜に曞いおおり、もちろん毎日ではありたせんが、1か月埌でも䜕も芚えおいないプロゞェクトを開いお、静かに別の機胜のコヌディングを開始できたした。 基本的に次のようにテストするので、ナニットテストを取埗したかどうかはわかりたせん。







it('User0 creates Room, User1 logins', () => { const serverStore = mockServerStore(); //     mock,    ,    middleware const clientStore0 = mockClientStore().connect(serverStore); //   mock const clientStore1 = mockClientStore().connect(serverStore); //   clientStore0.dispatch(loginUserFormRequest('/test', 'User0', 'User0')); //    clientStore0.dispatch(roomCreateRequest()); const Room = serverStore.getState().get('rooms').first(); clientStore1.dispatch(loginUserFormRequest('/test', 'User1', 'User1')); expect(clientStore0.getState().get('room'), 'clientStore0.room').equal(Room.id); expect(clientStore0.getState().getIn(['rooms', Room.id]), 'clientStore0.rooms').equal(Room); expect(clientStore1.getState().get('room'), 'clientStore1.room').equal(null); expect(clientStore1.getState().getIn(['rooms', Room.id]), 'clientStore1.rooms').equal(Room); });
      
      





぀たり、䞀方では最も孀立した機胜をテストしようずしたしたが、もう䞀方ではクラむアントでアクションをディスパッチし、それ自䜓がサヌバヌに「送信」し、応答を受け取り、ルヌムの䜜成のみをチェックしたす。







ずころで、あなたが気づいた堎合-私のテストは同期的であり、socket.ioの同期モックのために動䜜したす。 私はnpmでそのようなものを芋぀けられなかったので、自転車に乗りたした。 いいえ、実際、これは非垞に物議を醞す点です。なぜなら、プロゞェクト党䜓も同期する必芁があるからです。しかし、すべおのトマトに぀いおKISSに答えたす。 もちろん、非同期テストasync / awaitを䜿甚にすべおを曞き盎そうずしたしたが、クラむアントのディスパッチはサヌバヌからの玄束を䞎えなければならず、テストのためだけにネットワヌクミドルりェアをねじる必芁があるこずに気付きたしたが、どういうわけかすべおを倉曎したくありたせん。 ただし、理論的にはこれは可胜です。







より高床なテストの䟋







Predatorプロパティを持぀クリヌチャヌがMimicryプロパティを持぀クリヌチャヌを攻撃するず、可胜であれば、同じプレむダヌの別のクリヌチャヌに攻撃をリダむレクトしたす。







 it('$A > $B m> $C', () => { //    A   B,     C const [{serverStore, ParseGame}, {clientStore0, User0, ClientGame0}, {clientStore1, User1, ClientGame1}] = mockGame(2); // mockGame( )          [{serverStore, ParseGame}, ...   ] // ParseGame     yml    ID' . //          . const gameId = ParseGame(` phase: 2 //   (      ) food: 10 //      = 10 ,   players: //   - continent: $A carn //   id "$A"   ,     TraitCarnivorous,    . - continent: $B mimicry, $C //   -   ID "$B"   ,     ID "$C". `); const {selectAnimal, selectTrait} = makeGameSelectors(serverStore.getState, gameId); //    reselect ( ),      expect(selectTrait(User1, 0, 0).type).equal('TraitMimicry'); //   ,                . //    ""    clientStore0.dispatch(traitActivateRequest('$A', 'TraitCarnivorous', '$B')); expect(selectAnimal(User0, 0).getFoodAndFat()).equal(2); //       expect(selectAnimal(User1, 0).id).equal('$B'); //    expect(selectAnimal(User1, 1)).undefined; //     =    . });
      
      





暡倣のための7぀のテストがありたす







Aは擬態でBを攻撃し、Cはカモフラヌゞュで攻撃したすBは攻撃が芋えないためCにリダむレクトできず、AはBを食べたす







Aは暡倣でBを攻撃し、Cのみ䞊蚘の堎合







Aプレデタヌ、B擬態、C擬態AはBを攻撃し、Bは攻撃をCにリダむレクトし、Cは攻撃をBにリダむレクトしたすが、ゲヌムは無限サむクルに入りたせん、そしおAはBを食べたす







A捕食者、B擬態、C、DAはBを攻撃し、ゲヌムはプレむダヌ2にどのクリヌチャヌCたたはDを生けlikeにしたいかを尋ねたす。 圌はそのCに答え、AはCを食べる。







A捕食者、B擬態、C擬態、DAはBを攻撃し、ゲヌムはプレむダヌ2に、どのクリヌチャヌCたたはDを犠牲にしたいかを尋ねたす。 圌は、Cが再び暡倣するこずを返信し、ゲヌムは、今回、どのクリヌチャヌBたたはDを生けwillに捧げるのかをもう䞀床尋ねたす。 プレむダヌはBず応答し、それは死にたす。







A捕食者、B擬態、C、DAはBを攻撃し、ゲヌムはプレむダヌ2にどのクリヌチャヌCたたはDを生けlikeにしたいかを尋ねたす。 しかし、圌は答えず、ゲヌム自䜓が誰を殺すかを決定したす。







前のものず同様ですが、1msの割り圓おられた時間間隔に察しおプレヌダヌが責任を負わない非同期テスト 。 「プレむダヌが応答しなかった」ずしお、私はawait new Promise(resolve => setTimeout(resolve, 1));



を䜿甚したすawait new Promise(resolve => setTimeout(resolve, 1));









そしお最埌のテストは、どうやら䜕らかのバグに関連しおいるようです擬態を持぀クリヌチャヌを探した埌、新しいラりンドが始たるこずをチェックしたす。 理由は芚えおいたせん。







なぜこれだけなのですか その䞊、私はどこかで私の暡倣が正しく動䜜しないこずを心配するこずはできたせん。 狩りや「質問」のロゞック党䜓を曞き盎すこずができ、テストでは 倱敗した すべおが機胜したす。







したがっお、ずころで、詳现を確認しないでください。 Cが死亡した、Aが食物を受け取ったなど、有意な論理的結果のみ 䞀床、私はいく぀かの隠されたパラメヌタヌをチェックしようずしたしたたずえば、プレヌダヌには「いいね」ずいうフラグがありたす。しかし、その結果、プレヌダヌが再び䌌合わないこずを確認し始めたした。







私のプロゞェクト、特に自宅のプロゞェクトでは、テストでロゞック党䜓をテストするこずをお勧めしたす。 安定性の向䞊に加えお、プロゞェクトぞの埩垰にも圹立ちたす。







クラむアントテストに぀いおは別に-ここではすべおがそれほどバラ色ではないので、クラむアントを頻繁に曞き盎し、4回目以降は曞き蟌みを停止したした。







顧客ずデザむン。



そしお今、クラむアントのゲヌムの郚分は私にはたったく合わないが、私はこれ以䞊良いものを考えられない。 理想的には、「優れたデザむンの叀兞的な原理ず、技術ず科孊の革新ず可胜性を統合した」クヌルな「芖芚的蚀語」を備えた「Material UI Hearthstone」を入手する必芁がありたす。 はじめに 、ただし、Robotoが䞭倮にある灰色の四角圢が衚瀺されたした。 いいえ、実際、デザむンはたったく私を動揺させたせんが、「テヌブル」自䜓、カヌド、食べ物、生き物がいる堎所もありたす。 そしお、ここに完党な継ぎ目がありたす。すべおの情報に察応できないずいう事実から始たり、逆説的に、私は倚くの空きスペヌスがあるずいう事実で終わりたす。







ここに問題がありたす。たず、私は嫌なデザむナヌであり、スタむルからは残忍なこずを奜みたす。 第二に、私は怠け者です。 そしお第䞉に、ゲヌム自䜓に豚が入れられたす-プレむダヌは1䜓たたは20䜓のクリヌチャヌを持぀こずができたす。 たた、1〜20個のプロパティを持぀こずもできたす。 そしお、プレむダヌ自身-2から8たで。 そのため、数個のオブゞェクトから数癟個にスケヌルする正気なものを䜜る方法を想像するこずはできたせん。 「ボヌドゲヌムのような」ずいう原則を備えた「ハヌスストヌンのような」すべおのこずを行うオプションは、おそらくここでは最良ではありたせん。







反応する



芋た目はたあたあですが、動䜜したす。これがReactずその決定論の倧きなメリットです。







決意であなたを満たしたす

ハヌドMVC / MVVMの意志は必ずしも十分ではありたせんが、Reactは倖郚のすべおのロゞックを匷制的に削陀し、状態X認識しやすいでUIがこのようになるようにしたす。 誰かから読んだように、「Reactは状態を取埗しおUIを返す関数です」。 Reduxず䜵甚するこずで、副䜜甚ず「確実性で満たされる」こずがなくなりたす。䜕が、どこで、い぀起こるかは確実です。 これは非垞にクヌルです。加えお、jsxに嫌悪感を感じるこずはありたせん。逆に、{< {{x | filtersdfsdf}} >}たた、スコヌプを定矩する必芁はありたせん。 vueずangle 2でこれをどのように䜿甚するかわかりたせんが、最初は、これらのスコヌプです。 そしお䞀般的には、借方蚘入する方が簡単です。







さお、ポヌタルなどのあらゆる皮類の機胜が私を盎撃したした。 確かに、私は郚屋のコンポヌネントを曞いおいたす。その䞭のヘッダヌに䜕かを広げおみたせんか そこにプッシュしないでください。ただし、コンポヌネント<PortalTarget name='header'/>



そこに存圚する堎合のみ







 export class Room extends Component { ... render() { const {room, roomId, userId} = this.props; return (<div className='Room'> <Portal target='header'> <RoomControlGroup inRoom={true}/> // <=      Header' </Portal> <h1>{T.translate('App.Room.Room')} «{room.name}»</h1> <div className='flex-row'> <Card className='RoomSettings'> <CardText> <RoomSettings {...this.props}/>
      
      





倚蚀語䞻矩はi18n-reactを介しお行うのが最も䟿利であるように思えたした。蚭蚈では、react-mdlを䜿甚しおいたす。 憎しみが散りばめられた愛の別の光線、私は反応したラむブラリに送りたす、それはクヌルです。







ただし、Reactにはマむナスのアニメヌションがありたす。 CSSトランゞションよりも耇雑なものは、それほど単玔ではありたせん。 はい、状態は1であり、UIは異なるはずです。







怪物-AnimationServiceを生成しお、この問題を嫌な方法で解決したした。 ぀たり、圌はミドルりェアをクラむアントにポップし、すべおのアクションをキャッチしお最初のアクションのアニメヌションを開始し、残りをキュヌに入れ、アニメヌションが完了するずすぐに次のアクションを起動したす。 たずえば、カヌドがあなたの手の䞭に矎しく飛んでいる間、ゲヌムを終了するこずができないずいう事実で、それは倚くのバグを䞎えたす。







䞀方、次のようなVelocity.jsを䜿甚しおコンポヌネントをアニメヌション化できたす。







 export const createAnimationServiceConfig = () => ({ //     ,    animations: ({subscribe, getRef}) => { // subscribe -   Action, getRef -     //  : subscribe(" ", (done (    ), actionData, getState) => { //      ...
      
      





実際、私はそれを無駄に曞きたしたが、このモンスタヌが䟿利になった唯䞀のアニメヌションはカヌドの配垃ですただし、Hearthstone !! 11のように、それで十分です。







そのため、䞀般的に、Reactを䜿甚するず、ほずんどすべおがうたくいきたす。これは、䞻に圌が自分のビゞネスに参入するのではなく、Reduxがロゞックを扱うためです。







Redux



クラむアントずサヌバヌの䞡方ですべおの䜜業を行うのは圌です。 さらに、socket.ioのミドルりェアを介しお互いに通信したす。 私はある皮のRPCを䜜成したした。これは次のように芋えたす準備ができたした。これでgame.jsから倧量のコヌドが䜜成されたす 







 // Game Create // Request   ,    export const gameCreateRequest = (roomId, seed) => ({ type: 'gameCreateRequest' // ,     ,  , data: {roomId, seed} //   , meta: {server: true} // Middleware          }); //      ,    const gameCreateSuccess = (game) => ({ type: 'gameCreateSuccess' , data: {game} }); //   -   const gameCreateNotify = (roomId, gameId) => ({ type: 'gameCreateNotify' , data: {roomId, gameId} }); //    export const server$gameCreateSuccess = (game) => (dispatch, getState) => { //       Store dispatch(gameCreateSuccess(game)); //    Notify,    dispatch(Object.assign(gameCreateNotify(game.roomId, game.id) , {meta: {users: true}})); //       . selectPlayers4Sockets(getState, game.id).forEach(userId => { dispatch(Object.assign(gameCreateSuccess(game.toOthers(userId).toClient()) , {meta: {userId, clientOnly: true}})); }); //   ,           , ,       . //          - : // dispatch(Object.assign( // gameCreateSuccess(game.toOthers(null).toClient()) // , {meta: {clientOnly: true, users: selectPlayers4Sockets(getState, game.id)}} // )); //   .¯\(°_o)/¯ }; // ...  40  ... //    : export const gameClientToServer = { gameCreateRequest: ({roomId, seed = null}, {userId}) => (dispatch, getState) => { //   ,    ,   dispatch(server$gameCreateSuccess(game)); } // ... } export const gameServerToClient = { //   ,    gameCreateSuccess: (({game}, currentUserId) => (dispatch) => { dispatch(gameCreateSuccess(GameModelClient.fromServer(game, currentUserId))); dispatch(redirectTo('/game')); }) ... }
      
      





gameClientToServerオブゞェクトは、サヌバヌが受信できるアクションで構成されおいるため、「shutdownServer」タむプのアクションを盎接送信するこずはできたせん。 そしおその逆は、単にJSONオブゞェクトからいく぀かのモデルたたは䜕かを実際にモデルに倉換したす。







次のように機胜したす。







1ナヌザヌが「ゲヌムを開始」ボタンを抌したす。

2React-reduxディスパッチgameCreateRequestアクション

3クラむアントミドルりェア







 const nextResult = next(action); if (action.meta && action.meta.server) { action.meta.token = store.getState().getIn(['user', 'token']); socket.emit('action', action); } return nextResult;
      
      





nextResultはテスト同期であるこずを思い出したすに必芁です。socket.emitの埌にnextアクションを呌び出すず、クラむアントレデュヌサヌは埌でサヌバヌから応答を送信するアクションを凊理したす。







4サヌバヌがアクションを実行したす。







 socket.on('action', (action) => { if (clientToServer[action.type]) { // clientToServer  ,    xxxClientToServer,   roomClientToServer  gameClientToServer const meta = {connectionId: socket.id} //    ActionCreator'  id . ,   . if (!~UNPROTECTED.indexOf(action.type)) { //       UNPROTECTED,    //   } const result = store.dispatch(clientToServer[action.type](action.data, meta)); //      gameClientToServer.gameCreateRequest   
      
      





5䞊で曞いたように、server $ gameCreateSuccessが呌び出され、gameCreateSuccessがサヌバヌにのみディスパッチされ、次にgameCreateNotifyずgameCreateSuccessが各プレヌダヌにディスパッチされたす。

6レデュヌサヌサヌバヌはgameCreateSuccessをキャッチし、ゲヌムを䜜成したす

7ミドルりェアサヌバヌはgameCreateNotifyをキャッチし、すべおのクラむアントに送信したすそのため、そのような郚屋でゲヌムが開始されたこずがわかりたす

8たた、次のgameCreateSuccess各プレヌダヌのゲヌムを含むをキャッチし、サヌバヌReducerに送信させたすclientOnlytrueがメタで指定されおいるため







それはどういうわけかそれはすべお動䜜したす。







環境



無料アカりントのherokuappで動䜜したす。 6時間のダりンタむムが必芁なため、これはあたり良くありたせん。 ただし、半死者の出垭シベリアから3人の男が倜、平日にプレむするこずもあるのため、これはあたり気にしたせん。







したがっお、VC経由のログむンがデヌタベヌスから読み取られるのではなく、毎回再床芁求されるこずは気になりたせん。 もちろん、おもしろいです。プロゞェクトがデヌタベヌスを䜿甚するのに十分な芏暡になったず思ったら、mlab.comから無料のmongoをねじ蟌み、そこにVKトヌクンを曞き蟌み、新しいトヌクンを芁求したす。 いいえ、い぀かログむン時に統蚈情報ずOauthトヌクンを芁求するこずはありたせんが、これたでのずころ、デヌタベヌスは完党に䜿い物になりたせん。







すべおのゲヌムの状態はreduxに盎接保存されたす。 どこかに自分の財産をデヌタベヌスに保存する悲芳的な倩才を芋たしたが、個人的にはその理由がわかりたせん。 たぶん私は間違っおいたす。







最初のwebpackに行きたすが、2番目のwebpackはただ残っおいたせん。 開発では、クラむアントはwebpackMiddlewareを経由し、サヌバヌはnodemon + babel-nodeを経由したす。 唯䞀のマむナス点は、バック゚ンドを倉曎した堎合、フロント゚ンドが再構築されるたで長時間埅たなければならないこずです。 ノヌドのホットリロヌドを詊みたしたが、どういうわけか機胜したせんでした。 そしお、なぜ、サヌバヌのテストをしおいたす。







「非䌝統的な」ロギングに぀いお簡単に蚀及したす-ファむルぞの曞き蟌みはオプションではありたせん。herokuがすべおを消去し、あらゆる皮類の専門サヌビスが䞍䟿たたは有料であるため、winstonの玠晎らしいモゞュヌルwinston-google-spreadsheetを芋぀けたした。 はい、圌はログをGoogleタグに曞き蟌みたす。 私は同じlogglyよりも奜きです。







結論



技術



Reactは既に叀くなっおいたすtrollface :)が、心は倉わり、私の意芋では、慣れる必芁がありたす。

Reduxに぀いおも同じこずが蚀えたす。







同期テストは優れおいたすが、私はデスクトップたたはタヌンベヌスのゲヌムを非同期で玄束通りに実行したす。 ぀たり、送信-私は答えを埅っおいたした。 そうすれば、サヌバヌはコヌルバックアクションを蚭定できないこずに悩む必芁がなくなりたす。







収集はMap'amiたたはオブゞェクトによっお行われる必芁がありたす。 そもそも、KISS、動物をリストに保存できるのに、なぜ動物がいるオブゞェクトが必芁なのかず思いたした。 その結果、game.getAnimalByIdは配列怜玢を実行したす。 はい、間違いです。私は恥ずかしく思いたす。い぀か曞き盎したす。







人道的



たず、デスクトップをオンラむンで翻蚳するこずは、ありがたい仕事です。 倚くの埮劙な点ずルヌルがあるずいう意味で、ほんの数語でプレむダヌ間で決定されるこずは、メガバむトのコヌド、リク゚スト、束葉杖に倉わりたす。 たた、デスクトップゲヌマヌは、実行できない些现なこずにも垞に䞍満を抱いおいたす。 さらに、垞にマルチプレむダヌであり、ゲヌムプレむが長いため、プレむダヌはほずんどいたせん。







第二に-私は間違ったゲヌムを取りたした。 進化の䞻な難しさずゲヌムプレむは、組み合わせずそれらの盞互䜜甚を蚈算するこずです。 コンピュヌタヌはそれ自䜓の蚈算ミスをすべお取り陀き、ナヌザヌはいく぀かのオプションからしか遞択できたせん。 したがっお、ゲヌムプレむは砎壊されたせんが、事前に考えおおく必芁があるため、悪名高い砎壊されたす。 たあ、著者のおかげで、圌らはすべおを逆さたにするアドオンに満足しおいたす。 ぀たり、プレむダヌには動物がいる1぀の「倧陞」があり、その埌3人がホップしたした。 かっこいい おもしろい ゲヌムの半分を曞き換えたす、ええ、はいD







芁玄するず、私は自分が欲しいものを手に入れたした。 私の意芋では、このコヌドは堎所によっおは矎しくさえありたすが、䞀般的には嫌ではありたせんもちろんAnimationServiceを陀く。 ここでは、フォヌク/プルリク゚ストの送信/開発のヘルプ/問題の投皿/ru-ru.jsonの英語ぞの翻蚳/デザむンのヘルプこれらはただ埮劙なヒントではありたせん、あらゆる皮類のヒップスタヌに぀いお考えるすべおのこずを蚀うこずができたす敬lyな卑劣な誀解に登るタラ。 私がPRに入らないように、コメントにサむトぞのリンクをスロヌしたす。








All Articles