I tried to run your code on my machine. There is no syntax error, the graphics window opens, and I can see the snakes moving. However when I press a key, the program fails with Exception: Unix.Unix_error (Unix.EINTR, "select", "").
I will not try to figure out whether it's a bug in your program or in my configuration, but I can refactor your code a bit.
The first thing is to make sure that top-level values and functions are defined at the top level. At the beginning, everything is fine, but draw_background
is followed by a single semicolon, so the syntax prevents you from defining the next functions at the toplevel.
let player_2 = {x=((n_tiles-1)/2)*tile_size; y=(n_tiles-1)*tile_size; s_y= (-1)*speed; s_x=0} ;;
draw_background n_tiles tile_size;
let draw_players box_player_1 box_player_2 player_1 player_2 =
draw_box box_player_1 player_1.x player_1.y;
draw_box box_player_2 player_2.x player_2.y;
in
let key_pressed_player_1 button_pressed player =
(match button_pressed with
'q' -> if player.s_x <> speed then {x = player.x; y = player.y; s_x = (-1)*speed; s_y = 0} else player;
|'d' -> if player.s_x <> (-1)*speed then {x = player.x; y =player.y; s_x = speed; s_y = 0} else player;
|'z' -> if player.s_y <> (-1)*speed then {x = player.x; y =player.y; s_x = 0; s_y = speed} else player;
|'s' -> if player.s_y <> speed then {x = player.x; y =player.y; s_x = 0; s_y = (-1)*speed} else player;
|_ -> player)
in
Can be changed into
let player_2 = {x=((n_tiles-1)/2)*tile_size; y=(n_tiles-1)*tile_size; s_y= (-1)*speed; s_x=0} ;;
let () = draw_background n_tiles tile_size;;
let draw_players box_player_1 box_player_2 player_1 player_2 =
draw_box box_player_1 player_1.x player_1.y;
draw_box box_player_2 player_2.x player_2.y
let key_pressed_player_1 button_pressed player =
(match button_pressed with
'q' -> if player.s_x <> speed then {x = player.x; y = player.y; s_x = (-1)*speed; s_y = 0} else player;
|'d' -> if player.s_x <> (-1)*speed then {x = player.x; y =player.y; s_x = speed; s_y = 0} else player;
|'z' -> if player.s_y <> (-1)*speed then {x = player.x; y =player.y; s_x = 0; s_y = speed} else player;
|'s' -> if player.s_y <> speed then {x = player.x; y =player.y; s_x = 0; s_y = (-1)*speed} else player;
|_ -> player)
Notice that instead of using let f x = foo in let g x = bar in
we can keep defining top level bindings: let f x = foo let g x = bar
. The intermediate bindings can be adapted this way up until the call to the main loop:
aux player_1 player_2 [] false
in
main_loop player_1 player_2;
is turned into
aux player_1 player_2 [] false
let () = main_loop player_1 player_2;;
Now I can separate the function and value definitions from the instructions actually executing the code, which gives the following grouping at the end of the file.
let () =
Graphics.open_graph " ";
Graphics.set_window_title "Test !";
Graphics.plot 50 50;
draw_background n_tiles tile_size;
main_loop player_1 player_2;;
I try to improve (my) readability by adjusting the style slightly. For instance, following semicolons with a single space: (x1,y1);(ix1,iy1);
becomes (x1,y1); (ix1,iy1);
. Also editing the column alignment here and there.
I use the record update notation. This is quite useful when you are creating a new record based on an existing one and only changing a few fields. For instance:
let update_player_position pl = {
x = pl.x + pl.s_x ;
y = pl.y + pl.s_y ;
s_x = pl.s_x ;
s_y = pl.s_y};;
Becomes
let update_player_position pl = { pl with x = pl.x + pl.s_x ; y = pl.y + pl.s_y };;
The same trick can be used in the key_pressed_player_1
function. It gives
let key_pressed_player_1 button_pressed player =
(match button_pressed with
|'q' -> if player.s_x <> speed then { player with s_x = (-1)*speed; s_y = 0 } else player;
|'d' -> if player.s_x <> (-1) * speed then { player with s_x = speed; s_y = 0 } else player;
|'z' -> if player.s_y <> (-1) * speed then { player with s_x = 0; s_y = speed } else player;
|'s' -> if player.s_y <> speed then { player with s_x = 0; s_y = (-1)*speed } else player;
|_ -> player)
key_pressed_player_2
is similarly adapted, and now I see clearly the similarity between the two. It really begs for refactoring, as you indicated in your post edit. The only difference between the two functions is which key corresponds to which direction. Once the mapping is given, both function behave exactly the same. So I first define a type to capture the concept of direction, and two functions for the mapping from keys to directions.
type direction = Left | Right | Up | Down
let key_to_direction_1 button_pressed = match button_pressed with
| 'q' -> Some Left
| 'd' -> Some Right
| 'z' -> Some Up
| 's' -> Some Down
| _ -> None
let key_to_direction_2 button_pressed = match button_pressed with
| 'j' -> Some Left
| 'l' -> Some Right
| 'i' -> Some Up
| 'k' -> Some Down
| _ -> None
The next step is to capture how the speed of the player is updated based on the desired direction, if any.
let update_speed direction_option player = match direction_option with
| Some Left -> if player.s_x <> speed then { player with s_x = (-1)*speed; s_y = 0 } else player;
| Some Right -> if player.s_x <> (-1) * speed then { player with s_x = speed; s_y = 0 } else player;
| Some Up -> if player.s_y <> (-1) * speed then { player with s_x = 0; s_y = speed } else player;
| Some Down -> if player.s_y <> speed then { player with s_x = 0; s_y = (-1)*speed } else player;
| None -> player
Now I can redefine the key_pressed_player
functions to glue the key-direction mapping and the speed update.
let key_pressed_player_1 button_pressed player = update_speed (key_to_direction_1 button_pressed) player
let key_pressed_player_2 button_pressed player = update_speed (key_to_direction_2 button_pressed) player
Note how the key_to_direction_1
function matches immediately on its last (and single) argument and never uses the argument name anywhere else. In such a situation we don't even need to name the argument:
let key_to_direction_1 = function
| 'q' -> Some Left
| 'd' -> Some Right
| 'z' -> Some Up
| 's' -> Some Down
| _ -> None
The same trick can be used for update_speed
provided we switch the order of the arguments. Of course the order of the arguments also needs to be switched in calls to update_speed
.
let update_speed player = function
| Some Left -> if player.s_x <> speed then { player with s_x = -1 * speed; s_y = 0 } else player;
| Some Right -> if player.s_x <> -1 * speed then { player with s_x = speed; s_y = 0 } else player;
| Some Up -> if player.s_y <> -1 * speed then { player with s_x = 0; s_y = speed } else player;
| Some Down -> if player.s_y <> speed then { player with s_x = 0; s_y = -1 * speed } else player;
| None -> player
Minor stylistic update as I go over the code: the disjunction and conjunction boolean operators ||
and &&
have low priority, so we can drop the parentheses around expressions surrounding them. For instance
let has_lost player walls =
(is_out player) || (is_on_wall player walls)
becomes
let has_lost player walls = is_out player || is_on_wall player walls
Similarly,
if (has_lost player_1_next walls) then begin
minisleep 3.0;
exit 0
end else ();
can be turned into
if has_lost player_1_next walls then
(minisleep 3.0;
exit 0);
I usually prefer using parentheses instead of begin
and end
to group sequences of expressions, but it doesn't really matter. This way, I never need to use these two keywords.
I see that the graphics related functions are independent of the other ones, so I can group them in a module that I call Draw (in my own project, I would create a file Draw.ml to hold them). When I do this, I do not need the "draw" prefix that you added to avoid future namespace collision.
There is another convenient record syntax. When defining function taking a record as argument, one can deconstruct and bind the relevant fields directly. For instance is_out
can be rewritten as
let is_out { x; y; _ } = x > tile_size * n_tiles || y > tile_size * n_tiles || x < 0 || y < 0
It seems that both player's boxes are identical, except for the color. We can share the construction.
let box_player color = {
w = tile_size;
bw = tile_size / 2;
h = tile_size;
r = Top;
b1_col = color;
b2_col = color;
b_col = color }
let box_player_1 = box_player 657900
let box_player_2 = box_player 35700
The starting values for player_1
and player_2
are set globally, but are only used to initialize the main_loop. So these values do not need to be global and can be created immediately before the main loop.
let player_1 = {x=((n_tiles-1)/2)*tile_size; y=tile_size ;s_y=speed; s_x=0} ;;
let player_2 = {x=((n_tiles-1)/2)*tile_size; y=(n_tiles-1)*tile_size; s_y= (-1)*speed; s_x=0} ;;
is moved to
let () =
Graphics.open_graph " ";
Graphics.set_window_title "Test !";
Graphics.plot 50 50;
Draw.background n_tiles tile_size;
let player_1 = { x = (n_tiles - 1) / 2 * tile_size; y = tile_size; s_y = speed; s_x = 0 }
and player_2 = { x = (n_tiles - 1) / 2 * tile_size; y = (n_tiles - 1) * tile_size; s_y = -1 * speed; s_x = 0 } in
main_loop player_1 player_2;;
The updates_speed
function can be simplified by moving the magnitude of the speed to the update_player_position
function:
let update_player_position pl = { pl with x = pl.x + pl.s_x ; y = pl.y + pl.s_y };;
let update_speed player = function
| Some Left -> if player.s_x <> speed then { player with s_x = -1 * speed; s_y = 0 } else player;
| Some Right -> if player.s_x <> -1 * speed then { player with s_x = speed; s_y = 0 } else player;
| Some Up -> if player.s_y <> -1 * speed then { player with s_x = 0; s_y = speed } else player;
| Some Down -> if player.s_y <> speed then { player with s_x = 0; s_y = -1 * speed } else player;
| None -> player
is turned into
let update_player_position pl = { pl with x = pl.x + pl.s_x * speed ; y = pl.y + pl.s_y * speed }
let update_speed player = function
| Some Left -> if player.s_x <> 1 then { player with s_x = -1; s_y = 0 } else player;
| Some Right -> if player.s_x <> -1 then { player with s_x = 1; s_y = 0 } else player;
| Some Up -> if player.s_y <> -1 then { player with s_x = 0; s_y = 1 } else player;
| Some Down -> if player.s_y <> 1 then { player with s_x = 0; s_y = -1 } else player;
| None -> player
Don't forget to update the initial player values
let player_1 = { x = (n_tiles - 1) / 2 * tile_size; y = tile_size; s_y = 1; s_x = 0 }
and player_2 = { x = (n_tiles - 1) / 2 * tile_size; y = (n_tiles - 1) * tile_size; s_y = -1; s_x = 0 }
The update_speed
function can be rewritten to use guards, it'll make it slightly simpler to read.
let update_speed player = function
| Some Left when player.s_x <> 1 -> { player with s_x = -1; s_y = 0 };
| Some Right when player.s_x <> -1 -> { player with s_x = 1; s_y = 0 };
| Some Up when player.s_y <> -1 -> { player with s_x = 0; s_y = 1 };
| Some Down when player.s_y <> 1 -> { player with s_x = 0; s_y = -1 };
| _ -> player
Keep in mind that this modification makes the function a bit less robust. Now if you ever change the type direction
, for instance to add diagonals, the compiler will not complain about a non-exhaustive match, whereas the previous version would get the compiler to remind you to adapt update_speed
.
Now, let's say that we do care about robustness and that we want to take advantage of the type system to help us avoid meaningless states. Currently, there are four possible pairs s_x, s_y
that are meaningful, [-1, 0; 1, 0; 0, -1; 0, 1]
, but the type system does not prevent the construction of or update to player records with values like, say, s_x = 0; s_y = 0
. In order to get the compiler to rule out such values, we can change the record type for players. We replace the s_x
and s_y
fields with a current_direction
field that can only ever take 4 values.
type player = { x:int; y:int; cur_direction: direction }
We then need to adapt our functions to take this new type into account.
let update_player_position pl = match pl.cur_direction with
| Left -> { pl with x = pl.x - speed }
| Right -> { pl with x = pl.x + speed }
| Up -> { pl with y = pl.y + speed }
| Down -> { pl with y = pl.y - speed }
It also gives us the opportunity to clarify the guard condition we had in the update_speed
function: we only update the speed if the required direction is not opposite to the current one.
let opposite_direction = function
| Left -> Right
| Right -> Left
| Up -> Down
| Down -> Up
let update_speed player = function
| Some dir -> if opposite_direction player.cur_direction <> dir then { player with cur_direction = dir } else player
| None -> player
At this point, I realize that key_pressed_player_1
is simple enough that inlining it will not decrease readability. We can also rename update_player_position
into update_position
to match the naming of update_speed
. The main loop looks like
let main_loop player_1 player_2 =
let rec aux player_1 player_2 walls over =
Draw.players player_1 player_2;
let e = Graphics.wait_next_event [Graphics.Poll] in
if e.Graphics.keypressed then ignore (Graphics.wait_next_event [Graphics.Key_pressed]);
let player_1_bis = update_speed player_1 (key_to_direction_1 e.Graphics.key)
and player_2_bis = update_speed player_2 (key_to_direction_2 e.Graphics.key) in
minisleep refresh_rate;
let player_1_next = update_position player_1_bis
and player_2_next = update_position player_2_bis in
if has_lost player_1_next walls then (minisleep 3.0; exit 0);
if has_lost player_2_next walls then (minisleep 3.0; exit 0);
aux player_1_next player_2_next ((player_1_next.x, player_1_next.y) :: (player_2_next.x, player_2_next.y) :: walls) over in
aux player_1 player_2 [] false
We notice that as soon as player_i_bis
is defined, we don't need to access player_i
anymore and that as soon as player_i_next
is defined, we don't need to access player_i_bis
anymore. So we can use the same name player_i
all over and shadow the previous values. On the other hand, the only use of player_i
made by main_loop
is to feed aux, so if we change the ordering of the arguments of aux
we can define this function in main
once and for all (instead of redefining aux
for each instance of the arguments player_i
. Also, the over
argument of aux
is never used, so we can drop it.
When a player loses, we can get out of aux
naturally instead of calling exit 0
, by simply not performing a recursive call.
Back to the drawing. The box_player_1
and box_player_2
values are defined globally but they are only used to feed the Draw.players
function. This can be inlined.
By the way, I'm not exactly sure what you're doing with the relief
type. Only the Top
value seem to be used. Maybe it's there for future versions. If you don't care about Flat
and Bot
, then the code for Draw.box
can be simplified.
I see that Graphics.plot 50 50;
has hard-coded the value 50
, but it corresponds to n_tiles / tile_size
. Also, the Graphics
instructions corresponding to the setup can be moved to the Draw
module by creating an appropriate initialization function.
let init n_tiles tile_size =
Graphics.open_graph " ";
Graphics.set_window_title "Test !";
Graphics.plot (n_tiles / tile_size) (n_tiles / tile_size);
background n_tiles tile_size
After all these modifications, my refactored version of your program would be
#load "unix.cma";;
#load "graphics.cma";;
let minisleep (sec: float) = ignore (Unix.select [] [] [] sec)
type relief = Top | Bot | Flat
type box_config =
{ w:int; h:int; bw:int;
mutable r:relief;
b1_col : Graphics.color;
b2_col : Graphics.color;
b_col : Graphics.color }
let tile_size = 3
and n_tiles = 150
and refresh_rate = 0.02
let speed = tile_size
type direction = Left | Right | Up | Down
type player = { x : int; y : int; cur_direction : direction }
module Draw =
struct
let rect x0 y0 w h =
let (a, b) = Graphics.current_point ()
and x1 = x0 + w
and y1 = y0 + h in
Graphics.moveto x0 y0;
Graphics.lineto x0 y1; Graphics.lineto x1 y1;
Graphics.lineto x1 y0; Graphics.lineto x0 y0;
Graphics.moveto a b
let background n_tiles tile_size =
for i = 1 to n_tiles do
for j = 1 to n_tiles do
rect (i * tile_size) (j * tile_size) tile_size tile_size
done
done
let box_outline bcf col x1 y1=
Graphics.set_color col;
rect x1 y1 bcf.w bcf.h
let box bcf x1 y1 =
let x2 = x1 + bcf.w and y2 = y1 + bcf.h in
let ix1 = x1 + bcf.bw and ix2 = x2 - bcf.bw
and iy1 = y1 + bcf.bw and iy2 = y2 - bcf.bw in
let border1 g =
Graphics.set_color g;
Graphics.fill_poly [| (x1, y1); (ix1, iy1); (ix2, iy1); (ix2, iy2); (x2, y2); (x2, y1) |] in
let border2 g =
Graphics.set_color g;
Graphics.fill_poly [| (x1, y1); (ix1, iy1); (ix1, iy2); (ix2, iy2); (x2, y2); (x1, y2) |] in
Graphics.set_color bcf.b_col;
(match bcf.r with
| Top ->
Graphics.fill_rect ix1 iy1 (ix2 - ix1) (iy2 - iy1);
border1 bcf.b1_col;
border2 bcf.b2_col
| Bot ->
Graphics.fill_rect ix1 iy1 (ix2 - ix1) (iy2 - iy1);
border1 bcf.b2_col;
border2 bcf.b1_col
| Flat ->
Graphics.fill_rect x1 y1 bcf.w bcf.h);
box_outline bcf Graphics.black x1 y1
let box_player color = {
w = tile_size;
bw = tile_size / 2;
h = tile_size;
r = Top;
b1_col = color;
b2_col = color;
b_col = color }
let box_player_1 = box_player 657900
let box_player_2 = box_player 35700
let init n_tiles tile_size =
Graphics.open_graph " ";
Graphics.set_window_title "Test !";
Graphics.plot (n_tiles / tile_size) (n_tiles / tile_size);
background n_tiles tile_size
let players player_1 player_2 =
box box_player_1 player_1.x player_1.y;
box box_player_2 player_2.x player_2.y
end
let key_to_direction_1 = function
| 'q' -> Some Left | 'd' -> Some Right | 'z' -> Some Up | 's' -> Some Down
| _ -> None
let key_to_direction_2 = function
| 'j' -> Some Left | 'l' -> Some Right | 'i' -> Some Up | 'k' -> Some Down
| _ -> None
let update_position pl = match pl.cur_direction with
| Left -> { pl with x = pl.x - speed }
| Right -> { pl with x = pl.x + speed }
| Up -> { pl with y = pl.y + speed }
| Down -> { pl with y = pl.y - speed }
let opposite_direction = function
| Left -> Right
| Right -> Left
| Up -> Down
| Down -> Up
let update_speed player = function
| Some dir -> if opposite_direction player.cur_direction <> dir then { player with cur_direction = dir } else player
| None -> player
let is_out { x; y; _ } = x > tile_size * n_tiles || y > tile_size * n_tiles || x < 0 || y < 0
let is_on_wall player walls = List.mem (player.x, player.y) walls
let has_lost player walls = is_out player || is_on_wall player walls
let main_loop =
let rec aux walls player_1 player_2 =
Draw.players player_1 player_2;
let e = Graphics.wait_next_event [Graphics.Poll] in
if e.Graphics.keypressed then ignore (Graphics.wait_next_event [Graphics.Key_pressed]);
let player_1 = update_speed player_1 (key_to_direction_1 e.Graphics.key)
and player_2 = update_speed player_2 (key_to_direction_2 e.Graphics.key) in
minisleep refresh_rate;
let player_1 = update_position player_1
and player_2 = update_position player_2 in
if has_lost player_1 walls || has_lost player_2 walls then minisleep 3.0
else aux ((player_1.x, player_1.y) :: (player_2.x, player_2.y) :: walls) player_1 player_2 in
aux []
let () =
Draw.init n_tiles tile_size;
let player_1 = { x = (n_tiles - 1) / 2 * tile_size; y = tile_size; cur_direction = Up }
and player_2 = { x = (n_tiles - 1) / 2 * tile_size; y = (n_tiles - 1) * tile_size; cur_direction = Down } in
main_loop player_1 player_2;;
Deeper modifications are possible too. For instance, you could be concerned with the latency introduced by checking for wall collision. Currently, you store walls in a list and checking for membership is O(n). You can replace the list with a two-dimensional array, or with a hash-table and get O(1). If you would like to prefer keeping the code purely functional, then a Set
from the standard library would give you access in O(log n), or maybe you could go for a Patricia tree using Jean-Christophe Filliâtre's library.