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 as class="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 -- update

Q2: 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/list to the imports at the top of src/rps.gleam and use list.map to 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_click with the message that update should receive when the user clicks this button.
  • The second blank is the button's child: call html.text on the emoji for choice.
/// 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_buttons

Submit 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 -- history

Q4: 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 -- continue

Q5: 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.


Back to Top

Accessibility Nondiscrimination

Copyright ©2026, Regents of the University of California and respective authors.

This site is built following the Berkeley Class Site template, which is generously based on the Just the Class, and Just the Docs templates.

View all course offerings