あなたのアプローチにはほとんど問題がなく、コードにはいくつかのバグがあります。したがって、最初に後者を強調し、次に別のアプローチを提案して正当化します。
問題
問題1
send_to_client
type の値を返すようですunit Lwt.t
。式を で終了して無視する場合;
は、「メッセージが送信されるまで待たずに先に進む」ことを意味します。通常、これはあなたが望むものではありません。unit Lwt.t
そのため、戻り値にバインドして、スレッドが終了するまで待つ必要があります。
問題2
通常、Lwt プログラミングでは、関数は即時型 (つまり、 にラップされていない型) の値を受け入れ、Lwt.t
遅延スレッド (つまり、型の値) を返します'some Lwt.t
。もちろん、これは通常、あなたが何か違うことをするのを誰も妨げません。ただし、「即時入力、遅延出力」パターンに固執するようにしてください。
問題 3
ツールを使用します。ocp-indent
コードをインデントするために使用すると、読みやすくなります。また、コンパイラを使用せず、トップレベルでプレイしているようです。通常、特にシステム プログラミングでは、これは悪い考えです。ocamlbuild
コードをコンパイルして実行するには、次を使用します。
ocamlbuild game.native --
ゲーム
OCaml でのプログラミングは、Python やその他の型システムの弱い言語でのプログラミングとは異なる哲学を持っています。OCaml では、型とシグネチャの設計から始めて、後で実装を埋める必要があります。もちろん、これは理想化であり、実際には反復的な洗練のプロセスになりますが、一般的なアプローチは同じです。タイプから始めます。
というわけで、まずはplayer
型を定義しましょう。些細なことですが、改善の余地があります。
open Lwt
type player = {
fd : Lwt_unix.file_descr
}
次に、ゲームの初期化の問題を理解するために、型システムを使用してみましょう。ゲームをプレイする準備ができて喜んでいる 2 人のプレーヤーを用意する必要があります。つまり、3 つの連続した状態があることを意味します。
Nobody
準備ができています
One player
準備ができています
Both (player1, player2)
準備ができています
実際には、3 番目の状態に到達するとすぐにゲームの準備が整うため、その状態は必要ないため、選択肢は 2 つだけになります。
type stage =
| Nobody
| One of player
player option
選択したものと同型であるため、ここで typeを使用できます。stage
しかし、もっと明示的にして、独自の型を使用しましょう。これにより、モデルがより制約され、適合した状態に保たれます。
次のステップは、クライアントとサーバー間の対話のプロトコルを定義することです。request
サーバーからクライアントへresponse
のメッセージ、および反対方向に移動するメッセージには名前を使用します。
type request =
| Init
| Wait
| Unknown_command
| Bye
type response =
| Start
| Quit
このプロトコルは、具体的な表現を含まないという意味で抽象的です。これに基づいて、さまざまな表現 (たとえば、GUI インターフェイスや、さまざまな言語をサポートするテキスト チャット) を構築できます。
しかし、テキストコマンドを使用する最も単純な具体的な実装をモックアップしましょう:
let response_of_string msg =
match String.trim (String.uppercase msg) with
| "START" -> Some Start
| "QUIT" -> Some Quit
| _ -> None
そして反対方向に(注:このメッセージをクライアント側でレンダリングし、タイプの値を送信することをお勧めします。これにより、トラフィックプロファイルが低く保たれ、さらに重要なことに、さまざまなクライアントを透過request
的response
に接続できるようになります).
let string_of_request = function
| Init -> "Welcome to a game server.
Please, type
- `start' to start game;
- `quit' to finish session"
| Wait -> "Please wait for another player to join the game"
| Unknown_command -> "Don't understand this"
| Bye -> "Thank you, see you later!"
次のステップは、 のインターフェイスを定義することIo
です。このモジュールは、クライアントとサーバー間の対話を担当します。ソケットや文字列を使用するなど、すべての詳細を抽象化して非表示にする方法に注意してください。
module Io : sig
val send : player -> request -> unit Lwt.t
val recv : player -> response option Lwt.t
end = struct
let send dst msg = return_unit
let recv dst = return None
end
Game
これで、モジュールを定義できます。最初は、2 つの異なるオートマトンがあります。
init
2 人のプレイヤー間のゲームを初期化します。
play
ゲームを 1 回プレイすると、犠牲者が 2 人になります。
これをOCamlで明示的に言ってみましょう:
module Game : sig
(** [play a b] play a game between player [a] and player [b] *)
val play : player -> player -> unit Lwt.t
(** [init next_player] waits until two players are ready to play.
TODO: Describe a grammar that is recognized by this automaton. *)
val init : (unit -> player Lwt.t) -> (player * player) Lwt.t
end = struct
let play a b = return_unit
let init next_player =
let rec process stage player =
Io.send player Init >>= fun () ->
Io.recv player >>= function
| None ->
Io.send player Unknown_command >>= fun () ->
process stage player
| Some Quit ->
Io.send player Bye >>= fun () ->
next_player () >>= process stage
| Some Start -> match stage with
| One a -> return (a,player)
| Nobody ->
Io.send player Wait >>= fun () ->
next_player () >>= process (One player) in
next_player () >>= process Nobody
end
これで、すべてを結合する main 関数を書き出すことができます。
let main server_sock =
let next_player () =
Lwt_unix.accept server_sock >>=
fun (fd,_) -> return {fd} in
Game.init next_player >>= fun (a,b) ->
Game.play a b
このアプローチを続けると、ゲームのさまざまな有限状態マシンがさまざまな言語 (つまり、プロトコル) を定義していることに後で気付くかもしれません。そのため、1 つのプロトコルを使用する代わりに、各 FSM に特定のプロトコル ( 、 など) を使用するinit_protocol
ことplay_protocol
になるかもしれません。これを処理するには、サブタイピングとポリモーフィック バリアントを使用できます。