COMP6991 Supplementary Exam

Getting Started

Create a new directory for this lab called exam_supp, change to this directory, and fetch the provided code for the exam by running these commands:

mkdir exam_supp
cd exam_supp
6991 fetch exam

Or, if you're not working on CSE, you can download the provided code as a tar file.

Exercise:
Exam Preamble

Starting time: 2026-09-11 14:00:00

Finishing time: 2026-09-11 17:00:00

Time for the exam: 3 hours

This exam contains 7 questions, each of equal weight (10 marks each).

Total number of marks: 70

Total number of practical programming questions: 3 (30 marks)

Total number of theoretical programming questions: 4 (40 marks)

You should attempt all questions.

Exam Condition Summary

  • This exam is "Open Book"
  • Joint work is NOT permitted in this exam
  • You are NOT permitted to communicate (email, phone, message, talk) with anyone during this exam, except for the COMP6991 staff via cs6991.exam@cse.unsw.edu.au
  • The exam paper is confidential, sharing it during or after the exam is prohibited.
  • You are NOT permitted to submit code that is not your own
  • You may NOT ask for help from online sources.
  • Even after you finish the exam, on the day of the exam, do NOT communicate your exam answers to anyone. Some students have extended time to complete the exam.
  • Do NOT place your exam work in any location, including file sharing services such as Dropbox or GitHub, accessible to any other person.
  • Your zpass should NOT be disclosed to any other person. If you have disclosed your zpass, you should change it immediately.
  • The use of AI assistants is strictly prohibited in this exam. This includes services such as Github Copilot, OpenAI ChatGPT, agentic coding tools, etc.
Deliberate violation of these exam conditions will be referred to Student Integrity Unit as serious misconduct, which may result in penalties up to and including a mark of 0 in COMP6991 and exclusion from UNSW.
  • You are allowed to use any resources from the course during the exam.
  • You are allowed to use small amounts of code (< 10 lines) of general-purpose code (not specific to the exam) obtained from a site such as Stack Overflow or other publicly available resources. You should attribute the source of this code clearly in an accompanying comment.

Exam submissions will be checked, both automatically and manually, for any occurrences of plagiarism.

By starting this exam, as a student of The University of New South Wales, you do solemnly and sincerely declare that you have not seen any part of this specific examination paper for the above course prior to attempting this exam, nor have any details of the exam's contents been communicated to you. In addition, you will not disclose to any University student any information contained in the abovementioned exam for a period of 24 hrs after the exam. Violation of this agreement is considered Academic Misconduct and penalties may apply.

For more information, read the UNSW Student Code, or contact the Course Account.

  • This exam comes with starter files.
  • You will be able to commence the exam and fetch the files once the exam commences.
  • You may complete the exam questions using any platform you wish (VLab, VSCode, etc). You should ensure that the platform works correctly.
  • You may submit your answers, using the give command provided below each question.
  • You can use give to submit as many times as you wish. Only the last submission will be marked.
  • Do NOT leave it to the deadline to submit your answers. Submit each question when you finish working on it.
  • Please make sure that you submit all your answers at the conclusion of the exam - running the autotests does not automatically submit your code.
  • Autotests are available for all practical questions to assist in your testing. You can use the command: 6991 autotest
  • Passing autotests does not guarantee any marks. Remember to do your own testing!
  • No marks are awarded for commenting - but you can leave comments for the marker to make your code more legible as needed

Language Restriction

  • All practical programming questions must be answered entirely in Rust; you may not submit code in any other programming languages.
  • You are not permitted to use third-party crates other than the standard library (std).

Fit to Sit

By sitting or submitting an assessment on the scheduled assessment date, a student is declaring that they are fit to do so and cannot later apply for Special Consideration.

If, during an exam a student feels unwell to the point that they cannot continue with the exam, they should take the following steps:

  1. Stop working on the exam and take note of the time
  2. Contact us immediately, using cs6991.exam@cse.unsw.edu.au, and advise us that you are unwell
  3. Immediately submit a Special Consideration application saying that you felt ill during the exam and were unable to continue
  4. If you were able to advise us of the illness during the assessment (as above), attach screenshots of this conversation to the Special Consideration application

