A Level Computer Science Paper 2 B: Algorithms and Programming
A full length H446/02 paper. Section A: computational thinking, recursion, parameter passing, OOP, Big O, insertion sort, Dijkstra's algorithm and A*, tree traversals, and programs for bracket matching, a recursive binary search and a circular queue. Section B: a word game to program in Python, with a 9 mark evaluation of object-oriented design.
Instructions
- Time allowed
- 2 hours 30 minutes
- Total marks
- 140
- Sections
- Section A, 90 marks; Section B, 50 marks
- Calculator
- You must not use a calculator.
- Answer all the questions.
- The number of marks for each question is shown in brackets. Use it as a guide to how much to write.
- The clock at the top counts down from the real time allowed. When it reaches zero the paper is locked and marked, as it would be collected in the exam hall.
- Write program code in Python, so it can be run and marked here. In the real exam you may answer in OCR Exam Reference Language or a high-level language.
- The 9 mark question is an extended response. In the real exam it is marked in levels; here every relevant point you make earns a mark, up to the total.
- Run every program on the examples in the question, and on the edge cases: an empty list, a single item, the first and the last. In Section B each function builds on the last, so read the whole scenario first.
Questions
Answer every question, then finish the paper to have it marked. Marking as you go teaches you far less.
Section A 90 marks
Answer all the questions in this section.
- LDetails that are not needed, such as buildings, trees or the exact shape of the land, are removed
- LOnly what the driver needs, such as the roads, junctions and distances, is kept, so the problem is simpler to solve
Everything that does not help plan a route, such as buildings, trees and the shape of the land, has been removed. Only the roads, the junctions and the distances between them are kept, which turns the real world into a much simpler problem the app can solve.
- LThe list items must be sorted
- LThe target and the items must be of the same type, so they can be compared (or the list must not be empty)
The list must already be sorted, and the target must be the same type as the items in the list, so that they can be compared.
- LA cached page is loaded from local storage instead of being downloaded again
- LSo it loads faster and uses less bandwidth
- LDrawback: the cached copy may be out of date, or the cache uses storage space
A page in the cache is loaded from the computer's own storage rather than downloaded again, so it appears faster and less bandwidth is used. But if the page has changed since it was cached, the browser may show an out of date version.
- LIdentify the parts of the task that can be carried out at the same time
- LThe images can be downloaded at the same time as each other, while the text is laid out
- LBecause these tasks do not depend on each other, the total time is reduced
Thinking concurrently means identifying the parts of the task that can happen at the same time. Each image can be requested and downloaded at the same time as the others, while the text of the page is being laid out. None of these tasks depends on the others, so overlapping them makes the page appear much sooner than doing them one after another.
- LThey save development time, because the code does not need to be written again
- LThey have already been tested, so they are reliable
Using a library subroutine saves the time it would take to write and test the code again, and because it has already been tested and used by many programmers it is likely to be reliable.
| Thinking ahead | Thinking procedurally | Thinking logically | Thinking concurrently | |
|---|---|---|---|---|
| Identifying the inputs and outputs a program needs before writing it | ||||
| Breaking a problem into sub-procedures that can each be solved separately | ||||
| Identifying the decision points in an algorithm and the conditions that decide them | ||||
| Identifying the parts of a problem that can be tackled at the same time |
Tick one box in each row.
- L1 mark for both: Identifying the inputs and outputs a program needs before writing it: Thinking ahead; Breaking a problem into sub-procedures that can each be solved separately: Thinking procedurally
- L1 mark for both: Identifying the decision points in an algorithm and the conditions that decide them: Thinking logically; Identifying the parts of a problem that can be tackled at the same time: Thinking concurrently
Inputs and outputs are thinking ahead, sub-procedures are thinking procedurally, decisions are thinking logically and simultaneous parts are thinking concurrently.
Figure 1
def mystery(n):
if n == 0:
return 0
else:
return n % 10 + mystery(n // 10)- L13
2 + mystery(47) = 2 + 7 + mystery(4) = 2 + 7 + 4 + mystery(0) = 13. The function adds up the digits.
- L4
mystery(472), mystery(47), mystery(4) and mystery(0): four calls.
def mystery(n):
if n == 0:
return 0
else:
return n % 10 + mystery(n // 10)
- LReturns 13 for 472
- LReturns 0 for 0
- LWorks for other numbers: 36 for 9999 and 1 for 1000000
- LUses iteration, and the function no longer calls itself
Take the last digit with % 10, add it to a running total, and remove it with // 10, until no digits are left.
def mystery(n):
total = 0
while n > 0:
total = total + n % 10
n = n // 10
return total- LAdvantage: recursive code is often shorter and more natural for problems defined in terms of themselves, such as traversing a tree
- LBecause the code mirrors the structure of the problem, it is easier to follow
- LDisadvantage: every call adds a stack frame, using more memory
- LSo deep recursion can cause a stack overflow, and the extra calls make it slower
For a problem that is defined in terms of smaller versions of itself, such as visiting every node of a tree, a recursive solution is short and mirrors the structure of the data, so it is easier to write and follow. But every recursive call puts a new frame on the call stack, holding its parameters, local variables and return address, so recursion uses more memory and time than a loop, and very deep recursion can overflow the stack.
procedure change(byVal a, byRef b)
a = a + 1
b = b + 1
endprocedure
x = 5
y = 5
change(x, y)
print(x)
print(y)- L5
- L6
x is passed by value, so the procedure changes only its own copy and x stays 5. y is passed by reference, so the change to b changes y, which becomes 6.
| Breakpoint | Watch window | Stepping | Syntax highlighting | |
|---|---|---|---|---|
| Pauses the program when it reaches a chosen line | ||||
| Displays the values of chosen variables as the program runs | ||||
| Runs the program one line at a time | ||||
| Shows keywords, strings and comments in different colours |
Tick one box in each row.
- L1 mark for both: Pauses the program when it reaches a chosen line: Breakpoint; Displays the values of chosen variables as the program runs: Watch window
- L1 mark for both: Runs the program one line at a time: Stepping; Shows keywords, strings and comments in different colours: Syntax highlighting
Breakpoints pause, watches show values, stepping runs line by line, and highlighting colours the code.
Figure 2
class Animal:
def __init__(self, name):
self.__name = name
def getName(self):
return self.__name
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return "Woof"Tick one box.
- LB Polymorphism
Dog overrides the speak method it inherits, so the same call gives different behaviour depending on the object's class: polymorphism.
- LThe attribute name is private (__name), so it cannot be accessed directly from outside the class
- LIt can only be read through the public method getName
The attribute __name is private, so code outside the class cannot read or change it directly. The only way to get the name is through the public method getName, so the class controls how its data is used.
| Class | Object | Method | Inheritance | |
|---|---|---|---|---|
| A template that defines the attributes and methods of a type of object | ||||
| An instance of a class | ||||
| A subroutine that belongs to a class | ||||
| Creating a new class from an existing one, so it gains its attributes and methods |
Tick one box in each row.
- L1 mark for both: A template that defines the attributes and methods of a type of object: Class; An instance of a class: Object
- L1 mark for both: A subroutine that belongs to a class: Method; Creating a new class from an existing one, so it gains its attributes and methods: Inheritance
A class is the template, an object an instance of it, a method a subroutine in it, and inheritance builds a class on another.
- LA global variable can be changed by any part of the program, so errors caused by unexpected changes are hard to trace
- LSubroutines that use globals are harder to reuse and test separately, and the variable uses memory for the whole run
Because any subroutine can change a global variable, an unexpected change is hard to trace back to its cause. Subroutines that depend on globals are also harder to test separately and reuse in other programs.
Figure 3
4
/ \
2 7
/ \
5 9def countNodes(tree):
# Return the number of nodes in tree
pass
- LReturns 0 for an empty tree, None
- LReturns 1 for a tree of one node
- LCounts the nodes in both subtrees: 5 for the tree in Figure 3
- LIs recursive: countNodes calls itself
The base case is the empty tree, with 0 nodes. Any other tree has 1 node, its root, plus the nodes in its left and right subtrees.
def countNodes(tree):
if tree is None:
return 0
left, value, right = tree
return 1 + countNodes(left) + countNodes(right)| O(1) | O(log n) | O(n) | O(n log n) | O(n²) | |
|---|---|---|---|---|---|
| A binary search | |||||
| A bubble sort, in the worst case | |||||
| A merge sort | |||||
| Reading an item from an array by its index |
Tick one box in each row.
- LA binary search: O(log n)
- LA bubble sort, in the worst case: O(n²)
- LA merge sort: O(n log n)
- LReading an item from an array by its index: O(1)
Binary search halves the list each time, bubble sort compares every pair, merge sort does n work on each of log n levels, and indexing is a single step.
- LThe time an O(n²) algorithm takes grows with the square of the number of items, much faster than n log n
- LSo for large n the O(n log n) algorithm needs far fewer steps: for a million items, about 20 million against a million million
The time taken by an O(n²) algorithm grows with the square of the number of items, so doubling the data quadruples the time. For a million items, n² is a million million steps while n log n is about 20 million, so the O(n log n) algorithm is many thousands of times faster.
| Position 0 | Position 1 | Position 2 | Position 3 | Position 4 | |
|---|---|---|---|---|---|
| After pass 1 | |||||
| After pass 2 | |||||
| After pass 3 | |||||
| After pass 4 |
- LRow 1: 3, 7, 9, 1, 5
- LRow 2: 3, 7, 9, 1, 5
- LRow 3: 1, 3, 7, 9, 5
- LRow 4: 1, 3, 5, 7, 9
Pass 1 inserts 3 before 7. Pass 2 leaves 9 where it is. Pass 3 moves 1 to the front. Pass 4 inserts 5 between 3 and 7.
Tick one box.
- LA O(n²)
If every pivot is the largest or smallest item, each partition removes only one item, giving O(n²).
Figure 4
| Edge | A-B | A-C | B-C | B-D | C-D | C-E | D-E | D-F | E-F |
|---|---|---|---|---|---|---|---|---|---|
| Weight | 4 | 2 | 1 | 5 | 8 | 10 | 2 | 6 | 2 |
- LB and C: 3, 2
- LD and E: 8, 10
- LF: 12
C is 2. B is 3, through C (2 + 1), which beats the direct 4. D is 8, through B (3 + 5). E is 10, through D (8 + 2), which beats 12 through C. F is 12, through E (10 + 2), which beats 14 through D.
- LA, C, B, D, E, F
A, C, B, D, E, F, with a length of 12.
- LA* uses a heuristic: an estimate of the distance from each node to the goal
- LIt visits next the node with the lowest f = g + h: the distance so far plus the estimate
- LSo it heads towards the goal and usually visits fewer nodes, finding one route faster; Dijkstra finds the shortest path to every node
Dijkstra's algorithm always visits the unvisited node with the smallest distance from the start, so it spreads out in every direction and finds the shortest path to every node. A* adds a heuristic, an estimate of the remaining distance to the goal, and visits the node with the smallest total of distance so far plus estimate. That steers the search towards the goal, so it usually visits far fewer nodes to find a route to one destination.
Figure 5
A
/ \
B C
/ \ \
D E F| Traversal | 1st | 2nd | 3rd | 4th | 5th | 6th |
|---|---|---|---|---|---|---|
| Breadth-first | ||||||
| Depth-first |
- LRow 1: A, B, C, D, E, F
- LRow 2: A, B, D, E, C, F
Breadth-first visits each level in turn: A, then B and C, then D, E and F. Depth-first goes as deep as it can down the left first: A, B, D, E, then C and F.
- LA binary search must go straight to the middle item, which needs direct access by index
- LIn a linked list an item can only be reached by following the pointers from the start, so finding the middle takes linear time
A binary search works by jumping straight to the middle item, which needs direct access by position, as an array gives. A linked list can only be followed node by node from its start, so just reaching the middle takes as long as a linear search.
def isBalanced(text):
# Use a list as a stack
pass
- LReturns True for balanced brackets: (a[b]{c})
- LReturns True for a string with no brackets, and for an empty string
- LReturns False when a bracket is closed by the wrong type: (]
- LReturns False when brackets close in the wrong order: ([)]
- LReturns False when a bracket is never closed: ((x)
- LReturns False when a closing bracket has nothing to match: x)(
Push each opening bracket. For each closing bracket, the stack must not be empty and the bracket popped must be its partner. At the end, the stack must be empty.
def isBalanced(text):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for character in text:
if character in "([{":
stack.append(character)
elif character in ")]}":
if len(stack) == 0 or stack.pop() != pairs[character]:
return False
return len(stack) == 0def binarySearch(items, target, low, high):
# Return the index of target, or -1
pass
- LFinds an item in the middle: 25 is at index 4
- LFinds the first and the last items
- LFinds items in either half
- LReturns -1 when the item is not there, including for an empty list
- LIs recursive, and searches only between low and high
The base case is low > high: nothing is left to search. Otherwise compare the middle item and recurse on the half that could hold the target, moving low or high past the middle.
def binarySearch(items, target, low, high):
if low > high:
return -1
mid = (low + high) // 2
if items[mid] == target:
return mid
elif items[mid] < target:
return binarySearch(items, target, mid + 1, high)
else:
return binarySearch(items, target, low, mid - 1)def insertionSort(items):
for i in range(1, len(items)):
current = items[i]
# Complete the procedure: move each item before position i that is
# larger than current one place to the right, then put current
# into the gap that is left
pass
- LSorts the list in place: 7, 3, 9, 1, 5
- LSorts a list in reverse order, and one with repeated values
- LCopes with an empty list and a list of one item
- LKeeps the outer loop of the insertion sort, and uses no built-in sort
Shift each larger item one place right, working back from position i - 1, then drop current into the gap.
def insertionSort(items):
for i in range(1, len(items)):
current = items[i]
position = i
while position > 0 and items[position - 1] > current:
items[position] = items[position - 1]
position = position - 1
items[position] = current- LMerge sort creates new lists as it splits the list and merges the parts
- LSo it needs extra memory in proportion to the size of the list, while insertion sort only moves items within the original list
Merge sort builds new lists as it splits the data and merges the sorted halves, so it needs extra memory in proportion to the number of items. Insertion sort works in place, moving items within the original list, so it needs only one extra variable.
total = 0
for i in range(n):
for j in range(n):
total = total + i * jTick one box.
- LC O(n²)
The inner loop runs n times for each of the n passes of the outer loop: n x n steps.
while n > 1:
n = n // 2Tick one box.
- LD O(log n)
n is halved each time round the loop, so it repeats about log2 n times.
- LAn overestimate can make a node on the true shortest path look worse than it is
- LSo the algorithm may reach the goal by another route first, and stop before exploring the shortest path
A* explores the nodes with the smallest estimated total first. If the heuristic overestimates, a node on the genuinely shortest path can look more expensive than it is and be left unexplored, so the algorithm may reach the goal along a longer route first and stop there.
class Queue:
def __init__(self, size):
self.items = [None] * size
self.size = size
self.front = 0
self.rear = -1
self.count = 0
def isEmpty(self):
return self.count == 0
def isFull(self):
return self.count == self.size
def enqueue(self, item):
# Add item at the rear, wrapping round to position 0.
# Return False if the queue is full, otherwise True.
pass
def dequeue(self):
# Remove and return the item at the front, wrapping round.
# Return None if the queue is empty.
pass
- Lenqueue and dequeue give items back in first in, first out order
- Lenqueue returns True when the item is added
- Lenqueue returns False when the queue is full, and does not add the item
- Ldequeue returns None when the queue is empty
- LWraps round: when the end of the list is reached, the next item goes in position 0
- LThe front pointer wraps round, so items keep coming out in order
Check for a full or empty queue first. Move the pointer on with (pointer + 1) % size, which wraps from the last position back to 0, and keep count up to date.
class Queue:
def __init__(self, size):
self.items = [None] * size
self.size = size
self.front = 0
self.rear = -1
self.count = 0
def isEmpty(self):
return self.count == 0
def isFull(self):
return self.count == self.size
def enqueue(self, item):
if self.isFull():
return False
self.rear = (self.rear + 1) % self.size
self.items[self.rear] = item
self.count = self.count + 1
return True
def dequeue(self):
if self.isEmpty():
return None
item = self.items[self.front]
self.front = (self.front + 1) % self.size
self.count = self.count - 1
return item- LIn a linear queue, positions freed at the front by dequeuing cannot be used again
- LA circular queue wraps its pointers round to the start of the array, so every position is reused
In a linear queue the front pointer moves along the array as items are removed, so the positions behind it are never used again and the queue soon reports itself full. A circular queue wraps both pointers back to position 0, so the freed positions are reused.
Section B 50 marks
Answer all the questions in this section. They are about a word game.
A company is writing a word game for two or more players. Each player has a rack of seven letter tiles. On their turn, a player makes a word from the tiles on their rack and scores the total of the letter values in the word. A word of seven or more letters scores double.
The letter values are stored in a dictionary called VALUES:
VALUES = {"A": 1, "B": 3, "C": 3, "D": 2, "E": 1, "F": 4, "G": 2, "H": 4, "I": 1,
"J": 8, "K": 5, "L": 1, "M": 3, "N": 1, "O": 1, "P": 3, "Q": 10, "R": 1,
"S": 1, "T": 1, "U": 1, "V": 4, "W": 4, "X": 8, "Y": 4, "Z": 10}- LThe player's name
- LThe player's score
- LThe tiles on the player's rack
Each player's name, their score and the tiles on their rack.
VALUES = {"A": 1, "B": 3, "C": 3, "D": 2, "E": 1, "F": 4, "G": 2, "H": 4, "I": 1,
"J": 8, "K": 5, "L": 1, "M": 3, "N": 1, "O": 1, "P": 3, "Q": 10, "R": 1,
"S": 1, "T": 1, "U": 1, "V": 4, "W": 4, "X": 8, "Y": 4, "Z": 10}
def wordScore(word):
# Return the score for word
pass
- LReturns the total of the letter values: CAT scores 5
- LCounts every letter, including repeats: QUIZ scores 22 and BOOK 10
- LScores lower case letters as upper case: cat scores 5
- LDoubles a word of 7 or more letters: EXAMPLE scores 36 and QUESTION 34
- LDoes not double a word of 6 letters: PUZZLE scores 26
Convert the word to upper case, add up VALUES[letter] for each letter, then double the total if len(word) >= 7.
VALUES = {"A": 1, "B": 3, "C": 3, "D": 2, "E": 1, "F": 4, "G": 2, "H": 4, "I": 1,
"J": 8, "K": 5, "L": 1, "M": 3, "N": 1, "O": 1, "P": 3, "Q": 10, "R": 1,
"S": 1, "T": 1, "U": 1, "V": 4, "W": 4, "X": 8, "Y": 4, "Z": 10}
def wordScore(word):
total = 0
for letter in word.upper():
total = total + VALUES[letter]
if len(word) >= 7:
total = total * 2
return totalVALUES = {"A": 1, "B": 3, "C": 3, "D": 2, "E": 1, "F": 4, "G": 2, "H": 4, "I": 1,
"J": 8, "K": 5, "L": 1, "M": 3, "N": 1, "O": 1, "P": 3, "Q": 10, "R": 1,
"S": 1, "T": 1, "U": 1, "V": 4, "W": 4, "X": 8, "Y": 4, "Z": 10}
def canMake(word, rack):
# Return True if word can be made from the tiles in rack
pass
- LReturns True when every letter is on the rack: CAT from TACKLES
- LReturns False when a letter is not on the rack: DOG from TACKLES
- LUses each tile only once: BOOK cannot be made from BOKERST
- LAllows a repeated letter when there are enough tiles: BOOK from BOOKEND
- LCan use every tile on the rack: TACKLES from SLACKET
- LAccepts a word in lower case: cat from TACKLES
Copy the rack into a list of tiles and remove each letter of the word as it is used, so that a tile cannot be used twice. If a letter is not there, the word cannot be made.
VALUES = {"A": 1, "B": 3, "C": 3, "D": 2, "E": 1, "F": 4, "G": 2, "H": 4, "I": 1,
"J": 8, "K": 5, "L": 1, "M": 3, "N": 1, "O": 1, "P": 3, "Q": 10, "R": 1,
"S": 1, "T": 1, "U": 1, "V": 4, "W": 4, "X": 8, "Y": 4, "Z": 10}
def canMake(word, rack):
tiles = list(rack)
for letter in word.upper():
if letter in tiles:
tiles.remove(letter)
else:
return False
return TrueVALUES = {"A": 1, "B": 3, "C": 3, "D": 2, "E": 1, "F": 4, "G": 2, "H": 4, "I": 1,
"J": 8, "K": 5, "L": 1, "M": 3, "N": 1, "O": 1, "P": 3, "Q": 10, "R": 1,
"S": 1, "T": 1, "U": 1, "V": 4, "W": 4, "X": 8, "Y": 4, "Z": 10}
def wordScore(word):
total = 0
for letter in word.upper():
total = total + VALUES[letter]
if len(word) >= 7:
total = total * 2
return total
def canMake(word, rack):
tiles = list(rack)
for letter in word.upper():
if letter in tiles:
tiles.remove(letter)
else:
return False
return True
def bestWord(words, rack):
# Return the best word that can be made
pass
- LReturns the highest scoring word that can be made
- LIgnores a higher scoring word that cannot be made
- LReturns an empty string when no word can be made
- LReturns the first of two words with the same score
- LReturns an empty string for an empty list of words
- LUses the functions canMake and wordScore
Keep the best word so far and its score, starting from an empty string and 0. Replace them only when a word can be made and scores strictly more, so the first of two equal words is kept.
VALUES = {"A": 1, "B": 3, "C": 3, "D": 2, "E": 1, "F": 4, "G": 2, "H": 4, "I": 1,
"J": 8, "K": 5, "L": 1, "M": 3, "N": 1, "O": 1, "P": 3, "Q": 10, "R": 1,
"S": 1, "T": 1, "U": 1, "V": 4, "W": 4, "X": 8, "Y": 4, "Z": 10}
def wordScore(word):
total = 0
for letter in word.upper():
total = total + VALUES[letter]
if len(word) >= 7:
total = total * 2
return total
def canMake(word, rack):
tiles = list(rack)
for letter in word.upper():
if letter in tiles:
tiles.remove(letter)
else:
return False
return True
def bestWord(words, rack):
best = ""
bestScore = 0
for word in words:
if canMake(word, rack) and wordScore(word) > bestScore:
best = word
bestScore = wordScore(word)
return best- LA hash table finds a word in constant time, O(1), by calculating its position from the word
- LA linear search is O(n): it may compare the word with all 270,000 words
- LSo checking a word is much faster, which matters because a check is made on every turn
A hash table calculates where a word would be stored from the word itself, so a lookup takes constant time, O(1), however many words there are. A linear search is O(n): a word near the end, or one that is not valid, means comparing it with all 270,000 words. Words are checked on every turn, so the hash table keeps the game responsive.
VALUES = {"A": 1, "B": 3, "C": 3, "D": 2, "E": 1, "F": 4, "G": 2, "H": 4, "I": 1,
"J": 8, "K": 5, "L": 1, "M": 3, "N": 1, "O": 1, "P": 3, "Q": 10, "R": 1,
"S": 1, "T": 1, "U": 1, "V": 4, "W": 4, "X": 8, "Y": 4, "Z": 10}
def wordScore(word):
total = 0
for letter in word.upper():
total = total + VALUES[letter]
if len(word) >= 7:
total = total * 2
return total
class Player:
# Write the class here
pass
- LA new player's score is 0
- LgetName returns the name given to the constructor
- LaddWord returns the score for the word: 5 for CAT
- LaddWord adds the word's score to the player's score
- LThe score is a running total over several words
- LEach player has a score of their own
- LThe name and the score are private attributes
- LaddWord uses the function wordScore
Two underscores make an attribute private in Python. addWord works out the word's score once, adds it to the running total and returns it.
VALUES = {"A": 1, "B": 3, "C": 3, "D": 2, "E": 1, "F": 4, "G": 2, "H": 4, "I": 1,
"J": 8, "K": 5, "L": 1, "M": 3, "N": 1, "O": 1, "P": 3, "Q": 10, "R": 1,
"S": 1, "T": 1, "U": 1, "V": 4, "W": 4, "X": 8, "Y": 4, "Z": 10}
def wordScore(word):
total = 0
for letter in word.upper():
total = total + VALUES[letter]
if len(word) >= 7:
total = total * 2
return total
class Player:
def __init__(self, name):
self.__name = name
self.__score = 0
def getName(self):
return self.__name
def getScore(self):
return self.__score
def addWord(self, word):
points = wordScore(word)
self.__score = self.__score + points
return pointsVALUES = {"A": 1, "B": 3, "C": 3, "D": 2, "E": 1, "F": 4, "G": 2, "H": 4, "I": 1,
"J": 8, "K": 5, "L": 1, "M": 3, "N": 1, "O": 1, "P": 3, "Q": 10, "R": 1,
"S": 1, "T": 1, "U": 1, "V": 4, "W": 4, "X": 8, "Y": 4, "Z": 10}
def wordScore(word):
total = 0
for letter in word.upper():
total = total + VALUES[letter]
if len(word) >= 7:
total = total * 2
return total
class Player:
def __init__(self, name):
self.__name = name
self.__score = 0
def getName(self):
return self.__name
def getScore(self):
return self.__score
def addWord(self, word):
points = wordScore(word)
self.__score = self.__score + points
return points
class Game:
def __init__(self, players):
# players is a list of Player objects, in the order they take turns
pass
def nextPlayer(self):
# Return the player whose turn it is, and move them to the back of the queue
pass
def winner(self):
# Return the name of the player with the highest score
pass
- LnextPlayer returns the first player in the list first
- LThe players then take turns in order
- LAfter the last player, the first player has a turn again
- LWorks with two players
- Lwinner returns the name of the player with the highest score
- Lwinner works when the first player has the highest score
- Lwinner is still right after turns have moved the queue round
A list used as a queue: nextPlayer removes the player at the front and appends them to the back, so the order cycles. winner looks at every player in the queue, whatever order it is in.
VALUES = {"A": 1, "B": 3, "C": 3, "D": 2, "E": 1, "F": 4, "G": 2, "H": 4, "I": 1,
"J": 8, "K": 5, "L": 1, "M": 3, "N": 1, "O": 1, "P": 3, "Q": 10, "R": 1,
"S": 1, "T": 1, "U": 1, "V": 4, "W": 4, "X": 8, "Y": 4, "Z": 10}
def wordScore(word):
total = 0
for letter in word.upper():
total = total + VALUES[letter]
if len(word) >= 7:
total = total * 2
return total
class Player:
def __init__(self, name):
self.__name = name
self.__score = 0
def getName(self):
return self.__name
def getScore(self):
return self.__score
def addWord(self, word):
points = wordScore(word)
self.__score = self.__score + points
return points
class Game:
def __init__(self, players):
self.__queue = list(players)
def nextPlayer(self):
player = self.__queue.pop(0)
self.__queue.append(player)
return player
def winner(self):
best = self.__queue[0]
for player in self.__queue:
if player.getScore() > best.getScore():
best = player
return best.getName()- LEach word played, with its score, is pushed onto a stack
- LTo undo, the top item is popped: the most recent word
- LIts score is taken off the player's score, and its tiles are returned to the rack
- LBecause a stack is last in, first out, repeated undos remove the words in the reverse of the order they were played
Every time a word is played it is pushed onto a stack along with its score. Undo pops the top item, which is always the most recent word, subtracts its score from the player's total and puts its tiles back on the rack. Because a stack is last in, first out, pressing undo again removes the word before that, so the words are undone in the reverse order to the one they were played in.
- LClasses such as Player and Game model the things in the game, so the design matches the problem
- LEncapsulation protects data such as a player's score from being changed incorrectly
- LAny number of players can be created as instances of one class, each holding its own data
- LInheritance would let a ComputerPlayer class reuse Player's code
- LPolymorphism: a computer player could override a method, such as choosing its word, and be used in the same way
- LClasses can be written and tested separately, by different programmers, which helps maintenance
- LBut object-oriented design needs more planning, and adds code and complexity
- LA procedural approach is simpler for small parts, such as wordScore, which need no class
- LA justified conclusion: object-oriented programming suits this game
Object-oriented programming suits the game because its design can mirror the problem: there are players, a game and a queue of turns, and each becomes a class. Encapsulation keeps each player's score private, so it can only change through addWord, which stops other parts of the program corrupting it. Any number of players can be created as instances of Player, each with its own name and score, without any extra code. The design is also easy to extend. A ComputerPlayer class could inherit everything from Player and override a method to choose its own word, and through polymorphism the Game class would treat it exactly like a human player. Because each class is self-contained it can be written and tested separately, so a team can share the work and later changes are less likely to break other parts. There are costs. The classes and their relationships need careful design before coding starts, and for a small program the classes add code and complexity. Parts of the game, such as wordScore and canMake, are simple calculations that are clearer as ordinary functions, as a procedural approach would write them. Overall an object-oriented approach is the better choice for the game as a whole, because it has several kinds of object with their own data and behaviour and is likely to grow, while small stateless calculations can stay as plain functions.
This is an original paper modelled on the real OCR H446/02 past papers: the same structure, the same balance of topics, the same command words and mark tariffs, 140 marks in 2 hours 30 minutes, exactly as the real paper allows. It is not a past paper. Real past papers are the copyright of the exam board and cannot be republished here, so links to the official papers on the board's own site are on the exam papers page.