Homework 10
Due Date Tuesday 11/16 at 9:00pm (Week 10)
Purpose To enhance BoxOut with two-player support
Starter code: server code
Graded Exercises
In the previous part of BoxOut, you implemented the logic for growing walls, splitting regions, finishing levels, and making the game playable. In this week’s exciting conclusion, you’ll add support for a second player. The overall goal is for two players to connect to a server and be paired up. One player will be Player 1, and they will be responsible for moving the balls around, splitting regions, etc. The other player will be Player 2. Player 2 will notify Player 1 whenever they create a wall; Player 1 will in turn notify Player 2 every time the world gets updated. Each player will control their own mouse, and be able to create their own wall (so there can now be up to two walls growing simultaneously in a game). Both players will draw the game from their own perspective: their wall will be drawn in black, while the other player’s wall will be white. They will also only draw their own mouse cursor and wall direction.
In this design, we will provide you with code that implements the server-side logic of pairing up two players into a game together. When you start your program, you won’t know whether you’re going to be Player 1 or Player 2 (it depends on how many other players are available!), so once the server has matched you with a partner, it will inform you of which player you are, and your code will react accordingly. We’ll give step-by-step instructions, below. Read through the whole assignment, to get a sense of what you have to do, before you start coding. There are a lot of interrelated data definitions needed here, so it’s easy to get confused.
Exercise 1 You and your partner have two distinct implementations of BoxOut so far. Combine the best aspects of both of them: choose the cleaner data definitions, cleaner functions, more thorough testing, etc., and produce a new implementation of BoxOut part 2 that is better than either of your original starting points. Incorporate any feedback you received from your graders on the prior parts: if we noted that something is a poor implementation choice, you should fix it. Since this is the final part of this project, you will be graded cumulatively on all of it: prior mistakes can be penalized again.
Once you’ve merged your code and cleaned up any prior issues, you can add support for a second player, in several steps. Be sure to work through these in sequence, so that you don’t have incomplete and hard-to-debug programs along the way.
Initial steps:
- Renaming: Both of your players will have their own world state, and their own big-bang event handlers with their own logic. In preparation, you should rename your existing functions. Presumably, you currently have a big-bang call that looks roughly like
(big-bang (make-world ...various world info...) [on-tick tick-handler] [to-draw draw-world] [on-key key-handler] [on-mouse mouse-handler] ...) Rename all your functions with a /p1 suffix to their names, so that your big-bang call looks like this: - Organization: Your program will have four sections of related content. Add comments to your code to separate it into the following four sections:
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;; Shared data definitions and functions ;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;; Player 1 data definitions and functions ;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;; Player 2 data definitions and functions ;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;; Player-neutral data definitions and functions ;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Most of your existing code should currently go in the Player 1 section, but as you add new features, or generalize existing code, move it to the appropriate section. Put all of the relevant code (data definitions, templates, examples, functions, test, comments) in the relevant sections, to make it easier for both you and your graders to see which code belongs where. Supporting two kinds of players: When you connect your game to the server, you won’t know yet whether you’re going to be Player 1 or Player 2. So we need to create a new, more general big-bang world state, to account for the possibility of being Player 1, Player 2, or not-yet-playing.
We will also need a new data definition to describe the results of our event handlers (which could be world states or packages of world states and messages):
Corrections are marked in red.
; A World2Players is one of ; - 'not-playing-yet ; - (make-player1 Player1WorldState) ; - (make-player2 Player2WorldState) ; INTERPRETATION: This keeps track of whether you've been connected with a ; partner to play with, and whether you're Player 1 or Player 2 (define-struct player1 [world]) (define-struct player2 [world]) ; A Player1Result is one of ; - (make-player1 Player1WorldState) ; - (make-package (make-player1 Player1WorldState) Player1Message) ; INTERPRETATION: An event handler for Player 1 could either return a world ; or a world plus some messages to send to Player 2 ; A Player2Result is one of ; - (make-player2 Player2WorldState) ; - (make-package (make-player2 Player2WorldState) Player2Message) ; INTERPRETATION: An event handler for Player 2 could either return a world ; or a world plus some messages to send to Player 1 ; A World2PlayersResult is one of ; - 'not-playing-yet ; - Player1Result ; - Player2Result Note the template for a World2Players: it has three cases, for whether you’re waiting for a partner, whether you’re Player 1, or whether you’re Player 2.
For each big-bang handler you have, implement a new handler that takes in a World2Players, and chooses what to do accordingly. Here are the beginnings of two such handlers, to get you started:
; draw-world : World2Players -> Image ; Renders the world, depending on which player is playing (define (draw-world w) (cond [(symbol? w) (...draw something indicating you're not playing yet...)] [(player1? w) (draw-world/p1 (player1-world w))] [(player2? w) (error "Not yet implemented drawing for Player 2!")])) ; key-handler : World2Players KeyEvent -> World2PlayersResult ; Handles a key event, depending on which player is playing (define (key-handler w key) (cond [(symbol? w) w] ; nothing to do until you connect [(player1? w) (key-handler/p1 (player1-world w) key)] [(player2? w) (error "Not yet implemented key handling for Player 2!")])) Note that you’ll have to revise your Player 1 functions’ return values just slightly: instead of returning a (make-world ...), you’ll now need to return a (make-player1 (make-world ...)).
If you update your call to big-bang, your game should play exactly the same as before. This a good thing! It means you’ve refactored your code in preparation for adding Player 2.(big-bang (make-player1 (make-world ...various world info...)) [on-tick tick-handler] [to-draw draw-world] [on-key key-handler] [on-mouse mouse-handler] ...) Note: You will need to create three receive-handler functions, for Player 1, for Player 2, and a general one for the beginning of the game. We’ll come back to these later in the assignment.
Extending your Worlds: Player 2 will add a new mouse cursor and a new wall to the game, but everything else will stay the same. Add a new field to your World struct, so that you now have two fields for walls. At least initially, start Player 2’s wall as horizontal. (We’ll call this enhanced struct a Player1WorldState, in the data definitions below.)
Enhance your draw-world/p1 function to draw both walls onto your game board. Draw Player 1’s wall black, and Player 2’s wall white.
Moving balls, growing two walls and splitting regions: Only player 1 is responsible for moving the balls around the regions, growing the wall(s), and splitting regions. The only thing that should change here is that now you have potentially two walls to grow, clamp and split regions with, and collide with balls. You should grow Player 1’s wall first, clamp it to its region, and split the region if necessary, and then grow Player 2’s wall, clamp it to its region (which may have changed, if Player 1’s wall split regions!), and split the region if necessary.
You should also check whether any balls collide with Player 2’s wall, and destroy that wall if so. (Both players will share a single counter of lives; you don’t need to keep separate scores for them both.)
Player 2
Player 2’s logic is much simpler than Player 1’s, so you’ve finished most of the hard parts already! Design Player 2’s handlers (key-handler/p2, tick-handler/p2, etc.) as follows:
The Player2WorldState also needs to know about Player 2’s current wall direction, so that it can be toggled when needed. But does that need to be added into the Player2WorldState, or is it already present somehow?
Design a Player2WorldState. It should either be #false, to indicate Player 1 hasn’t sent any information to Player 2 yet, or it should be a struct that contains a Player1WorldState (which will contain all the current information about the game that Player 1 has sent to Player 2), as well as keep track of the current mouse position of Player 2.Design a function draw-world/p2 that draws the gameboard from Player 2’s perspective: it should be nearly identical to draw-world/p1, except that Player 2 wall should be black, and Player 1’s wall should be white.
Player 2 doesn’t need an tick-handler/p2 handler to do very much (since Player 1 will do all the computation), so just return the world unchanged.
- Player 2’s key-handler should toggle the direction of Player 2’s wall, and send a message to Player 1 with the new direction. When Player 2 creates a wall, it likewise should send a message to Player 1 with the initial wall information. The message should look like
Design a function receive-handler/p2 that takes in a Player2WorldState and a Player1Message, and produces a new Player2Result for Player 2. A Player1Message will be a message that Player 1 sends to Player 2, explaining the current state of the game:
; A Player1Message is a ; (list 'update Player1WorldStateSexp) ; A Player1WorldStateSexp is a ; (list 'world PosnSexp Number [List-of RegionSexp] WallSexp WallSexp PosnSexp) ; A PosnSexp is a (list 'posn Number Number) ; A RegionSexp is a (list 'region BoundsSexp [List-of BallSexp]) ; A BoundsSexp is a (list 'bounds PosnSexp PosnSexp) ; A BallSexp is a (list 'ball PosnSexp VelSexp) ; A VelSexp is a (list 'vel Number Number) ; A WallSexp is one of ; - Direction ; - (list 'wall Direction PosnSexp PosnSexp) ; INTERPRETATIONS: Each of these definitions corresponds exactly to the ; non-s-expression data definitions from Homework 7, except that the ; Player1WorldStateSexp has a second wall in it (as described above). You will need to design a whole bunch of helper functions that convert these s-expression messages into world states. Here is some starter code:
; wssexp->world : Player1WorldStateSexp -> Player1WorldState ; Converts a description of a world state into an actual world-state (define (wssexp->world wss) (make-world (posnsexp->posn (second wss)) ; board size (third wss) ; lives (map regionsexp->region (fourth wss)) ; all the regions (wallsexp->wall (fifth wss)) ; Player 1's wall (wallsexp->wall (sixth wss)) ; Player 2's wall (posnsexp->posn (seventh wss)))) ; Player 1's mouse ; posnsexp->posn : PosnSexp -> Posn ; Converts a description of a posn into an actual posn (define (posnsexp->posn p) (make-posn (second p) (third p))) ; Define more functions for walls, regions, velocities, etc. Once you have converted the message into a Player1WorldState, that should replace the existing world state in Player 2’s Player2WorldState.
Finishing Player 1: Design a bunch of functions that convert Player 1’s world state into an s-expression, so that you can send messages to Player 2 informing them of how the game is being played. Here is some starter code:
Hint: it’s probably a good idea to test these functions together with the ones above, as a round-trip test: (check-expect (wssexp->world (world->wssexp some-world)) some-world), and similar tests, help check whether these functions are inverses of each other.
; world->wssexp : Player1WorldState -> Player1WorldStateSexp ; Converts a world state into a description of a world-state (define (world->wssexp w) (list 'world (posn->posnsexp (world-size w)) (world-lives w) (map region->regionsexp (world-regions w)) (wall->wallsexp (world-wall1 w)) (wall->wallsexp (world-wall2 w)) (posn->posnsexp (world-mouse1 w)))) ; posn->posnsexp : Posn -> PosnSexp ; Converts a posn into a description of a posn (define (posn->posnsexp p) (list 'posn (posn-x p) (posn-y p))) ; Define more functions for walls, regions, velocities, etc.
Update your tick-handler/p1 function so that on every tick, it sends the current game state to Player 2.
Design a receive-handler/p1 function that takes in messages sent from Player 2, and updates its own world state as appropriate.
Putting it all together! At long last, you’re ready to put both players together. Download this starter code (right click > Save As...), which gives you our implementation of the server, and save it in the same directory as your homework file. At the top of your file, add the line (require "boxout-server.rkt") to include that code.
Complete the design of a receive-handler function for the outermost big-bang. It will take in a World2Players and a ServerMessage, and produce a World2PlayersResult:
; A ServerMessage is one of ; - "p1" ; - "p2" ; - Player1Message ; - Player2Message ; INTERPRETATION: One of the first two messages are sent by the server to each ; client, exactly one time, to inform them whether they will be Player 1 ; or Player 2. Subsequent player-specific messages are sent during the game. ; receive-handler : World2Players ServerMessage -> World2PlayersResult ; Handles incoming messages from the server by either creating an appropriate player ; or forwarding the message to the player-specific receive-handler functions. (define (receive-handler w msg) (cond [(symbol? w) (cond [(string=? msg "p1") ... create an initial player 1 world state...] [(string=? msg "p2") ... create an initial player 2 world state...])] [(player1? w) ...] [(player2? w) ...]))
(define (start-game playername servername) (big-bang 'not-playing-yet [on-tick tick-handler] [to-draw draw-world] [on-key key-handler] ...whatever else you need... [register servername] [name playername]))
At the very bottom of your file, add the lines
(launch-many-worlds (server "S") (start-game "Ben" LOCALHOST) (start-game "Amal" LOCALHOST))
We have not yet set up the server to run yet, so you’ll need to test locally for now. We’ll update you next week when the server is ready.
Have fun!