Technical Issues

If you experience a technical issue, you should take the following steps:

  1. If your issue is with the connection to CSE, please follow the following steps:
    • If you are using VLab: Try exiting VLAB and reconnecting again - this may put you on a different server, which may improve your connection. If you are still experiencing problems, you can try changing how you connect to the CSE servers. Consider:
    • If you are using VSCode remote-ssh: Try disconnecting VSCode, and then changing the URL from vscode.unsw.edu.au to vscode2.unsw.edu.au.
    • If you are using SSH: Try disconnecting SSH and reconnecting again.
  2. If things are still NOT working, take screenshots of as many of the following as possible:
    • error messages
    • screen not loading
    • timestamped speed tests
    • power outage maps
  3. Contact should be made immediately to advise us of the issue at cs6991.exam@cse.unsw.edu.au
  4. A Special Consideration application should be submitted immediately after the conclusion of the assessment, along with the appropriate screenshots.

Exercise:
Q1: Theory (10 marks)

Q1.1 (2 marks)

This question will ask you to comment on the following Rust snippet, which does not compile.

fn main() {
    let mut numbers = vec![10, 20, 30];

    for n in &numbers {
        if *n == 20 {
            numbers.push(40);
        }
    }

    println!("{numbers:?}");
}

Question:

  1. Identify and briefly explain which feature of the Rust language is preventing this code from compiling. (1 mark)
  2. Suggest one way to fix this code so it compiles successfully, while still appending 40 to the Vec when the value 20 is present. (1 mark)

Write your answer in exam_q1/q1_1.txt.

When you are finished working on your answer, submit your work with give:

give cs6991 exam_q1_1 q1_1.txt


Q1.2 (4 marks)

Provided is a function that finds the first even number in a list, written in both Rust and C.

// Rust version
fn first_even(values: &[i32]) -> Option<i32> {
    for &v in values {
        if v % 2 == 0 {
            return Some(v);
        }
    }
    None
}

// C version
#include <stddef.h>

int *first_even(int *values, int n) {
    for (int i = 0; i < n; i++) {
        if (values[i] % 2 == 0) {
            return &values[i];
        }
    }
    return NULL;
}

Question:

  1. What does the return type Option<i32> express, and what must a caller do before it can use the value inside? (1 mark)
  2. The C version returns int *, which may be NULL. Explain the bug that can occur if a caller forgets to check for NULL. (1 mark)
  3. Explain how Rust's type system makes the equivalent mistake impossible to compile. (1 mark)
  4. Briefly argue one advantage or one disadvantage of Rust's Option-based approach compared to C's null pointers. (1 mark)

Write your answer in exam_q1/q1_2.txt.

When you are finished working on your answer, submit your work with give:

give cs6991 exam_q1_2 q1_2.txt


Q1.3 (4 marks)

Provided is the same function written in both Rust and C. In each version, the programmer has forgotten to give sum an initial value.

// Rust version
fn sum_positives(values: &[i32]) -> i32 {
    let mut sum;
    for &v in values {
        if v > 0 {
            sum += v;
        }
    }
    sum
}

// C version
int sum_positives(const int *values, int n) {
    int sum;
    for (int i = 0; i < n; i++) {
        if (values[i] > 0) {
            sum += values[i];
        }
    }
    return sum;
}

Question:

  1. State whether the C version compiles, and explain what happens if it is run. (1 mark)
  2. State whether the Rust version compiles, and explain what happens if it is run. (1 mark)
  3. Explain the philosophy behind Rust's behaviour compared to C in this situation. (1 mark)
  4. A colleague suggests that compilers should simply initialise every variable to 0 automatically, which would help avoid errors. Give one argument against taking that approach. (1 mark)

Write your answer in exam_q1/q1_3.txt.

