First of all, welcome! I don't have a good source, but I suppose I can walk you through the text-based game you mentioned.
TL;DR
You can compile it Terminal style. You can put the code snippet below in a file, say adventure.swift
and run the command
swiftc ./adventure.swift
which will produce executable named adventure
for you to run, again, in Terminal.
After the snippet, I'll go on about how the code works, and gives you some more improvement over it. I'll have a few snippets, each becoming more Swifty.
Swift doesn't have goto
like Pascal. It is considered "unsafe" and is largely abandoned by many modern languages nowadays. So instead of literally goto
different part of the code to print different thing, we will use variable to keep track of the state. Since you're already familiar with programming a few years back , you'd realise it'll more or less work like this:
- Store current state of your game in
currentState
- In an infinite loop
- Check what state you are in, and print appropriate message
- Read user input
- Based on current state and user input,
goto
the next state
Here's a code example
var currentState = "INTRO"
while true {
// Print appropriate message
if currentState == "INTRO" {
print("AS SOON AS YOU ENTER THE DOOR YOU SEE A LAMP, A ROCK AND A PIECE OF WOOD. YOU NEED TO CONTINUE WALKING FORWARD. WHAT DO YOU PICK UP?")
} else if currentState == "DEAD" {
print("Something Something you are dead")
// This state is special, we don't wait for user input, so we move to `INTRO` immediately
currentState = "INTRO"
continue
} else if currentState == "DOOR2" {
print("You found a locked door, hmmm...")
} // else if
// Read user input from terminal
guard let userInput = readLine() else {
// Usually reach here because user press Ctrl+D, or we reach EOF
// Better to break out of the `while` loop
break
}
if currentState == "INTRO" && userInput == "ROCK" {
print("A MONSTER COMES BEHIND YOUR BACK IN THE DARK AND ATTACKS YOU.")
// goto [dead]
currentState = "DEAD"
} else if currentState == "INTRO" && userInput == "LAMP" {
print("YOU PICK UP THE LAMP AND START WALKING UNTIL YOU SEE ANOTHER DOOR RIGHT IN FRONT OF YOU.")
// goto [door2]
currentState = "DOOR2"
} // else if ...
}
Note that Swift is a strongly-typed language, like Pascal, so everything will have a type. Furthermore it is very smart and try to infer what type you want to use based on context. The line
var currentState = "INTRO"
says a few things. First of, it uses var currentState
, which means that we create variable currentState
, and assign "INTRO"
into it. As we use double quote to signify string of characters, swift knows that currentState
must be String
.
The rest should be familiar to you with the while
, if
/else
construct, with things like currentState == "INTRO" && userInput == "LAMP"
being TRUE
if both currentState
is "INTRO"
and that userInput is "LAMP"
.
Another thing is guard let
:
guard let userInput = readLine() else {
// Usually reach here because user press Ctrl+D, or we reach EOF
// Better to break out of the `while` loop
break
}
This does a few things. readLine()
is a function that reads from user input, its return type is String?
(note the trailing ?
) which means that it'll return a working String
, or it may return nil
, signifying that there's no String
to return. So we want to check if the value is assigned to userInput
is not nil
, that's why we put it in guard
condition.
Right now you can think of guard
as if-else
(it's alright folks, I know the nuance, put your pitchfork down ) that is
- If the condition is true, do nothing
- Otherwise execute the block after
else
The requirement for guard is that you must do return
or continue
or break
at the end, which is a deliberate choice, and I won't go into details here
Note also that I use let userInput
, similar to var currentState
, it creates a variable named userInput
. The only difference is that let
creates a constant, you can not change the value after you created it.
You probably notice that you do a lot of comparison against a variable or two, so, similar to Pascal, you can do swift
over variables. Though Swift's switch
is more powerful in a few notable ways. So the code v2 becomes:
var currentState = "INTRO"
while true {
// Print appropriate message
switch currentState {
case "INTRO":
print("AS SOON AS YOU ENTER THE DOOR YOU SEE A LAMP, A ROCK AND A PIECE OF WOOD. YOU NEED TO CONTINUE WALKING FORWARD. WHAT DO YOU PICK UP?")
case "DOOR2":
print("You found a locked door, hmmm...")
case "DEAD":
print("Something Something you are dead")
// This state is special, we don't wait for user input, so we move to `INTRO` immediately
currentState = "INTRO"
continue
default: fatalError("Hmm, we should've handled all the possibilities here. Let's crash a program if we hit this location")
}
// Read user input from terminal
guard let userInput = readLine() else {
// Usually reach here because user press Ctrl+D, or we reach EOF
// Better to break out of the `while` loop
break
}
switch (currentState, userInput) {
case ("INTRO", "ROCK"):
print("A MONSTER COMES BEHIND YOUR BACK IN THE DARK AND ATTACKS YOU.")
currentState = "DEAD"
case ("INTRO", "LAMP"):
print("YOU PICK UP THE LAMP AND START WALKING UNTIL YOU SEE ANOTHER DOOR RIGHT IN FRONT OF YOU.")
currentState = "DOOR2"
default:
// Well, we don't know what the user is trying to do, lets just go to the next loop silently.
// Break out of the switch without doing anything.
break
}
}
So you declare the variable you want to match against with switch
, and all the candidates in case <SOME VALUE HERE>:
. Note that unlike old school switch
, Swift won't go to the next case by default, ie, Swift's switch doesn't fallthrough
. Furthermore, switch
is very pedantic about you handling all possibilities. Since I'm using a string and there's no way I'll be able to check all possible string combination, I use default
case to handle any thing that doesn't matched existing cases.
One other nice thing about switch
is that it can matches multiple variables (by means of tuple, again, I won't go into details here) like what I did in the second switch
.
Now, using String
to save state is pretty cumbersome, you can misspell, or forget some cases in switch. Swift can help you with enum
. It is a collection of limited possible values. Like in this case, we know that the states will be either intro
, door2
, or dead
, and nothing else. So you can convert currentState
to State
as the code below.
// Create type State
enum State {
// Declare 3 possible cases, begin with keyword case
case intro, door2, dead
}
// Set currentState to value `intro` in the `State` type
var currentState = State.intro
while true {
// Print appropriate message
switch currentState {
case .intro: print("AS SOON AS YOU ENTER THE DOOR YOU SEE A LAMP, A ROCK AND A PIECE OF WOOD. YOU NEED TO CONTINUE WALKING FORWARD. WHAT DO YOU PICK UP?")
case .door2: print("You found a locked door, hmmm...")
case .dead:
print("Something Something you are dead")
// This state is special, we don't wait for user input, so we move to `INTRO` immediately
currentState = .intro
continue
}
// Read user input from terminal
guard let userInput = readLine() else {
// Usually reach here because user press Ctrl+D, or we reach EOF
// Better to break out of the `while` loop
break
}
switch (currentState, userInput) {
case (.intro, "ROCK"):
print("A MONSTER COMES BEHIND YOUR BACK IN THE DARK AND ATTACKS YOU.")
currentState = .dead
case (.intro, "LAMP"):
print("YOU PICK UP THE LAMP AND START WALKING UNTIL YOU SEE ANOTHER DOOR RIGHT IN FRONT OF YOU.")
currentState = .door2
default:
// Well, we don't know what the user is trying to do, lets just go to the next loop silently.
break
}
}
So now we don't need to worry about misspelling the states. Better yet, since the compiler can now proof that we exhausted all the possibility (there're only 3 in this case), we don't even need the default
case! Note that later I use .door2
to refer to the value, this is because you can normally do State.door2
to achieve the same effect, but since Swift can infer that it is expecting a State
where you write it, you can just use .door2
to refer to it.