Skip to content

Instantly share code, notes, and snippets.

@lukewilson2002
Last active September 20, 2019 18:11
Show Gist options
  • Save lukewilson2002/10edf2176433285a767cd0bbf8f2ffef to your computer and use it in GitHub Desktop.
Save lukewilson2002/10edf2176433285a767cd0bbf8f2ffef to your computer and use it in GitHub Desktop.
package main
import (
"bufio"
"fmt"
"os"
"strings"
"time"
)
// WPM is the (temporary) word-per-minute reading speed of the player.
const WPM int = 300
// Various actions which can be interpreted by ProcessString.
const (
GOTO = iota // Change location
LOOK // Look (in general) or at any object in specific
READ // Read an item in room or inventory
TAKE // Take an item from room to inventory
DROP // Drop an item from inventory
THROW // Throw an item
INV // List inventory items
)
// Action represents a single operation by a player, consisting of a purpose (.Action) and its subject and targets (.Operands).
type Action struct {
Action int // One of the consts above
Operands []string // Everything that goes with this action
}
// SecondsToRead returns the number of individual seconds it should take a player to read the given string, given a WPM reading level.
func SecondsToRead(txt string, wpm int) float32 {
words := len(strings.Fields(txt)) // Individual words in the given text
return float32(words) / (float32(wpm) / 60) // Words in text divided by words per second
}
// hasAnyOfPrefix checks if a given string has any of the given prefixes,
// and returns true when it does, along with the length of the matched prefix.
func hasAnyOfPrefix(a string, v ...string) (bool, int) {
for _, s := range v {
if strings.HasPrefix(a, s) {
return true, len(s)
}
}
return false, 0
}
// ProcessString returns a list of actions that were parsed by the given input. An error may be returned if
// an action could not be syntactically or grammatically determined appropriately. Some actions may have a
// variable number of operands expected, and thus any missing operands would appropriate an error.
func ProcessString(inp string) (actions []Action, e error) {
if strings.HasSuffix(inp, ".") {
inp = inp[:len(inp)-1] // Trim the period off the sentence
}
// These are referred to as the individual sentences, rather than, more appropriately, actions, to prevent confusion.
sentences := strings.Split(strings.ToLower(inp), ",") // Split a sentence by commas
for i, s := range sentences {
sentences[i] = strings.TrimSpace(s) // Trim whitespace off the beginning and end of all actions
}
for _, s := range sentences {
if b, l := hasAnyOfPrefix(s, "go to", "go", "walk to", "move to", "move"); b {
operand := s[l:]
if len(operand) <= 0 { // If there's no operand provided at all
e = fmt.Errorf("Go where?")
return
} else if operand[0] != ' ' { // Safe operation since we assert minimum size ^
goto what
}
actions = append(actions, Action{GOTO, []string{strings.TrimSpace(operand)}})
} else if s == "look" || s == "see" { // LOOK action without operands
actions = append(actions, Action{LOOK, []string{}})
} else if b, l := hasAnyOfPrefix(s, "look at"); b { // The LOOK action, but with operands
operand := s[l:]
if len(operand) <= 0 {
e = fmt.Errorf("Look at what?")
return
} else if operand[0] != ' ' {
goto what
}
actions = append(actions, Action{LOOK, []string{strings.TrimSpace(operand)}})
} else if b, l := hasAnyOfPrefix(s, "read"); b {
operand := s[l:]
if len(operand) <= 0 {
e = fmt.Errorf("Read what?")
return
} else if operand[0] != ' ' {
goto what
}
actions = append(actions, Action{READ, []string{strings.TrimSpace(operand)}})
} else if b, l := hasAnyOfPrefix(s, "take", "grab", "pick up", "collect", "retrieve"); b {
operand := s[l:]
if len(operand) <= 0 {
e = fmt.Errorf("Take what?")
return
} else if operand[0] != ' ' {
goto what
}
actions = append(actions, Action{TAKE, []string{strings.TrimSpace(operand)}})
} else if b, l := hasAnyOfPrefix(s, "put down", "drop"); b {
operand := s[l:]
if len(operand) <= 0 {
e = fmt.Errorf("Drop what?")
return
} else if operand[0] != ' ' {
goto what
}
actions = append(actions, Action{DROP, []string{strings.TrimSpace(operand)}})
} else if b, l := hasAnyOfPrefix(s, "throw", "pitch", "toss"); b {
if s[l] != ' ' { // If there is no space after this first word
goto what
}
operands := strings.Split(s[l:], " at ") // "throw pencil at wall"
if len(operands) != 2 { // We need subject and target
e = fmt.Errorf("I understand as much as you want to throw the %s, but at what?", strings.TrimSpace(operands[0]))
return
}
actions = append(actions, Action{THROW, []string{strings.TrimSpace(operands[0]), strings.TrimSpace(operands[1])}})
} else if s == "inventory" || s == "items" {
actions = append(actions, Action{INV, []string{}})
} else {
goto what
}
}
return
what:
e = fmt.Errorf("What?")
return
}
// ItemType values
const (
OTHER = iota // Default for items not given a type
READABLE
WEAPON
)
// ItemType is the representation of an item as its underlying purpose.
type ItemType int
// An Item represents anything which a player can take and use.
type Item struct {
Name string
Description string
Type ItemType
Aliases []string
Read string
}
// IsCalled returns true if the Item has the given name or alias.
func (i *Item) IsCalled(n string) bool {
if i.Name == n {
return true
}
for _, alias := range i.Aliases {
if alias == n {
return true
}
}
return false
}
func sliceGetItemByName(s *[]Item, n string) *Item {
for _, it := range *s {
if it.IsCalled(n) {
return &it
}
}
return nil
}
func sliceRemoveItemAt(s *[]Item, idx int) {
*s = append((*s)[:idx], (*s)[idx+1:]...)
}
func main() {
r := bufio.NewReader(os.Stdin)
deathCount := 0
start:
items := []Item{
Item{"strange prism", "It must be some sort of puzzle.", OTHER, []string{"prism", "toy", "puzzle"}, "I... don't know how to read a toy."},
Item{"letter", "Printed. I can read it.", READABLE, []string{}, `Dear Subject,
Welcome to the simulation! You have probably only just begun existing about... 30 seconds ago! I know this world is really lame, but no need to worry. You should stop existing here soon. Oh, don't fret; it shouldn't be painful!
Happy exploring,
Paradox Laboratories
P.S. Whatever you do, don't go anywhere.`},
}
inventory := make([]Item, 0, 8)
if deathCount > 0 { // The player has just been revived, say something witty! Quick!
if deathCount == 1 {
fmt.Println(`"Welcome back -- we reset you. Try not to die?"`)
} else if deathCount == 2 {
fmt.Println("*Sigh*")
} else if deathCount == 4 {
fmt.Println(`"Seriously, you're wasting our time. Do the puzzle."`)
}
} else {
fmt.Println("You open your eyes to grim, grey sky, void of all colors or even any stars. You clamber to your feet, tripping over a weird geometrical toy on the ground in the process.")
}
for {
fmt.Print(">")
line, _ := r.ReadString('\n')
line = strings.Replace(line, "\n", "", -1) // Remove new-line character from input
actions, err := ProcessString(line) // The high-level action interpreted by the entered `line`
if err != nil {
fmt.Println(err)
continue
}
for _, a := range actions {
if a.Action == GOTO {
if deathCount >= 7 { // If player dies seven times
fmt.Println(`"Okay, cut him off. He won't cooperate."`)
os.Exit(0) // Game ending: don't cooperate
} else {
fmt.Println("You took a step and fell faster and faster, until suddenly everything went dark.")
time.Sleep(time.Second * 3)
deathCount++
goto start
}
} else if a.Action == LOOK {
if len(a.Operands) > 0 { // For looking at an object in specific
item := sliceGetItemByName(&items, a.Operands[0])
if item == nil {
item = sliceGetItemByName(&inventory, a.Operands[0])
if item == nil {
fmt.Println("You see no such thing.")
} else {
fmt.Println(item.Description)
}
} else {
fmt.Println(item.Description)
}
} else {
fmt.Println("You are in an infinitely wide grey room with no sky. The floor looks a little dodgey meters away, as it ripples sporadically.")
fmt.Println("There are some items around you:")
for _, item := range items {
if item.Name != "" { // Only for valued items
fmt.Println("A", item.Name)
}
}
}
} else if a.Action == READ {
if item := sliceGetItemByName(&items, a.Operands[0]); item != nil { // The item to read is in world
for i := 0; i < len(items); i++ { // Add the item to player's inventory
if items[i].IsCalled(a.Operands[0]) {
inventory = append(inventory, items[i])
sliceRemoveItemAt(&items, i) // Remove item from world
fmt.Println("*Taking it first*")
break
}
}
fmt.Println(item.Read)
} else if item := sliceGetItemByName(&inventory, a.Operands[0]); item != nil { // The item to read is in inventory
fmt.Println(item.Read)
} else {
fmt.Println("You're not sure you see that anywhere.")
}
} else if a.Action == TAKE {
for i := 0; i < len(items); i++ {
if items[i].IsCalled(a.Operands[0]) {
inventory = append(inventory, items[i]) // Add item to inventory
sliceRemoveItemAt(&items, i) // Remove item from world
break
}
}
fmt.Println("You took the", a.Operands[0]+".")
} else if a.Action == DROP {
for i := 0; i < len(items); i++ {
if inventory[i].IsCalled(a.Operands[0]) {
items = append(items, inventory[i]) // Add item to world
sliceRemoveItemAt(&inventory, i) // Remove item from inventory
break
}
}
fmt.Println("You dropped the", a.Operands[0]+".")
} else if a.Action == THROW {
item := sliceGetItemByName(&inventory, a.Operands[0])
if item == nil {
fmt.Println("You don't have a", a.Operands[0])
} else {
// TODO: calculate what object they're throwing items at and respond differently than at the void
if item.IsCalled("strange prism") { // Player throws the prism
paragraphs := [6]string{
"You throw the prism and it rolls several meters, bouncing on its odd corners, before... falling! But just as it does, the world rips, and in a slowly widening seam you see scientists behind their keyboards.",
"\"There's spatial interference,\" a voice inside your head says. \"It's that toy! It fucking tore the dim- He can hear me. Shut it off. Shut it off now!\" The voice booms.",
"\"We can't!\" Another voice says. One scientist turns to see you, and all of the others follow in astonishment. \"My god.\" But the scientist is cut off.",
"The portal, or what could only be described as an interdimensionary tunnel, begins to flash through tens, then hundreds of other locations, as fast as the frames in a flipbook. A taiga, then a boiler room, and a kitchen with a busy cook, and hundreds of other places and unsuspecting people.",
"You put your hand out to the worlds that flash by, in that little spatial gap between so many colors, and yours of endless dreary grey. You can feel the heat and cold and wind bursting through this window all at once."}
fmt.Println() // Blank line before paragraphs
for _, p := range paragraphs {
fmt.Printf("%s\n\n", p)
time.Sleep(time.Second * time.Duration(SecondsToRead(p, WPM)))
}
} else {
fmt.Printf("You threw the %s at the %s.\n", item.Name, a.Operands[1])
for i := 0; i < len(items); i++ {
if inventory[i].IsCalled(a.Operands[0]) {
inventory = append(inventory, items[i])
sliceRemoveItemAt(&items, i) // Remove item from world
break
}
}
}
}
} else if a.Action == INV {
if len(inventory) != 0 {
fmt.Println("You have:")
for _, item := range inventory {
if item.Name != "" {
fmt.Println("A", item.Name)
}
}
} else {
fmt.Println("You have nothing.")
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment