# A Hotel Safe
HotelSafe simulates a type of safe frequently seen in hotel rooms.
Locking the Safe
- Press Reset (R). The display will prompt for a new code.
- Enter a 4 digit code and then press Lock (L).
- The code will flash. The safe is now locked.
Unlocking the Safe
- Enter the 4 digit code.
- The safe will flash OPENED if the code is correct. The safe is now open.
- If the code is incorrect, the display will flash ERROR and the safe will stay locked.
/**
* The state machine's data type
*/
type SafeData = {
code: Array<number>,
input: Array<number>,
timeout: Timeout,
codeSize: number,
message?: string
}
export default class HotelSafe extends StateMachine<SafeData> {
handlers: Handlers<SafeData> = [
// Clear data when safe enters OPEN
['enter#*_#open', () => keepState().data({
code: {$set: []},
input: {$set: []},
message: {$set: 'Open'},
})],
// User pressed RESET -- get new code
['cast#reset#open', () => nextState('open/locking').data({
message: {$set: 'Enter Code'},
})],
// Track the last {codeSize} digits.
// show code on display. Repeat state for setting timeout
['cast#button/:digit#open/locking', ({args, data}) => {
let code = pushFixed(Number(args.digit), data.code, data.codeSize)
return repeatState().data({
code: {$set: code},
message: {$set: code.join('')},
})
}],
// User pressed LOCK. CLose safe if code is long enough
// else, repeat state (sets timeout on reentry)
['cast#lock#open/locking', ({data}) =>
data.code.length !== data.codeSize ?
repeatState() :
nextState('closed/success').data({
message: {$set: `**${data.code.join('')}**`},
})],
// Clear input when safe is closed
['enter#*_#closed', () => keepState().data({
input: {$set: []},
message: {$set: 'Locked'}
})],
// Postpone button press and go to closed/unlocking
['cast#button/*_#closed', ({}) =>
nextState('closed/unlocking').postpone()],
// User entered digit(s).
// Keep state if code is not long enough
// OPEN if input matches code
// go to MESSAGE if code does not match and set a timeout
['cast#button/:digit#closed/unlocking', ({args, data}) => {
let digit = Number(args.digit)
let input = data.input.concat(digit)
// code is the correct length. Decision time.
if (input.length >= data.code.length) {
let [state, msg] = arrayEqual(data.code, input) ?
['open/success', "Opened"] :
['closed/error', "ERROR"]
return nextState(state).data({message: {$set: msg}})
}
// Not long enough. Keep collecting digits.
// Show masked code. Repeat state for
// setting timeout
return repeatState().data({
input: {$push: [digit]},
message: {$set: "*".repeat(input.length)}
})
}],
// These states timeout on inactivity (eventTimeout)
[['enter#*_#open/locking',
'enter#*_#closed/unlocking'], ({data}) =>
keepState().eventTimeout(data.timeout)],
// these states just timeout
['enter#*_#:state/*_', ({data}) =>
keepState().timeout(data.timeout)],
// If we timeout in a sub state, go to the base state
[['genericTimeout#*_#:state/*_',
'eventTimeout#*_#:state/*_',], ({args}) =>
nextState(args.state)],
]
initialData: SafeData = {
code: [],
codeSize: 4,
timeout: 200,
input: [],
}
initialState = 'open'
constructor(timeout: Timeout) {
super()
this.initialData.timeout = timeout
}
/**
* Safe Interface. casts 'reset'
*/
reset() {
this.cast('reset')
}
/**
* Safe Interface. cast 'lock'
*/
lock() {
this.cast('lock')
}
/**
* Safe Interface. send button digit
* @param digit
*/
button(digit: number) {
this.cast({button: digit})
}
}
Here’s the hotel safe’s state diagram. The collection of open(/*) states (and likewise, closed(/*) states) represents a state machine and the rule of thumb is to implement it in a separate machine. Complex states however, make the job easier for simple hierarchies, as shown above.