Stop Thief! is an out-of-print board game by Parker Brothers released in the late 1970s. The game relies on a hand-held electronic device (see Fig. 1) that acts as the thief, using sounds and numbers corresponding to board spaces to give clues to players as to where the thief is on the board (see Fig. 2). Players use these clues to track the thief, and use their corresponding token and dice to advance to the perceived thief location to "arrest" the thief. If the electronic hand-held "Electronic Crime Scanner" breaks, the game is effectively unplayable. This iOS app simulates the logic of the Crime Scanner and modernizes it for today's mobile Apple devices.


The StopThiefDataTest is a proof of concept Swift/Xcode project that hosts and validates the thief logic for the app.
Source code can be found on Github
BuildingStreet
The game utilizes a three-digit number to indicate a potential location of the thief. The first of the three numbers represents the building or street, with the final two representing the board space within that building or street (see Fig. 2 above).
This enumeration lists the buildings and streets and assigns them the corresponding number.
enum BuildingStreet: Int {
case furs = 1
case jewelry = 2
case antiques = 3
case bank = 4
case fifthStreet = 5
case sixthStreet = 6
case seventhStreet = 7
case eighthStreet = 8
}
LocationType
Enumeration representing the type of board space; the thief can start or move to a board space visually represented by one of the following:
enum LocationType:String {
case crime
case floor
case door
case window
case street
case subway
case donothing
}
Location
When the user starts the game, the app initializes the game board as an array in memory, each numbered board space represented by a Location structure. The app also keeps track of the thief's movements with an array containing copies of the Location structure
Variables:
var locationNumber: Int
var buildingStreet: BuildingStreet
var locationType: LocationType // if LocationType.crime, and a crime occurs here, change to floor/street
var adjacentLocations: [Int] //stores array of numbers representing board spaces
Initialize locationNumber, buildingStreet, locationType and adjacentLocations with corresponding values passed to the constructor.
init(locationNumber: Int, buildingStreet:BuildingStreet, locationType:LocationType, adjacentLocations: [Int]){
self.locationNumber = locationNumber
self.buildingStreet = buildingStreet
self.locationType = locationType
self.adjacentLocations = adjacentLocations
}
If the board space locationType is crime, the app calls this function to change its type to just a regular floor or street space. The game rules indicate that once the thief commits a crime at that board space, they cannot return to that space to commit another crime (the jewels, money, etc., have been stolen and cleared out previously), so if the thief moves to that board space again, it is treated as a regular building or street space.
mutating func changeLocationType(){
if buildingStreet == BuildingStreet.seventhStreet{
locationType = LocationType.street
}
else{
locationType = LocationType.floor
}
}
GreaterOrLessThan
When the thief moves to a door or window board space, they must move through it on the next turn, barring staying in that board space. Board spaces on opposite sides of doors and windows are numbered such that board spaces on one side are always greater than or less the board spaces on the other. The GameState class uses this enumeration when determining in which direction the thief must move.
enum GreaterOrLessThan:Int{
case greaterThan
case lessThan
}
GameState
This class sets up and stores the initial state of the game board and drives gameplay in advancing the thief between board spaces.
Variables and Constants:
let MAX_NUM_CRIMESCENES = 19
let PERCENT_CHANCE_THIEF_DOES_NOT_MOVE = 10
var gameBoard: [Int: Location] = [:] //stores Location spaces where thief might be
let crimeScenes = [123,144,146,164,242,245,247,265,267,337,352,355,376,425,445,463,465,467,709]
var currentLocation = 0
var previousLocation = 0
The constructor calls createGameboard in preparation to start gameplay.
init(){
createGameboard()
}
Called by the constructor. createGameboard aggregates calls to functions that add all the numbered board spaces to the gameBoard array, representing the spaces where the thief can move.
This function initially calls the function insertLocation inserting a location number of 0, which represents when the thief does not move on their turn. In another class, the app uses an array to keep track of the board spaces that represent each turn the thief takes. On turns when the thief does not move, the app still needs to store something in this array to represent their turn. In these cases, the app stores a copy of Location with location number 0. Subsequently, createGameboard calls addFurriers, addJewelers, addAntiques, addBank, addFifthStreet, addSixthStreet, addSeventhStreet and addEighthStreet to populate the Location array.
func createGameboard(){
//insert a "do nothing" location at index 0
//this occurs when 10% of the time the thief does not move;
//this "action" is still recorded in the thief's move history so needs a location
//corresponding to a sound
insertLocation(locationNumber: 0, buildingStreet: BuildingStreet.furs, locationType: LocationType.donothing, adjacentLocations: [])
//add all the remaining game board locations
addFurriers()
addJewelers()
addAntiques()
addBank()
addFifthStreet()
addSixthStreet()
addSeventhStreet()
addEighthStreet()
}
Each of these functions repeatedly calls insertLocation passing the board space number, the corresponding building/street, board space type, and an array of the adjacent board spaces that the thief could move to on their next turn.
This function adds a Location object into the gameBoard array at an index corresponding to the board space three-digit number, and populates that Location with its corresponding building/street, the board space type (subway, building, etc.), and an array of the adjacent board spaces that the thief could move on their next turn.
func insertLocation(locationNumber:Int, buildingStreet:BuildingStreet, locationType:LocationType, adjacentLocations:[Int]){
gameBoard[locationNumber] = Location(locationNumber: locationNumber, buildingStreet: buildingStreet, locationType: locationType, adjacentLocations: adjacentLocations)
}
This function returns a three-digit number representing the starting board space of the thief. The function determines this by randomly choosing a number from the crimeScenes array.
func getFirstClue() -> Int{
currentLocation = crimeScenes[getRandomNumber(max: MAX_NUM_CRIMESCENES)]
return currentLocation
}
This function returns a three-digit number representing the new location of the thief. The function first does a random number check to see if the thief is skipping their turn and standing still, in which getClue returns case board space location number 0. getClue then determines if the thief's current board space is a crime, and if so, calls changeLocationType to change the location type of the current Location array board space to a regular street or building floor board space as the thief cannot steal from a crime square they've already been to. getClue then calls getNextLocation to retrieve a new board space location where the thief will move. getClue then assigns the current Location to the previous Location, and sets the new board space Location number to the currentLocation and returns that.
func getClue()->Int{
var nextLocation = 0
//check to see if thief does nothing
if getRandomNumber(max: PERCENT_CHANCE_THIEF_DOES_NOT_MOVE) == 0{
return 0
}
if gameBoard[currentLocation]?.locationType==LocationType.crime{
gameBoard[currentLocation]?.changeLocationType()
}
nextLocation=getNextLocation()
previousLocation = currentLocation
currentLocation=nextLocation
return currentLocation
}
getClue calls this function to perform extensive checks on the adjacent locations of the thief to determine the their next move. getNextLocation gets a copy of the array of adjacentLocations to the current Location; these represent all the possible board spaces to where the thief can move on this turn.
getNextLocation then removes the previousLocation from the adjacentLocations array as this is an invalid move.
getNextLocation then determines if the current location is a window or door. If so, the thief must continue through it to the other side, so getNextLocation calls getLocationsOnOtherSideOfDoorWindow which will return a subset of the adjacent Locations array from which to randomly choose a new board space.
getNextLocation then determines if the current location is a subway. If so, it calls dealWithSubwayTravel to get a subset of the adjacentLocations array from which to randomly choose a new board space.
getNextLocation then determines if any of the board spaces in the adjacent Locations array is a crime type by calling lookForAdjacentCrimeLocations which will return a corresponding array.
getNextLocation then calls getRandomNumber on the adjacentLocations array to get and return the next random Location board space to which the thief will move.
func getNextLocation() -> Int
//get list of adjacent locations
var adjacentLocations = gameBoard[currentLocation]?.adjacentLocations
//thief cannot return to location she/he was just at so remove from list
adjacentLocations?.remove(at: getIndexOfMatchingValue(adjacents: adjacentLocations!,lookingFor:previousLocation))
//is the current location a door or window; if so thief HAS TO go through it
if gameBoard[currentLocation]?.locationType==LocationType.door || gameBoard[currentLocation]?.locationType==LocationType.window{
adjacentLocations=getLocationsOnOtherSideOfDoorWindow(adjacents: adjacentLocations!)
}
//is the current location a subway
if gameBoard[currentLocation]?.locationType==LocationType.subway{
adjacentLocations=dealWithSubwayTravel(adjacents: adjacentLocations!)
}
//if any of the remaining adjacent locations is a crime location thief must go there
let crimeLocations: [Int] = lookForAdjacentCrimeLocations(adjacents: adjacentLocations!)
if crimeLocations.count != 0{
adjacentLocations = crimeLocations
}
//randomly choose a location number from the array and return it
return adjacentLocations![getRandomNumber(max: (adjacentLocations?.count)!)]
}
Once a thief leaves a building, all the valuables at each crime board space are replenished and floor locations return to being crime locations if they were robbed and then changed.
resetTheCrimeScenes first determines the building number of the current board space Location.
resetTheCrimeScenes then creates an array of the crime board space Locations in that building.
resetTheCrimeScenes then loops through that array using the number to index the gameBoard array and resetting the locationType of that Location to that of crime.
func resetTheCrimeScenes(){
//get the building number
let buildingNumber = currentLocation/100
var crimeLocations:[Int] = []
//for that building, get the list of crime locations
switch buildingNumber {
case 1:
crimeLocations=[123,144,146,164]
case 2:
crimeLocations=[242,245,247,265,267]
case 3:
crimeLocations=[337,352,355,376]
default:
crimeLocations=[425,445,463,465,467]
}
//loop through crime locations and ensure they are crime
for index in 0 ... crimeLocations.count-1{
let locationNumber = crimeLocations[index]
gameBoard[locationNumber]?.locationType = LocationType.crime
}
}
This function creates and returns an array of the three-digit Location board spaces whose location type is crime.
func lookForAdjacentCrimeLocations(adjacents: [Int])->[Int]{
var crimeLocations: [Int] = []
for index in 0 ... adjacents.count-1{
let locationNumber = adjacents[index]
let location = gameBoard[locationNumber]
if location != nil{
if location?.locationType==LocationType.crime{
crimeLocations.append(adjacents[index])
}
}
}
return crimeLocations
}
If the previousLocation type was a subway, the thief cannot take that subway again. dealWithSubwayTravel determines if the prevousLocation type is subway, and if so calls getNonSubwayLocations to get a subset of the adjacentLocations that are not subway board space Locations.
If the currentLocation is a corner subway (there are four corner subway stations and one in the centre of the game board) the thief must take the subway to another subway station. This function gets a subset of just the five subway stations.
The function returns the subset of adjacent Locations.
func dealWithSubwayTravel(adjacents:[Int])->[Int]{
var tempAdjacents = adjacents
//if previousLocation was a subway, thief already travelled so can't immediately travel again
if gameBoard[previousLocation]?.locationType==LocationType.subway{
tempAdjacents = getNonSubwayLocations(adjacents: tempAdjacents)
}
else{
//if the currentLocation is a corner subway, thief must take subway
if currentLocation==599 || currentLocation==699 || currentLocation==799 || currentLocation==899{
tempAdjacents = [500,599,699,799,899]
}
}
return tempAdjacents
}
If a thief opens a door or breaks a window, they have to go through it. Board space locations greater than 499 are on the street, while those below are in buildings. In addition, all locations in a building on one side of a door are either greater than or less than those on the opposite side. Thus by comparing the previousLocation to the currentLocation (the door/window) this function can determine in which direction the thief must move.
If the previous location was on the street, the function calls getIndexOfGreaterOrLesserValue passing lessThan to get all the locations in the adjacentLocations that are in the building.
Otherwise, the function calls getIndexOfGreaterOrLesserValue passing greaterThan to get all the adjacentLocations that are on the street. Here, also, the function calls resetTheCrimeScenes as the thief is leaving the building so all the valuables will be refreshed and the thief can reenter the building later in the game to steal again.
Buildings have internal doors and windows between rooms. If by now the temporary array has no values in it, the door/window was internal to a building, so the function just calls getIndexOfGreaterOrLesserValue to get the array of board space Locations on the opposite side of the window or door.
The function then returns the temporary array of adjacent Locations.
func getLocationsOnOtherSideOfDoorWindow(adjacents:[Int])->[Int]{
var tempAdjacents = adjacents
//if the previousLocation is greater than 499, thief was on the street and is entering the building
if previousLocation>499 {
tempAdjacents = getIndexOfGreaterOrLesserValue(adjacents: tempAdjacents, lookingFor: 499, greaterOrLessThan: GreaterOrLessThan.lessThan)
}
else{
//go through adjacents list and get anything > 499; this indicates an outside door/window
tempAdjacents = getIndexOfGreaterOrLesserValue(adjacents: tempAdjacents, lookingFor: 499, greaterOrLessThan: GreaterOrLessThan.greaterThan)
//if there are items in list, thief was leaving building so reset the crime locations
if tempAdjacents.count != 0{
resetTheCrimeScenes()
}
}
//check if door/window is inside a building; if tempAdjacents has no items, the door/window is inside
if tempAdjacents.count == 0{
tempAdjacents=adjacents //reset working list of adjacent locations
if previousLocation > currentLocation{
tempAdjacents = getIndexOfGreaterOrLesserValue(adjacents: tempAdjacents, lookingFor: currentLocation, greaterOrLessThan: GreaterOrLessThan.lessThan) }
else{
tempAdjacents = getIndexOfGreaterOrLesserValue(adjacents: tempAdjacents, lookingFor: currentLocation, greaterOrLessThan: GreaterOrLessThan.greaterThan) }
}
return tempAdjacents
}
This function loops through a list of adjacent Locations for a given value and returns the array index.
func getIndexOfMatchingValue(adjacents:[Int],lookingFor:Int)->Int{
for index in 0 ... adjacents.count-1{
if lookingFor == adjacents[index]{
return index
}
}
return 0
}
Given an array, this function can return an array subset whose values are greater than or less than a specific value. getLocationsOnOtherSideOfDoorWindow and getNextLocation call this function to get the list of adjacentLocations where the thief can legally move.
func getIndexOfGreaterOrLesserValue(adjacents:[Int],lookingFor:Int,greaterOrLessThan:GreaterOrLessThan)->[Int]{
var tempAdjacents:[Int] = []
if greaterOrLessThan==GreaterOrLessThan.greaterThan{
for index in 0 ... adjacents.count-1{
if adjacents[index]>lookingFor{
tempAdjacents.append(adjacents[index])
}
}
}
else{
for index in 0 ... adjacents.count-1{
if adjacents[index]<lookingFor{
tempAdjacents.append(adjacents[index]) }
}
}
return tempAdjacents
}
If the thief just took the subway, they have to exit to the street. This function loops through the adjacentLocations and creates a tempArray containing only non-subway board space Locations to return. dealWithSubwayTravel calls this function.
func getNonSubwayLocations(adjacents:[Int])->[Int]{
var tempAdjacents:[Int] = []
for index in 0 ... adjacents.count-1{
let locationNumber = adjacents[index]
let location = gameBoard[locationNumber]
if location != nil{
if location?.locationType==LocationType.subway{
tempAdjacents.append(adjacents[index])
}
}
}
return tempAdjacents
}
getNextLocation calls this function to randomly generate the index of the thief's next board space Location.
func getRandomNumber(max: Int) -> Int {
let randomNum = Int(arc4random_uniform(UInt32(max)))
return randomNum
}
ViewController
This class is the ViewController in Swift's MVC pattern for the app UI. Meant to test the core data classes, ViewController has a clueButton the user can press to generate clues, or the new board space Location of the thief, and a tableView to display all the thief's moves or board space Locations.
Variables:
let nib = UINib(nibName: "ClueTableViewCell", bundle: nil)
@IBOutlet var tableView: UITableView!
@IBOutlet var clueButton: UIButton!
var firstClue = true
var clues: [Location] = []
var gameState = GameState()
This function responds to the user pressing the clueButton.
getClue examines the firstClue Boolean, and if true, calls the GameState getFirstClue function to get a crime game board Location to add to the clues array. getClue then flips the firstClue Boolean and it will remain so for the remainder of the game. If firstClue is false, getClue calls the normal GameState getClue function to get the thief's next board space Location.
getClue calls reloadData on tableView to refresh the view to show the new Location in the list.
@IBAction func getClue(sender: UIButton){
if firstClue{
clues.append(gameState.gameBoard[gameState.getFirstClue()]!)
firstClue=false
}
else{
clues.append(gameState.gameBoard[gameState.getClue()]!)
}
tableView.reloadData()
}
This function registers the ClueTableViewCell, the formatted UI row for the TableView so it can be recycled to display rows as the user scrolls the list. To bind the tableView to the clues array, this function sets the delegate and dataSource which will require tableView constructors in this class.
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(nib, forCellReuseIdentifier: "ClueTableViewCell")
tableView.delegate = self
tableView.dataSource = self
}
Overridden system function added by Xcode called in the case of memory issues.
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
Two constructors for the TableView UI control: one constructor returns the number of items in the clues array that is bound to the TableView UI control; and the other constructor removes and returns ClueTableViewCells as they disappear from the TableView as the user scrolls so the UI can reuse them to show the next item in the clue array bound to the TableView.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return clues.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ClueTableViewCell", for: indexPath) as! ClueTableViewCell
cell.locationNumber.text = String(clues[indexPath.row].locationNumber)
cell.locationType.text = String(describing: clues[indexPath.row].locationType)
return cell
}
}