When you are finished working on your answer, submit your work with give:

give cs6991 exam_q1_3 q1_3.txt


Exercise:
Q2: Practical (10 marks)

You are building the text-processing core of a small log tool. It pulls apart borrowed string data, picking out lines and tokens without ever copying the text.

We have written three items which are implemented correctly, but are missing lifetime annotations. Your task is to add the correct lifetime annotations.

use require_lifetimes::require_lifetimes;

/// Returns the first line of `text` that starts with `prefix`.
/// If no line matches, returns `fallback` instead.
/// (4 marks)
#[require_lifetimes]
pub fn first_line_or(text: &str, prefix: &str, fallback: &str) -> &str {
    for line in text.lines() {
        if line.starts_with(prefix) {
            return line;
        }
    }
    fallback
}

/// A scanner containing the delimiter characters,
/// and the text it has not scanned yet.
/// (2 marks: this struct and `new`)
pub struct Scanner {
    remaining: &str,
    delimiters: &str,
}

impl Scanner {
    #[require_lifetimes]
    pub fn new(source: &str, delimiters: &str) -> Scanner {
        Scanner {
            remaining: source,
            delimiters,
        }
    }

    /// Returns the run of characters up to the next delimiter.
    /// Leading delimiters are skipped. Returns `None` once no tokens remain.
    /// (4 marks)
    #[require_lifetimes]
    pub fn next_token(&mut self) -> Option<&str> {
        let delimiters = self.delimiters;
        let start = self.remaining.trim_start_matches(|c: char| delimiters.contains(c));
        if start.is_empty() {
            self.remaining = start;
            return None;
        }
        let end = start.find(|c: char| delimiters.contains(c)).unwrap_or(start.len());
        let (token, rest) = start.split_at(end);
        self.remaining = rest;
        Some(token)
    }
}

You must add lifetime annotations to:

  • first_line_or: returns the first matching line of text, or fallback if none match (4 marks)
  • Scanner and its new constructor: a scanner that holds the source text and the delimiters it splits on (2 marks)
  • next_token: a method that returns the next token from the source and advances past it (4 marks)

You are only permitted to add, remove, or modify lifetime annotations. You must not change any other code in src/lib.rs.

This is an example of the expected behaviour:

6991 cargo run --bin exam_q2
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/exam_q2`
First role line: role: admin
Tokens: the quick brown fox

6991 cargo run --bin exam_q2_alt
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/exam_q2_alt`
Found: name: Sam
Collected: ["a", "b", "c"]

Write your answer in exam_q2/src/lib.rs.

When you think your program is working, you can use autotest to run some simple automated tests:

6991 autotest

When you are finished working on your answer, submit your work with give:

cd src/
give cs6991 exam_q2 lib.rs


Exercise:
Q3: Theory (10 marks)

Q3.1 (4 marks)

Consider the following trait, used by a weather station to describe its sensors:

use std::fmt::Display;

trait Sensor: Display {
    fn id(&self) -> u32;

    fn report(&self) -> String {
        format!("sensor {}: {}", self.id(), self)
    }
}

Question:

  1. Identify everything that must be provided in order to implement Sensor for a custom type. (2 marks)
  2. Explain how it is possible for report to format self with {}, even though Self is not a concrete type. (1 mark)
  3. Explain how you could call report() on a value whose concrete type is not known until run time. (1 mark)

Write your answer in exam_q3/q3_1.txt.

When you are finished working on your answer, submit your work with give:

give cs6991 exam_q3_1 q3_1.txt


Q3.2 (3 marks)

The following program does not compile:

fn main() {
    let mut readings: Vec<f64> = vec![2.5, 1.0, 3.75];
    readings.sort();
    println!("{readings:?}");
}

Question:

  1. Identify the trait bound required by sort which is not satisfied. (1 mark)
  2. The sort method can be used on a Vec of many possible types. Does your program pay a runtime cost to work out which type it is sorting, and how to compare those elements? Justify your answer. (1 mark)
  3. Modify the call to sort (or replace it with a different method call) so that the readings are sorted in increasing order and the program compiles. An answer containing only the correctly modified code is sufficient. (1 mark)

