Last active
September 20, 2019 18:11
-
-
Save lukewilson2002/10edf2176433285a767cd0bbf8f2ffef to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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