Problem Set 4: Memory Scramble
The purpose of this problem set is to explore multithreaded programming with a shared mutable data type, which you will protect using different thread safety strategies; and to practice writing a client/server system.
Pay attention to the PS4 instructions in the provided code.
You must satisfy the provided specifications for certain methods in
WebServer, and you should not change the code in
You may rewrite or delete any of the other provided code, add new classes and methods, etc.
Take care that the coordinate system of your game board matches the specifications: (row, column) coordinates start at (1, 1) in the top left corner, increasing vertically downwards and horizontally to the right.
Your code will be tested against the HTTP protocol specified below. It is your responsibility to examine Didit feedback and make sure your code compiles and runs for grading, but you must do your own testing to ensure correctness.
On this problem set, you will build a networked multiplayer version of Memory, a.k.a. Concentration, the game in which you turn over face-down cards and try to find matching pairs. Our version will have players turning over cards simultaneously, rather than taking turns. We’ll call it Memory Scramble.
When you’re done, you can play Memory Scramble in your browser.
Iterative Development Recommendation
Both automatic and manual grading for the alpha will focus more on your board and server without concurrency.
First, do problems 1 & 2 without considering concurrency: don’t make your
Board or other ADTs threadsafe, and don’t handle multiple concurrent players in your server.
Implement the gameplay rules specified below by writing tests, choosing data structures, and writing code.
Consider the abstraction functions and rep invariants of all your data types.
Then, revise your work in problem 3 to make your system safe for concurrency. Revise your choice of ADT operations based on the need for atomic actions, revise your implementation using threadsafe data structures and a technique for blocking calls, and write your thread safety arguments.
All cards start face down. Players will turn them face up, and the cards will either turn face down again (if the player turned over cards that don’t match) or be removed from the board (if they do match).
A random 3×3 board with |
BOARD_FILE ::= ROW "x" COLUMN NEWLINE (CARD NEWLINE)+ CARD ::= [^\s\n\r]+ ROW ::= INT COLUMN ::= INT INT ::= [0-9]+ NEWLINE ::= "\n" | "\r" "\n"?
3x3↵ 🦄↵ 🦄↵ 🌈↵ 🌈↵ 🌈↵ 🦄↵ 🌈↵ 🦄↵ 🌈↵
- When a player turns over a first card in a pair, they control that card. If someone else already controls it, they block until they can take control.
- When the player turns over a second card, if the cards match, the player scores one point and keeps control of them; otherwise, the player gives up control. The cards stay face up for now.
- When the player makes their next move, if their previous cards matched, those cards are removed from the board; otherwise, if no one controls them, they turn face down.
- If there is no card there (the player identified an empty space, perhaps because the card was just removed by another player), the operation fails.
- If the card is face down, it turns face up (all players can now see it) and the player controls that card.
- If the card is already face up, but not controlled by another player, then it remains face up, and the player controls the card.
- And if the card is face up and controlled by another player, the operation blocks. The player will contend with other players to take control of the card at the next opportunity.
- If there is no card there, the operation fails. The player also relinquishes control of their first card (but it remains face up for now).
- If the card is face up and controlled by a player (another player or themselves), the operation fails. To avoid deadlocks, the operation does not block. The player also relinquishes control of their first card (but it remains face up for now).
- If the card is face down, or if the card is face up but not controlled by a player, then:
- If it is face down, it turns face up.
- If the two cards are the same, that’s a successful match! The player keeps control of both cards (and they remain face up on the board for now). The player’s score increases by one point.
- If they are not the same, the player relinquishes control of both cards (again, they remain face up for now).
After trying to turn over a second card, successfully or not, the player will try again to turn over a first card. When they do that, before following the rules above, they finish their previous play:
- If they had turned over a matching pair, they control both cards. Now, those cards are removed from the board, and they relinquish control of them.
- Otherwise, they had turned over one or two non-matching cards, and relinquished control but left them face up on the board. Now, for each of those card(s), if the card is still on the board, currently face up, and currently not controlled by another player, the card is turned face down.
To support multiplayer Memory Scramble games over the nextwork, we’ll define a HTTP protocol so the game can be played in a web browser.
The protocol uses standard HTTP, which means every communication is a client request with a server response.
All of our client requests will be
GET requests for a particular path, and all server responses will be plain text.
REQUEST ::= "/look/" PLAYER | "/flip/" PLAYER "/" ROW "," COLUMN | "/scores" | "/watch/" PLAYER RESPONSE ::= BOARD | SCORES BOARD ::= ROW "x" COLUMN NEWLINE (SPOT NEWLINE)+ SCORES ::= (PLAYER " " INT NEWLINE)* PLAYER ::= [\w]+ SPOT ::= "none" | "down" | "up " CARD | "my " CARD CARD ::= [^\s\n\r]+ ROW ::= INT COLUMN ::= INT INT ::= [0-9]+ NEWLINE ::= "\n" | "\r" "\n"?
/watch/... requests, the server responds with
BOARD, the current board.
In the response,
ROW is the number of rows,
COLUMN is the number of columns, and the cards are listed reading across each row, starting with the top row.
none indicates no card in that location,
down is a face-down card,
up indicates a face-up card controlled by another player (or by no one), and
my is a face-up card controlled by the player who sent the request.
3x3↵ up␣A↵ down↵ down↵ none↵ my␣B↵ none↵ down↵ down↵ up␣C↵
The response includes every player that has previously attempted to flip over a card on the board (successfully or not). It does not include players that have only observed the board, or any other IDs. The order of players in the response is arbitrary.
A change is defined as any cards turning face up or face down or being removed from the board.
GET /watch/player request must block until such a change occurs.
When there is no change in the state of the cards — for example, if a player tries to turn over an empty space, or if a player takes control or relinquishes control of a card but it does not turn face up or down — pending watch requests must continue to block.
Iterative development recommendation: implement a single-threaded board first, and connect it to a server in Problem 2. The alpha grading will focus more on using your server without concurrency. Then revise for thread safety and correct behavior with concurrent players in Problem 3.
The utility function
Files.readAllLines(..)will read all the lines from a
toPath()method). If you want to program in a functional style,
Scanneris designed for breaking up input text into pieces. It provides many features and you’ll need to read its specs carefully.
- Each player might use an internal identifier instead, e.g. a unique number.
- Each player might be represented by a different object, e.g. a
PlayerADT. Some of the player’s state might be stored in the
Playerobject, or the
Boardmight store all the state.
You decide which actions are atomic, since the rules do not specify. In rule 3-B, for example, turning the two cards back over might be a single action — where no other player can see or manipulate the intermediate board — or two separate actions. But the board must never reach an inconsistent state, such as two players controlling the same card, or having a face-down card that a player still controls.
Threads might wait for a message from a blocking queue.
You might use
wait()(to block) and
notifyAll()(to wake up blocked threads). These calls must be inside a
waitrelinquishes the lock before it blocks. When waiting threads wake up, only one will win the race for the lock, similar to how only one waiting consumer can take a single message from a blocking queue.
wait()must be used in a condition-checking loop. See, e.g., Guarded Blocks in The Java Tutorials for an example.
Or you might use another tool from the
When a player is waiting, the thread must be blocked waiting for an event, it must not be busy-waiting (see the Reordering example in Concurrency).
ConcurrentHashMapoffers atomic operations like
java.util.concurrent.atomicpackage provides mutable references with atomic operations like
To implement the web server, we will use the
com.sun.net.httpserver HTTP server library provided as part of the JDK.
WebServerconstructor creates a
HttpServerand sets it to handle concurrent requests by automatically creating and reusing multiple threads (
- The constructor has an example of attaching a handler for requests to paths that begin with the prefix
/hello/. The handler receives a
HttpExchangeobject that represents a single client/server request/response. The handler callback will run on a background thread, not the main thread. Since the server handles concurrent requests on multiple threads, multiple threads may be running various callbacks concurrently.
- The example handler calls the private method
handleHello(), which demonstrates how to use the
HttpExchangeto examine the request and send a response. If
handleHellowere to block, the client would be blocked waiting to read the response.
- The constructor also adds a pair of filters to the
loglogs every request to stderr, and
headersadds required HTTP headers. You should add those filters to all your handlers.
- Requests for
/hello/are not part of the game protocol, and you should replace the example code once you understand it.
testHelloValidtests the behavior of the provided server on the valid request
testHelloInvalidtests its behavior on invalid request
To see the example code running, use
This provided class parses command-line arguments, calls one of the static
Board creators based on those arguments, and creates and starts a server.
You should not need to change any of this code.
First right click
ServerMain and select Run As → Java Application.
You should see an error in the console.
Then go to Run → Run Configurations…, select the “Arguments” tab, and enter in the “Program arguments” box:
See Unicode’s Full Emoji List to find character codes for other emoji. To start the server with a board from a file, give the filename instead of size and cards:
http://localhost:8080/hello/world→ valid request,
http://localhost:8080/hello/%F0%9F%8C%8D→ invalid request,
Go away, 🌍.
Remember that the server keeps running until you terminate its process, and if you try to run a second server on the same port, it will fail with “address already in use”.
If you run the server on the command line, use
C in that terminal.
If you run the server in Eclipse, click the double-gray-X button to close consoles for terminated programs, then click the stop button.
Specify, test, and implement support for
/scores requests according to the HTTP game protocol.
(You will implement
/watch/player requests in the last problem.)
At this point, your
WebServer may not implement correct blocking behavior according to the rules or be able to handle concurrent players or concurrent requests from the same player, but it should be able to handle multiple clients that make moves in sequence.
Board to make it threadsafe, updating other ADTs as needed.
Make sure it has a clear, complete thread safety argument.
See How to make a thread safety argument, including with synchronization and with message passing.
SimulationMain.java, increase the value of
players to simulate multi-player Memory Scramble games.
By instrumenting your code, for example with
println statements or by keeping track of cards that different players flip over, you should observe correct control of cards, correct blocking when players try to flip over a first card, and correct removal of matching cards when players make a next move.
Notice that your simulation should not terminate if one player turns over matching cards in their last move (leaving two cards face up under that player’s control) and another player tries to flip one of them as their first card. In Eclipse, click the stop button to terminate the program.
While one request is blocked, other requests must be handled normally.
In the provided code,
handles each request on a separate thread to achieve this.
WebServer has a clear, complete thread safety argument.
WebServer does not need to be a threadsafe type (it may only be safe to use a
WebServer instance from one thread), it must document why it is safe to call back its own request handlers from multiple internal threads.
Notice as well that when a
WebServer is constructed, the board may be shared with other clients of that same
Board is being served, for example, by two different web servers, players on those two servers are all playing together.
However, if there are specifications you are not sure how to test automatically, you can document manual tests that cover parts of some partitions.
Document these tests in the relevant JUnit test file — manual
WebServer tests belong in
WebServerTest.java — immediately below the testing strategy.
For each manual test: give it a descriptive name, list the parts covered, and briefly but precisely describe each step a human tester should follow, along with assertions about expected results at each step.
/* * Manual test: navigate to academic calendar * Covers: page=home, type=static, type=redirect, data-source=registrar * 1. browse to http://web.mit.edu => assert that header contains today's date * 2. click "education" * 3. click "academic calendar" => assert that page shows previous June through next June */
- Browsers cache pages you visit (so revisiting a URL doesn’t always fetch it again from the server) and they preload URLs you might visit (so a page might be fetched before you hit enter in the URL bar).
- Our game protocol violates the spec of HTTP ( ! ) by using
GEToperation should only be an observer, never a mutator! That requirement allows browsers to e.g. preload
GETrequests without worrying about mutating data on the server. Since we’ve violated HTTP spec (to keep things simple for this pset), beware that your browser may pre- or re-request
/flip/...URLs when you manually visit them.
- A change is defined as any cards turning face up or face down or being removed from the board.
- When there is no change in the state of the cards — for example, if a player tries to turn over an empty space, or if a player takes control or relinquishes control of a card but it does not turn face up or down — it is not considered a change.
- If the client receives only a single change, it might be a blocking method that returns when the board changes.
- Listeners might implement a callback interface specified by
Board, and call a method to add themselves as a listener. The board will call them back on each change.
- Clients might be consumers of a threadsafe queue of events provided by the board. The board puts an event object on the queue for each change.
In your web server request handlers, you must call
sendResponseHeaders before the method returns.
You decide which changes are atomic. The “watch” request specification does not say whether, when a pair of matched (or mismatched) cards is removed (or turned face down), that is reported as a single change or two separate changes.
Enter the address of your running server (the default is
localhost:8080), or connect to a friend’s server by IP address.
There is no code running on the
When you implement blocking behavior, make sure the thread is blocked waiting for an event, not busy-waiting (see the Reordering example in Concurrency).
@Overridewhen you override
- There is no guarantee that Didit tests will run within any particular timeframe, or at all. If you push code close to the deadline, the large number of submissions will slow the turnaround time before your code is examined.
- If you commit and push right before the deadline, the Didit build does not have to complete in order for that commit to be graded.
- Passing some or all of the public tests on Didit is no guarantee that you will pass the full battery of autograding tests, but failing the
WebProtocolTestis sure to mean many lost points on the problem set.
The autograder test cases will not change from the alpha to the beta, but their point values will. In order to encourage incremental development, alpha autograding will be weighted more toward situations where clients make moves in series, not concurrently. On the beta, autograding will look at a full range of concurrent situations.
Manual grading of the alpha will only examine internal documentation (AF, RI, testing strategy, etc.) and implementation that does not relate to concurrency. Manual grading of the beta may examine any part, including re-examining those docs or code, and how you addressed code review feedback.