Write your answer in exam_q3/q3_2.txt.

When you are finished working on your answer, submit your work with give:

give cs6991 exam_q3_2 q3_2.txt


Q3.3 (3 marks)

A programmer wants Vec<u32> to print nicely with {}, so they write the following in their own crate:

use std::fmt;

impl fmt::Display for Vec<u32> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "[a list of {} numbers]", self.len())
    }
}

The compiler rejects this impl block outright.

Question:

  1. Identify the rule being enforced here, and explain what problem the rule prevents. (2 marks)
  2. Describe a common pattern that lets the programmer achieve (nearly) the same goal without breaking this rule. (1 mark)

Write your answer in exam_q3/q3_3.txt.

When you are finished working on your answer, submit your work with give:

give cs6991 exam_q3_3 q3_3.txt


Exercise:
Q4: Theory (10 marks)

Q4.1 (4 marks)

Consider the following code, which reads and updates a shared bank balance:

use std::sync::Mutex;

fn log_balance(balance: &Mutex<i32>) {
    let current = balance.lock().unwrap();
    println!("Current balance: {}", *current);
}

fn main() {
    let balance = Mutex::new(100);

    let mut guard = balance.lock().unwrap();
    *guard += 10;

    log_balance(&balance);

    println!("Final balance: {}", *guard);
}

When run, this program hangs forever and never prints.

Question:

  1. Name this failure, and explain precisely why it occurs in this program. (2 marks)
  2. Rust famously prevents data races at compile time. Explain why the compiler does not prevent this failure. (1 mark)
  3. Suggest a change to this program that fixes the problem, and explain why it works. (1 mark)

Write your answer in exam_q4/q4_1.txt.

When you are finished working on your answer, submit your work with give:

give cs6991 exam_q4_1 q4_1.txt


Q4.2 (3 marks)

The following code attempts to count to 10000 using ten threads:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0u32));

    let mut handles = Vec::new();
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                let current = *counter.lock().unwrap();
                *counter.lock().unwrap() = current + 1;
            }
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Total: {}", *counter.lock().unwrap());
}

This program compiles without error, and contains no unsafe code. However, when run repeatedly, it sometimes prints totals less than 10000.

Question:

  1. One counter is shared between all ten threads. Explain why this compiles and is accepted as a thread-safe way to share the counter. (1 mark)
  2. Explain, with reference to a possible interleaving, why the total can be less than 10000. (1 mark)
  3. Fix the program so that it always prints 10000. An answer containing only the correctly modified code is sufficient. (1 mark)

Write your answer in exam_q4/q4_2.txt.

When you are finished working on your answer, submit your work with give:

give cs6991 exam_q4_2 q4_2.txt


Q4.3 (3 marks)

You are tasked with implementing a duration! macro that creates a std::time::Duration from a human-readable description.

The macro should use the following syntax, where each term is an integer followed by a unit (h for hours, m for minutes, or s for seconds), and terms are separated by commas:

let launch_hold = duration![90 s];
// launch_hold is Duration::from_secs(90)

let lecture = duration![1 h, 30 m];
// lecture is Duration::from_secs(5400)

let marathon = duration![2 h, 15 m, 30 s];
// marathon is Duration::from_secs(8130)

let tiktoks = duration![30 s, 1 m, 45 s, 15 s];
// tiktoks is Duration::from_secs(150)

Question:

Implement the duration! macro. Provide the full macro_rules! declaration.

You will receive (1 mark) for correctly implementing the macro with only single term support (i.e. duration![N unit]),

or (3 marks) for correctly implementing the macro supporting any number of comma-separated terms.

Partial marks may be awarded for progress made.


Write your answer in exam_q4/q4_3.txt.

When you are finished working on your answer, submit your work with give:

give cs6991 exam_q4_3 q4_3.txt


