Lab 9: Web Applications
- Due: Wednesday 11/11 @ 11:59pm
- Points: 1
- Download: lab09.zip
In this lab, you'll build a web app for playing rock, paper, scissors against the computer. Click one of three emoji buttons to make your choice. The computer makes its choice (based on the history of prior rounds), and whoever wins the round earns a point.
Attendance
You need to submit the lab problems in addition to attending to get credit for lab. Students in mega lab only need to submit the lab problems. Preceptor interviews are highly recommended but not required for lab submissions.
If you are in regular lab, your TA will come around during lab to check you in. If you didn't attend for a good reason (such as being sick), fill out this form (within 2 weeks of your lab): attendance form.
Setup
Download lab09.zip from the link at the top of this page, unzip it, and open
the lab09 folder in VS Code, just as you did in Lab 8.
The folder contains:
src/rps.gleam: The app, which you'll complete.src/agent.gleam: The computer's strategy.index.html: The web page that the app appears in.rps.css: The styles that describe how the page looks.dev/: A small web server that sends these files to your browser.test/: Tests for the app.
Components
A web browser displays a page by loading three kinds of files:
- HTML describes what is on the page as a tree of elements, such as
<div>(a box),<p>(a paragraph), and<button>. An element can have attributes, such asclass="score-card", and its children are written between its opening tag and closing tag. - CSS describes how each element is displayed: its size, color, font, and position.
- JavaScript is run by the browser and can change the contents of the page and respond to user actions (such as button clicks). Gleam compiles to JavaScript.
The index.html file describes the app's page and connects these parts
together.
Run the App
From the Terminal menu within VS Code, select New Terminal. Start the web server, which compiles the app to JavaScript and sends the files in this folder to your browser:
gleam dev
The first time it runs, Gleam downloads the packages that the app depends on and may print warnings about their code, which you can ignore.
Then open http://localhost:8000 in your web browser. You'll see the score
cards and a Start over button, but where the choices should be, it only
says "Buttons go here." (If port 8000 is in use, add a different port to the
command: gleam dev 8001.)
The server keeps running until you press Ctrl-C in the terminal. Each time you
save src/rps.gleam, the server compiles your code again, and then you can
reload the page in your browser to see the change. If it doesn't compile, the
error appears in the server's terminal and the page shows the last version that
did. The server won't start if your code doesn't compile, so if gleam dev
fails, fix the error it reports and run it again. To run other commands, such as
gleam test, open a second terminal by clicking the + button above the
terminal in VS Code.
The finished app looks like this: rockpaperscissors.cs61a.org
Model-View-Update Architecture
This lab uses Lustre, Gleam's main web framework. A Lustre app has three parts:
- The model is the state of the app.
- The view is a function of the model that returns the tree of elements to display. It specifies which message to send when the user takes an action (such as clicking a button).
- The update is a function that takes the current model and a message, and returns an updated model.
Lustre runs the app in a loop: it calls view on the model and displays the
result. For every message that comes in, Lustre calls update with the current
model and that message, then calls view on the new model and
efficiently updates the page to match what view returns.
Here is the model for this app. Each Round (defined in src/agent.gleam)
holds two choices: Round(you: Choice, computer: Choice). The init function
returns the model before any rounds have been played.
/// Each player's score and every round played so far, most recent first.
pub type Model {
Model(your_score: Int, computer_score: Int, rounds: List(Round))
}
/// The model before any rounds have been played.
pub fn init(_flags) -> Model {
Model(your_score: 0, computer_score: 0, rounds: [])
}
The view is built from functions in lustre/element/html, one for each kind of
HTML element. Each one takes a list of attributes and a list of children. For
example, this Gleam code:
html.div([attribute.class("score-card")], [
html.div([attribute.class("score-number")], [html.text("3")]),
html.div([attribute.class("score-label")], [html.text("You")]),
])
describes these HTML elements:
<div class="score-card">
<div class="score-number">3</div>
<div class="score-label">You</div>
</div>
Here is the whole view. It specifies what messages are sent by each button.
/// The whole page.
pub fn view(model: Model) -> Element(Message) {
html.div([], [
html.div([attribute.class("scoreboard")], [
view_score_card(model.your_score, "You"),
view_score_card(model.computer_score, "Computer"),
]),
view_buttons(),
case model.rounds {
[] -> element.none()
[Round(you:, computer:), ..] ->
html.p([attribute.class("hands")], [
html.text(emoji(you) <> " vs " <> emoji(computer)),
])
},
html.button([attribute.class("button"), event.on_click(UserClickedReset)], [
html.text("Start over"),
]),
])
}
Required Questions
Q1: Update
Implement the UserChose case of update, which handles the message that the
view sends when you click one of the choices. Call agent.computer_choice,
passing the history of past rounds but not what you just chose, then update the
model with new scores and a longer history that includes the round you just
played. The function beats(a, b) in src/rps.gleam returns whether choice a
beats choice b.
Hint: Calculating Scores
Add a helper function point(a: Choice, b: Choice) -> Int that returns 1 if
choice a beats choice b and 0 otherwise. Then each player's new score is
their old score plus the return value of point.
Hint: Updating the Model
Return a new Model built from this template:
Model(
your_score: // your new score
computer_score: // the computer's new score
rounds: // the round just played, followed by model.rounds
)
The most recent round comes first in rounds.
pub type Message {
UserChose(Choice)
UserClickedReset
}
/// The model that results from a message. UserChose plays a round of
/// rock-paper-scissors against the computer. UserClickedReset starts over.
pub fn update(model: Model, message: Message) -> Model {
case message {
UserClickedReset -> init(Nil)
// YOUR CODE HERE (replace the code below)
UserChose(_) -> model
}
}
gleam test -- updateQ2: Buttons
Implement view_buttons, which returns a div with class("choices")
containing three buttons, one for each choice: Rock, Paper, and Scissors.
Each button's only child is the text of the emoji for its choice, and
clicking it sends the UserChose message for that choice. Follow the example of
the Start over button in view.
Recommended: Add
import gleam/listto the imports at the top ofsrc/rps.gleamand uselist.mapto create the buttons. It takes a list and a function and returns a list containing the return values of calling the function on each item in the input list.
Hint: Inputs to list.map
There is one button for each choice, so call list.map([Rock, Paper, Scissors], fn(choice) { ... }).
Hint: Template
Replace [html.text("Buttons go here")] with this template, which makes one
button for each choice:
list.map([Rock, Paper, Scissors], fn(choice) {
html.button([____], [____])
})
- The first blank is the button's attribute: call
event.on_clickwith the message thatupdateshould receive when the user clicks this button. - The second blank is the button's child: call
html.texton theemojiforchoice.
/// A button for each choice, showing its emoji. Clicking one sends UserChose.
pub fn view_buttons() -> Element(Message) {
// YOUR CODE HERE (replace the code below)
html.div([attribute.class("choices")], [html.text("Buttons go here")])
}
gleam test -- view_buttonsSubmit Assignment
Submit this assignment by running Provenance: Prepare Submission Bundle from the VS Code command palette and uploading the resulting zip to Gradescope. The zip already contains the files you've edited. Lab 00 has detailed instructions.
Your responses to WWPD questions are not submitted, and they do not need to be. Lab credit is based on the code writing questions.
Optional Questions
These questions are optional. If you don't complete them, you will still receive credit for this assignment. They are great practice, so do them anyway!
Q3: History
For this challenge problem, there are no "YOUR CODE HERE" markers. Add code to
src/rps.gleam where you see fit. Do not rename or remove the your_score,
computer_score, or rounds fields of Model, which the tests rely on.
Add "Previous round" and "Next round" buttons that allow the user to see the outcomes of previous rounds.
Pressing these buttons should not change the score or round history, but should change which round is displayed by hand emojis. If the round being displayed is not the latest round, then right below the hand emojis, add a message: "(1 round ago)" or "(N rounds ago)", where N is how many rounds before the latest round the displayed round is. Pressing "Next round" should show the round after the one currently displayed.
"Previous round" should only be enabled (pressable) when there is some previous round to show, and "Next round" only enabled when viewing some previous round.
When the user presses rock, paper, or scissors, then the outcome of that newly completed round should be displayed, regardless of how many times "Previous round" was pressed before.
How to track the round being displayed. Add information to the model. The easiest way is to add back: Int that represents how many rounds back from the latest round the displayed one is. It increases upon pressing "Previous round" and decreases upon pressing "Next round" but should always be non-negative and less than the number of rounds (or 0 when there are no rounds).
How to handle button presses. Add Message variants, one for each button.
How to update the model. Both the init and update functions will need to handle the new model field, and update will need to handle the new Message variants. In the UserChose case, set back to 0.
Where to add buttons and text. In the view function, you can add html.button items to the list of page elements. Add html.p for text.
Designing buttons. A button has a list of attributes and then a list of children. Here is a template for the "Previous round" button. You will need to fill in the first blank with a boolean value for whether the button should be disabled and the second with the message that should be sent when the button is clicked.
html.button([attribute.class("button"), attribute.disabled(____), event.on_click(____)], [
html.text("Previous round"),
])
Styling text. The message "(2 rounds ago)" can be added with this template. The "ago" class makes the line height for the message constant even if the text of the message is empty.
html.p([attribute.class("ago")], [
html.text(_____),
])
How to show previous rounds. To show a previous round, use the existing case model.rounds { ... } in view, which displays the first round in a list, but on a list whose first round is the one to display. You can use list.drop (docs) on model.rounds. (Add import gleam/list to the imports at the top of src/rps.gleam if you haven't already.)
gleam test -- historyQ4: Continue from Here
Add a "Continue from here" button that rewinds the game to the round being displayed, so that the user can play from that point as if the later rounds never happened. Like "Next round", it is only available when viewing some previous round.
Pressing "Continue from here" removes every round after the displayed one from the round history, so the displayed round becomes the latest round. Both scores change to count only the rounds that remain, and the "(N rounds ago)" message disappears.
How to handle the button press. Add one more Message variant.
How to update the model. The rounds are stored most recent first, so
list.drop (docs) can
be used to find the history that remains after forgetting some number of rounds.
Recompute both scores from the remaining rounds.
Where to add the button. Add it after the "Next round" button in view,
using the same template as in Q3. It is disabled under the same condition as
"Next round".
gleam test -- continueQ5: Extensions
Feel free to make the game more interesting. Some ideas:
You could have a goal score, and each point could move a player's avatar one more step around a racetrack toward the goal.
The computer's strategy is in src/agent.gleam. Each round, it picks one of
your last 10 choices at random and plays the choice that beats it. If you play
rock half the time, it plays paper about half the time. While no strategy is
better than any other against a random opponent, human players are typically not
random. There are lots of strategy ideas for this game shared as part of this
Kaggle
competition. You
could try to implement some.