Exercise:
Q5: Practical (10 marks)

Robin is building a data-grouping library called Grouped, which implements a multimap. Internally it holds a HashMap<K, Vec<V>>, so each key names a group and each group holds any number of values. It provides convenient methods for inserting values into groups, transforming them, filtering whole groups, and reading the result back out.

Robin started by making the system work only with String keys and i32 values. However, users want to group other types too! Robin needs to make the struct and methods generic to accommodate this.

It's late, so Robin has asked you to help finish the implementation. All that's needed is to make it as generic as possible!

You have been given the following starter code in src/lib.rs:

use std::collections::HashMap;

pub struct Grouped {
    groups: HashMap<String, Vec<i32>>,
}

impl Grouped {
    pub fn new() -> Self {
        Self {
            groups: HashMap::new(),
        }
    }

    /// Insert `value` into the group named `key`.
    pub fn insert(&mut self, key: String, value: i32) {
        self.groups.entry(key).or_default().push(value);
    }

    /// Insert `value`, deriving its group key from the value itself.
    pub fn insert_with(&mut self, value: i32, key_of: fn(&i32) -> String) {
        let key = key_of(&value);
        self.insert(key, value);
    }

    /// Transform every value with `f`, producing a new Grouped with the same keys.
    pub fn map_values(self, mut f: fn(i32) -> i32) -> Grouped {
        let mut groups = HashMap::new();
        for (k, vs) in self.groups {
            let mapped = vs.into_iter().map(&mut f).collect();
            groups.insert(k, mapped);
        }
        Grouped { groups }
    }

    /// Keep only the groups for which `keep` returns true.
    pub fn retain_groups(&mut self, keep: fn(&String, &[i32]) -> bool) {
        self.groups.retain(|k, vs| keep(k, vs));
    }

    /// Consume into (key, values) pairs, sorted by key.
    pub fn into_sorted(self) -> Vec<(String, Vec<i32>)> {
        let mut v: Vec<_> = self.groups.into_iter().collect();
        v.sort_by(|a, b| a.0.cmp(&b.0));
        v
    }

    /// Total number of values across all groups.
    pub fn total(&self) -> usize {
        self.groups.values().map(|vs| vs.len()).sum()
    }
}

This code correctly implements Grouped for String keys and i32 values.

Your task is to make this struct and its methods far more generic. You will need to make the following changes:

  • Support keys of any type K and values of any type V, not just String and i32.
  • Allow map_values to change the value type to any output type U, not just i32.
  • Convert the function pointers to appropriate closure types.

You will be required to constrain some generic type parameters as you solve the exercise. You must ensure that you do not overly constrain the types, only requiring what is minimally needed. In particular:

  • Put trait bounds only on the individual methods that need them.
  • For each closure parameter, use the minimal closure trait (FnOnce, FnMut, or Fn) that its body requires.

The code already runs correctly on a basic test case:

6991 cargo run --bin exam_q5
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/exam_q5`
Total: 6
Sorted: [("even", [20, 40, 60]), ("odd", [10, 30, 50])]

However, the code does not yet typecheck on the other test cases provided. Once you have modified the types to make the Grouped as generic as possible, you should find these other test cases now compile and run:

6991 cargo run --bin exam_q5_alt
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/exam_q5_alt`
Total: 5
Sorted: [('a', [5, 7]), ('b', [6]), ('c', [6, 9])]
6991 cargo run --bin exam_q5_alt2
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/exam_q5_alt2`
Total after retain: 4
N: [1, 3]
S: [2, 5]
6991 cargo run --bin exam_q5_alt3
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/exam_q5_alt3`
Sorted: [("nums", [11, 22, 33])]
Counter: 3

Partial marks will be awarded for solutions which pass a subset of the test cases.


Write your answer in exam_q5/src/lib.rs.

When you think your program is working, you can use autotest to run some simple automated tests:

6991 autotest

When you are finished working on your answer, submit your work with give:

cd src/
give cs6991 exam_q5 lib.rs


Exercise:
Q6: Theory (10 marks)

Q6.1 (4 marks)

Rust has both unsafe fn (an unsafe function) and unsafe { } (an unsafe block). These are two different features that are easy to confuse.

Question:

  1. Explain what safety is each of the two features are responsible for upholding. (2 marks)
  2. Standard-library types like Vec<T> use considerable amounts of unsafe code internally, yet expose quite a wide "safe" public API. Explain how this is possible, and what obligation the author of such a type must uphold for that to be justified. (2 marks)

Write your answer in exam_q6/q6_1.txt.

When you are finished working on your answer, submit your work with give:

give cs6991 exam_q6_1 q6_1.txt


Q6.2 (6 marks)

The following code implements a Cursor type, making use of unsafe code.

/// A cursor that walks over a row of cells, where each cell is a growable
/// buffer of items. It hands out access to the cell at its current position.
pub struct Cursor<'a, T> {
    cells: &'a mut [Vec<T>],
    pos: usize,
}

impl<'a, T> Cursor<'a, T> {
    /// Creates a cursor positioned at the first cell.
    pub fn new(cells: &'a mut [Vec<T>]) -> Self {
        Cursor { cells, pos: 0 }
    }

    /// Returns a mutable reference to the cell at the cursor's position,
    /// so the caller can grow or edit it.
    pub fn current(&mut self) -> &'a mut Vec<T> {
        unsafe { &mut *self.cells.as_mut_ptr().add(self.pos) }
    }

    /// Returns a shared reference to the cell `offset` positions ahead
    /// of the cursor, for looking ahead without moving.
    pub fn peek(&self, offset: usize) -> &'a Vec<T> {
        unsafe { &*self.cells.as_ptr().add(self.pos + offset) }
    }

    /// Advances the cursor to the next cell.
    pub fn advance(&mut self) {
        self.pos += 1;
    }
}

The following example code aims to demonstrate how this type can be used:

use unsafe_review::Cursor;

fn main() {
    let mut cells = vec![Vec::new(), Vec::new(), Vec::new()];
    let mut cursor = Cursor::new(&mut cells);

    // Fill the cell at the cursor, then look ahead without moving.
    cursor.current().push(1);
    cursor.current().push(2);
    println!("cell at cursor:  {:?}", cursor.peek(0));
    println!("cell after that: {:?}", cursor.peek(1));

    // Move on and fill the next cell.
    cursor.advance();
    cursor.current().push(9);
    println!("cell at cursor:  {:?}", cursor.peek(0));
}

When executed, the program outputs:

cell at cursor:  [1, 2]
cell after that: []
cell at cursor:  [9]

The implementation appears to work correctly for this example.

Question:

  1. Perform a brief code review on the Cursor implementation, with respect to the idiomatic usage of unsafe Rust. Soundness should not be considered in this part. (1 mark)
  2. There exists a subtle unsoundness in Cursor, allowing a user to provoke a segmentation fault in safe Rust. Explain the soundness issue. (2 marks)
  3. Write an example fn main that exploits the unsoundness in Cursor to provoke a segmentation fault in safe Rust. (3 marks)
    An answer causing memory corruption other than a segmentation fault will be awarded only one mark.


Write your answer in exam_q6/q6_2.txt.

When you are finished working on your answer, submit your work with give:

give cs6991 exam_q6_2 q6_2.txt


Exercise:
Q7: Practical (10 marks)

2048

In this question you will implement the sliding-tile puzzle 2048, played on a 4×4 grid.

Each cell is either empty or holds a tile with a power-of-two value. A move slides every tile as far as it can in one direction. When two tiles of the same value collide, they merge into a single tile of twice the value. After a move that changes the board, one new tile appears. The goal is to create a tile with the value 2048.

Spawning new tiles

So that games are reproducible, the starter code provides a function you must use (and must not modify) to decide where new tiles appear:

pub fn next_spawn() -> (Coordinate, Value)

It returns the (pseudo-random) coordinate and value of the next tile to place. Use it like this:

  • At the start of the game, place two tiles by calling next_spawn twice.
  • After any move that changes the board, place one tile.
  • If next_spawn returns a coordinate that is already occupied, simply call it again and use the next result. Keep going until you get an empty cell.

Input Format

Your program reads commands from standard input, one per line:

  • L, R, U, D: make a move (left, right, up, down).
  • PRINT: print the current board.
  • SCORE: print the current score.

Output Format

PRINT outputs the 4×4 board as four lines. Each cell is right-aligned in a field four characters wide, and cells are separated by a single space. An empty cell is shown as a single dot . and an occupied tile as its number. Moves themselves produce no output (except possibly the end-condition messages in Stage 5).


Stage 1: Setup and Rendering (2 marks)

Start a game: place two tiles using next_spawn, then support the PRINT command.

Example

6991 cargo run
PRINT
   .    .    .    .
   2    .    2    .
   .    .    .    .
   .    .    .    .

Stage 2: Sliding (2 marks)

Implement moves that slide tiles (no merging yet). Every tile moves as far as it can in the chosen direction, with no gaps left between a tile and the edge it moved towards. After a move that changes the board, place one new tile with next_spawn.

Example

6991 cargo run
U
PRINT
   2    .    2    .
   .    2    .    .
   .    .    .    .
   .    .    .    .

(The two 2s slid to the top row, and a new tile appeared.)


Stage 3: Merging (2 marks)

When a move brings two tiles of equal value together, they merge into one tile of double the value. Merges are resolved starting from the edge the tiles move towards, and a tile that was just formed by a merge does not merge again on the same move (so a row 2 2 2 2 moved left becomes 4 4 . ., not 8 . . .).

Example

6991 cargo run
L
PRINT
   .    .    .    .
   4    2    .    .
   .    .    .    .
   .    .    .    .

(The two 2s merged into a 4, then a new tile appeared.)


Stage 4: Scoring (1 mark)

Track the score: each merge adds the value of the newly formed tile to it (so merging two 2s adds 4). Support the SCORE command, which prints Score: N.

Example

6991 cargo run
L
SCORE
Score: 4

Stage 5: End Conditions (3 marks)

Detect when the game ends, as the result of a move:

  • If that move creates a tile with value 2048, the game is won: print You win!.
  • Otherwise, if no move in any direction could now change the board, the game is lost: print You lost!.

In either case, immediately after the message, print the final board, print the final score, and exit. For example, a game that reaches 2048 ends like:

You win!
2048  256    .    .
  16   64   32    .
   8    4    .    .
   2    .    .    .
Score: 22368

A game with no moves left ends the same way, but with You lost! in place of You win!.


Summary of Marks

StageFeaturesMarks
1Setup and rendering (PRINT)2
2Sliding moves + spawning2
3Merging2
4Scoring (SCORE)1
5End conditions (You win! / You lost!)3
Total10

Partial marks may be awarded for partially working implementations within each stage.


Write your answer in exam_q7/src/main.rs.

When you think your program is working, you can use autotest to run some simple automated tests:

6991 autotest

When you are finished working on your answer, submit your work with give:

cd src/
give cs6991 exam_q7 main.rs


Submission

When you are finished each exercise make sure you submit your work by running give.

You can run give multiple times. Only your last submission will be marked.

Don't submit any exercises you haven't attempted.

Do not leave it to the deadline to submit your answers. Submit each question when you finish working on it. Running autotests does not automatically submit your code.

If you are working at home, you may find it more convenient to upload your work via give's web interface.

Remember you have until 2026-09-11 17:00:00, Sydney time, to complete this exam (not including any extra time provided by ELPs).

You cannot obtain marks by e-mailing your code to tutors or lecturers.

6991 classrun -check exam_q1_1
6991 classrun -check exam_q1_2
6991 classrun -check exam_q1_3
6991 classrun -check exam_q2
...
6991 classrun -check exam_q7

- End of examination. -