Compare commits

...

21 Commits

Author SHA1 Message Date
Jordan Orelli ae409fd065 list games command 5 years ago
Jordan Orelli d603c74a18 dying should be more of a ceremony 6 years ago
Jordan Orelli 4e1ade8e12 global index is gone! :party_parrot: 6 years ago
Jordan Orelli 209635083d remove old edges table 6 years ago
Jordan Orelli 25d142fb8a get rid of system distances function 6 years ago
Jordan Orelli ce94d3cdd2 scan and broadcast both work without global index 6 years ago
Jordan Orelli dbb2c86f5a slowly getting rid of global index 6 years ago
Jordan Orelli 5ce9a530f8 player name selection no longer blocks new connections 6 years ago
Jordan Orelli 495f4c3fa2 fix nil panic when lobby connections leave 6 years ago
Jordan Orelli 34a984a397 move connection state to its own file 6 years ago
Jordan Orelli 6cd419d013 needs more explanation 6 years ago
Jordan Orelli bdc2783838 uniform status command for all states 6 years ago
Jordan Orelli 15015a9d5b rename command help to summary 6 years ago
Jordan Orelli aa6bd1ca9e travel progress indicator 6 years ago
Jordan Orelli 8cdd7bdaa6 cleaning up enter travel text 6 years ago
Jordan Orelli 2914275282 adding a status command for every player state 6 years ago
Jordan Orelli 50128a64a6 display travel time in output of "nearby" command 6 years ago
Jordan Orelli b0aaa046ad fix broken help command 6 years ago
Jordan Orelli a330d87b4c refactor lobby state 6 years ago
Jordan Orelli 938348ce26 run multiple games at once 6 years ago
Jordan Orelli e629c0b6b6 get rid of crazy id scheme
that was so unecessary wow
6 years ago

@ -1,24 +1,5 @@
space-dragons-in-outer-space
============================
there's no url because it's not a web app
it's just a tcp server
you go
like this
`nc 104.236.57.163 9220`
and that is how
you be a dragon
in space
in space
in space
This is a [real-time strategy
game](https://en.wikipedia.org/wiki/Real-time_strategy) in the style of a [MUD](https://en.wikipedia.org/wiki/MUD).

@ -2,6 +2,7 @@ package main
import (
"fmt"
"strings"
"time"
)
@ -32,10 +33,10 @@ func (b *Bomb) Dead() bool {
return b.done
}
func (b *Bomb) Tick(frame int64) {
func (b *Bomb) Tick(game *Game) {
b.fti -= 1
if b.fti <= 0 {
b.target.Bombed(b.profile, frame)
b.target.Bombed(b.profile, game)
b.done = true
log_info("bomb went off on %v", b.target)
}
@ -56,7 +57,6 @@ func MakeBomb(s *System) ConnectionState {
m.CommandSuite = CommandSet{
balCommand,
BroadcastCommand(s),
helpCommand,
NearbyCommand(s),
playersCommand,
}
@ -78,7 +78,23 @@ func (m *MakeBombState) Tick(c *Connection, frame int64) ConnectionState {
return m
}
func (MakeBombState) String() string { return "Making a Bomb" }
func (m *MakeBombState) Exit(c *Connection) {
c.bombs += 1
c.Printf("Done! You now have %v bombs.\n", c.bombs)
}
func (m *MakeBombState) FillStatus(c *Connection, s *status) {
elapsedFrames := c.game.frame - m.start
elapsedDur := framesToDur(elapsedFrames)
desc := fmt.Sprintf(`
Currently making a bomb!
Build time elapsed: %v
Build time remaining: %v
`, elapsedDur, options.makeBombTime-elapsedDur)
s.Description = strings.TrimSpace(desc)
s.Location = m.System.String()
}

@ -9,8 +9,8 @@ type broadcast struct {
start time.Time
origin *System
dist float64
nextHitIndex int
message string
neighborhood Neighborhood
}
func NewBroadcast(from *System, template string, args ...interface{}) *broadcast {
@ -21,20 +21,27 @@ func NewBroadcast(from *System, template string, args ...interface{}) *broadcast
}
}
func (b *broadcast) Tick(frame int64) {
func (b *broadcast) Tick(game *Game) {
if b.neighborhood == nil {
log_info("setting up neighborhood for broadcast: %s", b.message)
b.neighborhood = game.galaxy.Neighborhood(b.origin)
log_info("nearest neighbor: %v", b.neighborhood[0])
}
b.dist += options.lightSpeed
for ; b.nextHitIndex < len(b.origin.Distances()); b.nextHitIndex += 1 {
candidate := b.origin.Distances()[b.nextHitIndex]
if b.dist < candidate.dist {
break
for len(b.neighborhood) > 0 && b.neighborhood[0].distance <= b.dist {
s := game.galaxy.GetSystemByID(b.neighborhood[0].id)
log_info("broadcast %s has reached %s from %s", b.message, s, b.origin)
s.NotifyInhabitants("message received from system %v:\n\t%s\n", b.origin, b.message)
if len(b.neighborhood) > 1 {
b.neighborhood = b.neighborhood[1:]
} else {
b.neighborhood = nil
}
candidate.s.NotifyInhabitants("message received from system %v:\n\t%s\n", b.origin, b.message)
}
}
func (b *broadcast) Dead() bool {
return b.dist > b.origin.Distances()[len(b.origin.Distances())-1].dist
}
func (b *broadcast) Dead() bool { return b.neighborhood == nil }
func (b *broadcast) String() string {
return fmt.Sprintf("[broadcast origin: %v message: %s]", b.origin, b.message)

@ -17,7 +17,6 @@ func MakeColony(c *Connection, sys *System) {
CommandSuite: CommandSet{
balCommand,
BroadcastCommand(sys),
helpCommand,
NearbyCommand(sys),
playersCommand,
},
@ -49,3 +48,7 @@ func (m *MakeColonyState) Exit(c *Connection) {
m.System.colonizedBy = c
c.Printf("Established colony on %v.\n", m.System)
}
func (m *MakeColonyState) FillStatus(c *Connection, s *status) {
s.Location = m.System.String()
}

@ -2,15 +2,45 @@ package main
import (
"fmt"
"sort"
// "strconv"
"strings"
"text/template"
)
var helpTemplate = template.Must(template.New("help").Parse(`
{{.Name}} Command Reference
Summary: {{.Summary}}
{{- if .Usage}}
Usage: {{.Usage}}
{{end}}
{{- if .Description}}
Details:
{{.Description}}
{{end}}
`))
func printHelp(conn *Connection, cmd *Command) {
desc := strings.ReplaceAll(strings.TrimSpace(cmd.help), "\n", "\n ")
helpTemplate.Execute(conn, struct {
Name string
Summary string
Usage string
Description string
}{
Name: cmd.name,
Summary: cmd.summary,
Usage: cmd.usage,
Description: desc,
})
}
var commandRegistry map[string]*Command
type Command struct {
name string
summary string
usage string
help string
arity int
variadic bool
@ -37,6 +67,14 @@ func (c Command) Commands() []Command {
type CommandSet []Command
func (c CommandSet) GetCommand(name string) *Command {
switch name {
case "help":
return &helpCommand
case "commands":
return &commandsCommand
case "status":
return &statusCommand
}
for _, cmd := range c {
if cmd.name == name {
return &cmd
@ -46,104 +84,147 @@ func (c CommandSet) GetCommand(name string) *Command {
}
func (c CommandSet) Commands() []Command {
return []Command(c)
return append([]Command(c), statusCommand, helpCommand, commandsCommand)
}
var helpCommand = Command{
name: "help",
help: "helpful things to help you",
summary: "explains how to play the game",
usage: "help [command-name]",
help: `
help explains the usage of various commands in Exocolonus. On its own, the help
command displays some basic info about how the game is played. If given an
argument of a command name, the help command displays the detailed usage of the
specified command.
`,
handler: func(conn *Connection, args ...string) {
msg := `
Star Dragons is a stupid name, but it's the name that Brian suggested. It has
nothing to do with Dragons.
Anyway, Star Dragons is a game of cunning text-based, real-time strategy. You
play as some kind of space-faring entity, faring space in your inspecific
space-faring vessel. If you want a big one, it's big; if you want a small one,
it's small. If you want a pink one, it's pink, if you want a black one, it's
black. And so on, and so forth. It is the space craft of your dreams. Or
perhaps you are one of those insect-like alien races and you play as the queen.
Yeah, that's the ticket! You're the biggest baddest queen bug in space.
In Star Dragons, you issue your spacecraft (which is *not* called a Dragon)
textual commands to control it. The objective of the game is to be the first
person or alien or bug or magical space ponycorn to eradicate three enemy
species. Right now that is the only win condition.
All of the systems present in Star Dragons are named and positioned after known
exoplanet systems. When attempting to communicate from one star system to
another, it takes time for the light of your message to reach the other star
systems. Star systems that are farther away take longer to communicate with.
Exocolonus is a game of cunning text-based, real-time strategy. You play as
some kind of space-faring entity, faring space in your inspecific space-faring
vessel. If you want a big one, it's big; if you want a small one, it's small.
If you want a pink one, it's pink, if you want a black one, it's black. And so
on, and so forth. It is the space craft of your dreams. Or perhaps you are
one of those insect-like alien races and you play as the queen. Yeah, that's
the ticket! You're the biggest baddest queen bug in space.
In Exocolonus, you issue your spacecraft textual commands to control it. The
objective of the game is to be the first person or alien or bug or magical
space ponycorn to eradicate three enemy species. Right now that is the only
win condition.
Exocolonus deals with relativity with respect to observation. When an effect
takes place, knowledge of that effect travels throughougt the galaxy at the
speed of light. If a system is bombed, the closest systems to it will know
first. When you broadcast messages, they travel at the speed of light.
All of the systems present in Exocolonus are named and positioned after known
exoplanet systems. Each star system in Exocolonus is a real star system that
has been researched by astronomers, and the number of planets in each system
corresponds to the number of known exoplanets in those systems. When
attempting to communicate from one star system to another, it takes time for
the light of your message to reach the other star systems. Star systems that
are farther away take longer to communicate with.
`
if len(args) == 0 {
msg = strings.TrimSpace(msg)
fmt.Fprintln(conn, msg)
if len(args) == 0 {
fmt.Fprint(conn, "\n")
conn.Line()
fmt.Fprint(conn, "\n")
fmt.Fprintln(conn, `use the "commands" command for a list of commands.`)
fmt.Fprintln(conn, `use "help [command-name]" to get info for a specific command.`)
return
}
for _, cmdName := range args {
cmd, ok := commandRegistry[cmdName]
if !ok {
cmd := conn.GetCommand(cmdName)
if cmd == nil {
conn.Printf("no such command: %v\n", cmdName)
continue
}
conn.Printf("%v: %v\n", cmdName, cmd.help)
printHelp(conn, cmd)
}
},
}
var commandsCommand = Command{
name: "commands",
help: "gives you a handy list of commands",
type status struct {
State string
GameCode string
Balance int
Bombs int
Kills int
Location string
Description string
}
var statusTemplate = template.Must(template.New("status").Parse(`
Current State: {{.State}}
--------------------------------------------------------------------------------
{{- if .GameCode}}
Current Game: {{.GameCode}}
Balance: {{.Balance}}
Bombs: {{.Bombs}}
Kills: {{.Kills}}
Location: {{.Location}}
{{end}}
{{.Description}}
`))
var statusCommand = Command{
name: "status",
summary: "display your current status",
handler: func(conn *Connection, args ...string) {
names := make([]string, 0, len(commandRegistry))
for name, _ := range commandRegistry {
names = append(names, name)
s := status{
State: conn.ConnectionState.String(),
}
sort.Strings(names)
fmt.Fprintln(conn, "--------------------------------------------------------------------------------")
for _, name := range names {
cmd := commandRegistry[name]
conn.Printf("%-16s %s\n", name, cmd.help)
conn.ConnectionState.FillStatus(conn, &s)
if conn.game != nil {
s.GameCode = conn.game.id
s.Balance = conn.money
s.Bombs = conn.bombs
s.Kills = conn.kills
}
fmt.Fprintln(conn, "--------------------------------------------------------------------------------")
statusTemplate.Execute(conn, s)
},
}
// this isn't a real command it just puts command in the list of commands, this
// is weird and circular, this is a special case.
var commandsCommand = Command{
name: "commands",
summary: "lists currently available commands",
}
func BroadcastCommand(sys *System) Command {
return Command{
name: "broadcast",
help: "broadcast a message for all systems to hear",
summary: "broadcast a message for all systems to hear",
handler: func(c *Connection, args ...string) {
msg := strings.Join(args, " ")
b := NewBroadcast(sys, msg)
log_info("player %s send broadcast from system %v: %v\n", c.Name(), sys, msg)
currentGame.Register(b)
c.game.Register(b)
},
}
}
func NearbyCommand(sys *System) Command {
handler := func(c *Connection, args ...string) {
neighbors, err := sys.Nearby(25)
if err != nil {
log_error("unable to get neighbors: %v", err)
return
}
c.Printf("--------------------------------------------------------------------------------\n")
c.Printf("%-4s %-20s %s\n", "id", "name", "distance")
c.Printf("--------------------------------------------------------------------------------\n")
for _, neighbor := range neighbors {
other := index[neighbor.id]
c.Printf("%-4d %-20s %-5.6v\n", other.id, other.name, neighbor.distance)
neighbors := c.game.galaxy.Neighborhood(sys)
c.Line()
c.Printf("%-4s %-20s %-12s %s\n", "id", "name", "distance", "trip time")
c.Line()
for _, neighbor := range neighbors[:25] {
other := c.game.galaxy.GetSystemByID(neighbor.id)
dur := NewTravel(c, sys, other).(*TravelState).tripTime()
c.Printf("%-4d %-20s %-12.6vpc %v\n", other.id, other.name, neighbor.distance, dur)
}
c.Printf("--------------------------------------------------------------------------------\n")
c.Line()
}
return Command{
name: "nearby",
help: "list nearby star systems",
summary: "list nearby star systems",
arity: 0,
handler: handler,
}
@ -151,7 +232,7 @@ func NearbyCommand(sys *System) Command {
var winCommand = Command{
name: "win",
help: "win the game.",
summary: "win the game.",
debug: true,
handler: func(conn *Connection, args ...string) {
conn.Win("win-command")
@ -160,9 +241,9 @@ var winCommand = Command{
var playersCommand = Command{
name: "players",
help: "lists the connected players",
summary: "lists the connected players",
handler: func(conn *Connection, args ...string) {
for other, _ := range currentGame.connections {
for other, _ := range conn.game.connections {
conn.Printf("%v\n", other.Name())
}
},
@ -170,7 +251,7 @@ var playersCommand = Command{
var balCommand = Command{
name: "bal",
help: "displays your current balance in space duckets",
summary: "displays your current balance in space duckets",
handler: func(conn *Connection, args ...string) {
fmt.Fprintln(conn, conn.money)
},

@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net"
"runtime"
"sort"
"strings"
"time"
@ -12,6 +13,7 @@ import (
type Connection struct {
*bufio.Reader
game *Game
net.Conn
ConnectionState
bombs int
@ -30,69 +32,63 @@ func NewConnection(conn net.Conn) *Connection {
bombs: options.startBombs,
money: options.startMoney,
}
c.SetState(SpawnRandomly())
currentGame.Join(c)
c.SetState(EnterLobby())
return c
}
func (c *Connection) Login() {
for {
c.Printf("what is your name, adventurer?\n")
name, err := c.ReadString('\n')
if err == nil {
name = strings.TrimSpace(name)
} else {
log_error("player failed to connect: %v", err)
return
}
if !ValidName(name) {
c.Printf("that name is illegal.\n")
continue
}
log_info("player connected: %v", name)
profile, err := loadProfile(name)
if err != nil {
log_error("could not read profile: %v", err)
profile = &Profile{name: name}
if err := profile.Create(); err != nil {
log_error("unable to create profile record: %v", err)
}
c.Printf("you look new around these parts, %s.\n", profile.name)
c.Printf(`if you'd like a description of how to play, type the "help" command\n`)
c.profile = profile
} else {
c.profile = profile
c.Printf("welcome back, %s.\n", profile.name)
}
break
}
currentGame.Register(c)
}
func (c *Connection) Dead() bool {
return false
}
func (c *Connection) Tick(frame int64) {
func (c *Connection) Tick(game *Game) {
if c.ConnectionState == nil {
log_error("connected client has nil state.")
c.Printf("somehow you have a nil state. I don't know what to do so I'm going to kick you off.")
c.Close()
return
}
c.SetState(c.ConnectionState.Tick(c, frame))
c.SetState(c.ConnectionState.Tick(c, game.frame))
}
func (c *Connection) RunCommand(name string, args ...string) {
defer func() {
if r := recover(); r != nil {
c.Printf("something is broken. Log this as a ticket!\n")
c.Printf("recovered: %v\n", r)
c.Printf("(something is broken)")
c.Printf("ERROR: %v\n", r)
callers := make([]uintptr, 40)
n := runtime.Callers(5, callers)
callers = callers[:n]
frames := runtime.CallersFrames(callers)
log_error("recovered: %v", r)
for {
frame, more := frames.Next()
if !more {
break
}
log_error(" %s +%d (%s)\n", frame.File, frame.Line, frame.Function)
}
}
}()
switch name {
case "commands":
c.ListCommands()
return
}
cmd := c.GetCommand(name)
if cmd == nil {
c.Printf("No such command: %v\n", name)
return
}
cmd.handler(c, args...)
}
func (c *Connection) ListCommands() {
c.Printf("\n")
c.Line()
c.Printf("- Available Commands in state: %s\n", c.ConnectionState.String())
c.Line()
commands := c.Commands()
names := make([]string, len(commands))
@ -102,18 +98,9 @@ func (c *Connection) RunCommand(name string, args ...string) {
sort.Strings(names)
for _, name := range names {
cmd := c.GetCommand(name)
c.Printf("%-20s%s\n", name, cmd.help)
c.Printf("%-20s%s\n", name, cmd.summary)
}
c.Line()
return
}
cmd := c.GetCommand(name)
if cmd == nil {
c.Printf("No such command: %v\n", name)
return
}
cmd.handler(c, args...)
c.Printf("\n")
}
func (c *Connection) SetState(s ConnectionState) {
@ -126,8 +113,8 @@ func (c *Connection) SetState(s ConnectionState) {
c.ConnectionState.Exit(c)
}
log_info("enter state: %v", s)
s.Enter(c)
c.ConnectionState = s
s.Enter(c)
}
func (c *Connection) ReadLines(out chan []string) {
@ -162,7 +149,9 @@ func (c *Connection) Printf(template string, args ...interface{}) (int, error) {
func (c *Connection) Close() error {
log_info("player disconnecting: %s", c.Name())
currentGame.Quit(c)
if c.game != nil {
c.game.Quit(c)
}
if c.Conn != nil {
return c.Conn.Close()
}
@ -177,17 +166,17 @@ func (c *Connection) Name() string {
}
func (c *Connection) RecordScan() {
c.Printf("scanning known systems for signs of life\n")
c.Printf("Scanning known systems for signs of life\n")
c.lastScan = time.Now()
time.AfterFunc(options.scanTime, func() {
c.Printf("scanner ready\n")
c.Printf("Scanner ready\n")
})
}
func (c *Connection) RecordBomb() {
c.lastBomb = time.Now()
time.AfterFunc(15*time.Second, func() {
fmt.Fprintln(c, "bomb arsenal reloaded")
fmt.Fprintln(c, "Bomb arsenal reloaded")
})
}
@ -226,37 +215,9 @@ func (c *Connection) Deposit(n int) {
}
func (c *Connection) Win(method string) {
currentGame.Win(c, method)
c.game.Win(c, method)
}
func (c *Connection) Die(frame int64) {
c.SetState(NewDeadState(frame))
}
type ConnectionState interface {
CommandSuite
String() string
Enter(c *Connection)
Tick(c *Connection, frame int64) ConnectionState
Exit(c *Connection)
}
// No-op enter struct, for composing connection states that have no interesitng
// Enter mechanic.
type NopEnter struct{}
func (n NopEnter) Enter(c *Connection) {}
// No-op exit struct, for composing connection states that have no interesting
// Exit mechanic.
type NopExit struct{}
func (n NopExit) Exit(c *Connection) {}
func SpawnRandomly() ConnectionState {
sys, err := randomSystem()
if err != nil {
return NewErrorState(fmt.Errorf("unable to create idle state: %v", err))
}
return Idle(sys)
}

@ -0,0 +1,39 @@
package main
type ConnectionState interface {
// commands available while in this state
CommandSuite
// human-readable description of the state
String() string
// fills a status struct to be printed by the status command. The
// ConnectionState only needs to fill in things that are unique to the
// state itself, the common things on the connection are filled in
// automatically
FillStatus(*Connection, *status)
// Triggered once when the state is entered
Enter(c *Connection)
// Triggered every frame in which the state is the connection's current
// state. Returning a different ConnectionState transitions between states.
Tick(c *Connection, frame int64) ConnectionState
// Triggered once when this state has finished for that connection
Exit(c *Connection)
}
// No-op enter struct, for composing connection states that have no interesitng
// Enter mechanic.
type NopEnter struct{}
func (n NopEnter) Enter(c *Connection) {}
// No-op exit struct, for composing connection states that have no interesting
// Exit mechanic.
type NopExit struct{}
func (n NopExit) Exit(c *Connection) {}

53
db.go

@ -49,63 +49,12 @@ func planetsData() {
planet.Store(db)
}
}
indexSystems()
}
func edgesTable() {
stmnt := `create table if not exists edges (
id_1 integer,
id_2 integer,
distance real
);`
if _, err := db.Exec(stmnt); err != nil {
log_error("couldn't create distance table: %v", err)
}
// indexSystems()
}
func setupDb() {
planetsTable()
planetsData()
edgesTable()
profilesTable()
gamesTable()
fillEdges()
}
func fillEdges() {
row := db.QueryRow(`select count(*) from edges;`)
var n int
if err := row.Scan(&n); err != nil {
log_error("couldn't get number of edges: %v", err)
return
}
if n > 0 {
return
}
for i := 0; i < len(index); i++ {
for j := 0; j < len(index); j++ {
if i == j {
continue
}
if index[i] == nil {
log_error("wtf there's nil shit in here for id %d", i)
continue
}
if index[j] == nil {
log_error("wtf there's nil shit in here 2 for id %d", j)
continue
}
dist := index[i].DistanceTo(index[j])
log_info("distance from %s to %s: %v", index[i].name, index[j].name, dist)
_, err := db.Exec(`
insert into edges
(id_1, id_2, distance)
values
(?, ?, ?)
;`, i, j, dist)
if err != nil {
log_error("unable to write edge to db: %v", err)
}
}
}
}

@ -1,6 +1,9 @@
package main
import ()
import (
"strings"
"time"
)
type DeadState struct {
CommandSuite
@ -8,16 +11,71 @@ type DeadState struct {
}
func NewDeadState(died int64) ConnectionState {
return &DeadState{start: died}
return &DeadState{
start: died,
CommandSuite: CommandSet{},
}
}
func (d *DeadState) Enter(c *Connection) {
c.Printf("You are dead.\n")
msg := `
Y88b d88P d8888
Y88b d88P d88888
Y88o88P d88P888
Y888P .d88b. 888 888 d88P 888 888d888 .d88b.
888 d88""88b 888 888 d88P 888 888P" d8P Y8b
888 888 888 888 888 d88P 888 888 88888888
888 Y88..88P Y88b 888 d8888888888 888 Y8b.
888 "Y88P" "Y88888 d88P 888 888 "Y8888
____
__,---' '--.__
,-' ; '.
,' '--.'--.
,' '._ '-.
; ; '-- ;
,-'-_ _,-~~-. ,-- '.
;; '-,; ,'~'.__ ,;;; ; ;
;; ;,' ,;; ', ;;; '. ;
': ,' ':; __/ '.; ; ;
;~~^. '. '---'~~ ;; ; ;
',' '. '. .;;; ;'
,',^. '. '._ __ ':; ,'
'-' '--' ~'--'~~'--. ~ ,'
/;'-;_ ; ;. /. / ; ~~'-. ;
-._ ; ; ; ',;'-;__;---; '----'
'--.__ ''-'-;__;: ; ;__;
... '--.__ '-- '-'
'--.:::... '--.__ ____
'--:::::--. '--.__ __,--' '.
'--:::';.... '--' ___ '.
'--'-:::... __ ) ;
~'-:::... '---. ( ,'
~'-:::::::::'--. '-.
~'-::::::::'. ;
~'--:::,' ,'
~~'--'~
8888888b. 8888888888 d8888 8888888b.
888 "Y88b 888 d88888 888 "Y88b
888 888 888 d88P888 888 888
888 888 8888888 d88P 888 888 888
888 888 888 d88P 888 888 888
888 888 888 d88P 888 888 888
888 .d88P 888 d8888888888 888 .d88P
8888888P" 8888888888 d88P 888 8888888P"
`
lines := strings.Split(msg, "\n")
for _, line := range lines {
c.Write([]byte(line + "\n"))
time.Sleep(20 * time.Millisecond)
}
}
func (d *DeadState) Tick(c *Connection, frame int64) ConnectionState {
if frame-d.start > options.respawnFrames {
return SpawnRandomly()
return c.game.SpawnPlayer()
}
return d
}
@ -26,6 +84,55 @@ func (d *DeadState) Exit(c *Connection) {
c.Printf("You're alive again.\n")
}
func (d *DeadState) String() string {
return "dead"
func (d *DeadState) String() string { return "dead" }
func (d *DeadState) FillStatus(c *Connection, s *status) {
s.Description = `
Y88b d88P d8888
Y88b d88P d88888
Y88o88P d88P888
Y888P .d88b. 888 888 d88P 888 888d888 .d88b.
888 d88""88b 888 888 d88P 888 888P" d8P Y8b
888 888 888 888 888 d88P 888 888 88888888
888 Y88..88P Y88b 888 d8888888888 888 Y8b.
888 "Y88P" "Y88888 d88P 888 888 "Y8888
____
__,---' '--.__
,-' ; '.
,' '--.'--.
,' '._ '-.
; ; '-- ;
,-'-_ _,-~~-. ,-- '.
;; '-,; ,'~'.__ ,;;; ; ;
;; ;,' ,;; ', ;;; '. ;
': ,' ':; __/ '.; ; ;
;~~^. '. '---'~~ ;; ; ;
',' '. '. .;;; ;'
,',^. '. '._ __ ':; ,'
'-' '--' ~'--'~~'--. ~ ,'
/;'-;_ ; ;. /. / ; ~~'-. ;
-._ ; ; ; ',;'-;__;---; '----'
'--.__ ''-'-;__;: ; ;__;
... '--.__ '-- '-'
'--.:::... '--.__ ____
'--:::::--. '--.__ __,--' '.
'--:::';.... '--' ___ '.
'--'-:::... __ ) ;
~'-:::... '---. ( ,'
~'-:::::::::'--. '-.
~'-::::::::'. ;
~'--:::,' ,'
~~'--'~
8888888b. 8888888888 d8888 8888888b.
888 "Y88b 888 d88888 888 "Y88b
888 888 888 d88P888 888 888
888 888 8888888 d88P 888 888 888
888 888 888 d88P 888 888 888
888 888 888 d88P 888 888 888
888 .d88P 888 d8888888888 888 .d88P
8888888P" 8888888888 d88P 888 8888888P"
`
}

@ -62,3 +62,5 @@ func (e *ErrorState) String() string {
func (e *ErrorState) RunCommand(c *Connection, name string, args ...string) ConnectionState {
return e
}
func (e *ErrorState) FillStatus(c *Connection, s *status) {}

@ -0,0 +1,85 @@
package main
import (
"math/rand"
"sort"
"strconv"
)
// Galaxy is a collection of systems
type Galaxy struct {
systems map[int]*System
names map[string]int
}
func NewGalaxy() *Galaxy {
g := &Galaxy{
systems: make(map[int]*System),
names: make(map[string]int),
}
g.indexSystems()
return g
}
func (g *Galaxy) indexSystems() {
rows, err := db.Query(`select * from planets`)
if err != nil {
log_error("unable to select all planets: %v", err)
return
}
defer rows.Close()
for rows.Next() {
s := System{}
if err := rows.Scan(&s.id, &s.name, &s.x, &s.y, &s.z, &s.planets); err != nil {
log_info("unable to scan planet row: %v", err)
continue
}
g.systems[s.id] = &s
g.names[s.name] = s.id
s.money = int64(rand.NormFloat64()*options.moneySigma + options.moneyMean)
}
}
// GetSystem gets a system by either ID or name. If the provided string
// contains an integer, we assume the lookup is intended to be by ID.
func (g *Galaxy) GetSystem(s string) *System {
id, err := strconv.Atoi(s)
if err == nil {
return g.GetSystemByID(id)
}
return g.GetSystemByName(s)
}
func (g *Galaxy) GetSystemByID(id int) *System {
return g.systems[id]
}
func (g *Galaxy) GetSystemByName(name string) *System {
id := g.SystemID(name)
if id == 0 {
return nil
}
return g.GetSystemByID(id)
}
func (g *Galaxy) SystemID(name string) int { return g.names[name] }
// Neighborhood generates the neighborhood for a given system.
func (g *Galaxy) Neighborhood(sys *System) Neighborhood {
neighbors := make(Neighborhood, 0, len(g.systems))
for id, sys2 := range g.systems {
if id == sys.id {
continue
}
neighbors = append(neighbors, Neighbor{id: id, distance: sys.DistanceTo(sys2)})
}
sort.Sort(neighbors)
return neighbors
}
func (g *Galaxy) randomSystem() *System {
id := rand.Intn(len(g.systems))
return g.GetSystemByID(id)
}

@ -2,11 +2,12 @@ package main
import (
"fmt"
"math/rand"
"time"
)
type Game struct {
id Id
id string
start time.Time
end time.Time
done chan interface{}
@ -15,6 +16,7 @@ type Game struct {
connections map[*Connection]bool
frame int64
elems map[GameElement]bool
galaxy *Galaxy
}
func gamesTable() {
@ -30,28 +32,32 @@ func gamesTable() {
}
}
func init() { rand.Seed(time.Now().UnixNano()) }
func newID() string {
chars := []rune("ABCDEEEEEEEEFGHJJJJJJJKMNPQQQQQQQRTUVWXXXXXYZZZZZ234677777789")
id := make([]rune, 0, 4)
for i := 0; i < cap(id); i++ {
id = append(id, chars[rand.Intn(len(chars))])
}
return string(id)
}
func NewGame() *Game {
game := &Game{
id: NewId(),
id: newID(),
start: time.Now(),
done: make(chan interface{}),
connections: make(map[*Connection]bool, 32),
elems: make(map[GameElement]bool, 32),
galaxy: NewGalaxy(),
}
if err := game.Create(); err != nil {
log_error("unable to create game: %v", err)
}
for _, system := range index {
for _, system := range game.galaxy.systems {
game.Register(system)
}
if currentGame != nil {
log_info("passing %d connections...", len(currentGame.connections))
for conn, _ := range currentGame.connections {
log_info("moving player %s to new game", conn.Name())
currentGame.Quit(conn)
game.Join(conn)
}
}
return game
}
@ -61,7 +67,7 @@ func (g *Game) Create() error {
(id, start)
values
(?, ?)
;`, g.id.String(), g.start)
;`, g.id, g.start)
if err != nil {
return fmt.Errorf("error writing sqlite insert statement to create game: %v")
}
@ -78,7 +84,12 @@ func (g *Game) Store() error {
}
func (g *Game) Join(conn *Connection) {
log_info("Player %s has joined game %s", conn.Name(), g.id)
for there, _ := range g.connections {
there.Printf("Player %s has joined the game", conn.Name())
}
g.connections[conn] = true
g.Register(conn)
}
func (g *Game) Quit(conn *Connection) {
@ -97,6 +108,8 @@ func (g *Game) Win(winner *Connection, method string) {
for conn, _ := range g.connections {
conn.Printf("player %s has won by %s victory.\n", winner.Name(), method)
}
gm.Remove(g)
}
func (g *Game) Reset() {
@ -128,7 +141,7 @@ func (g *Game) Register(elem GameElement) {
func (g *Game) tick() {
g.frame += 1
for elem := range g.elems {
elem.Tick(g.frame)
elem.Tick(g)
}
for elem := range g.elems {
if elem.Dead() {
@ -138,7 +151,11 @@ func (g *Game) tick() {
}
}
func (g *Game) SpawnPlayer() ConnectionState {
return Idle(g.galaxy.randomSystem())
}
type GameElement interface {
Tick(frame int64)
Tick(*Game)
Dead() bool
}

@ -0,0 +1,41 @@
package main
import (
"sync"
)
var gm *GameManager
func init() {
gm = &GameManager{
games: make(map[string]*Game, 32),
}
}
type GameManager struct {
games map[string]*Game
sync.Mutex
}
func (g *GameManager) NewGame() *Game {
g.Lock()
defer g.Unlock()
game := NewGame()
g.games[game.id] = game
return game
}
func (g *GameManager) Get(id string) *Game {
g.Lock()
defer g.Unlock()
return g.games[id]
}
func (g *GameManager) Remove(game *Game) {
g.Lock()
defer g.Unlock()
delete(g.games, game.id)
}

55
id.go

@ -1,55 +0,0 @@
package main
import (
"encoding/binary"
"fmt"
"sync/atomic"
"time"
)
// NewObjectId returns a new unique ObjectId.
// This function causes a runtime error if it fails to get the hostname
// of the current machine.
func NewId() Id {
b := make([]byte, 12)
// Timestamp, 4 bytes, big endian
binary.BigEndian.PutUint32(b, uint32(time.Now().Unix()))
b[4] = global.machineId[0]
b[5] = global.machineId[1]
b[6] = global.machineId[2]
// Pid, 2 bytes, specs don't specify endianness, but we use big endian.
b[7] = byte(global.pid >> 8)
b[8] = byte(global.pid)
// Increment, 3 bytes, big endian
i := atomic.AddUint32(&global.idCounter, 1)
b[9] = byte(i >> 16)
b[10] = byte(i >> 8)
b[11] = byte(i)
return Id(b)
}
// Id is used for tagging each incoming http request for logging
// purposes. The actual implementation is just the ObjectId implementation
// found in launchpad.net/mgo/bson. This will most likely change and evolve
// into its own format.
type Id string
func (id Id) String() string {
return fmt.Sprintf("%x", string(id))
}
// Time returns the timestamp part of the id.
// It's a runtime error to call this method with an invalid id.
func (id Id) Time() time.Time {
secs := int64(binary.BigEndian.Uint32(id.byteSlice(0, 4)))
return time.Unix(secs, 0)
}
// byteSlice returns byte slice of id from start to end.
// Calling this function with an invalid id will cause a runtime panic.
func (id Id) byteSlice(start, end int) []byte {
if len(id) != 12 {
panic(fmt.Sprintf("Invalid Id: %q", string(id)))
}
return []byte(string(id)[start:end])
}

@ -15,43 +15,37 @@ func Idle(sys *System) ConnectionState {
i := &IdleState{System: sys}
i.CommandSuite = CommandSet{
balCommand,
helpCommand,
playersCommand,
BroadcastCommand(sys),
NearbyCommand(sys),
Command{
name: "goto",
help: "travel between star systems",
summary: "travel between star systems",
arity: 1,
handler: i.travelTo,
},
Command{
name: "bomb",
help: "bomb another star system",
summary: "bomb another star system",
arity: 1,
usage: "bomb [system-name or system-id]",
handler: i.bomb,
},
Command{
name: "mine",
help: "mine the current system for resources",
summary: "mine the current system for resources",
arity: 0,
handler: i.mine,
},
Command{
name: "info",
help: "gives you information about the current star system",
arity: 0,
handler: i.info,
},
Command{
name: "scan",
help: "scans the galaxy for signs of life",
summary: "scans the galaxy for signs of life",
arity: 0,
handler: i.scan,
},
Command{
name: "make",
help: "makes things",
summary: "makes things",
handler: i.maek,
},
}
@ -71,9 +65,9 @@ func (i *IdleState) Tick(c *Connection, frame int64) ConnectionState {
}
func (i *IdleState) travelTo(c *Connection, args ...string) {
dest, err := GetSystem(args[0])
if err != nil {
c.Printf("%v\n", err)
dest := c.game.galaxy.GetSystem(args[0])
if dest == nil {
c.Printf("no such system: %s", args[0])
return
}
c.SetState(NewTravel(c, i.System, dest))
@ -89,37 +83,36 @@ func (i *IdleState) bomb(c *Connection, args ...string) {
return
}
target, err := GetSystem(args[0])
if err != nil {
c.Printf("Cannot send bomb: %v\n", err)
target := c.game.galaxy.GetSystem(args[0])
if target == nil {
c.Printf("Cannot send bomb: no such system: %v\n", args[0])
return
}
c.bombs -= 1
c.lastBomb = time.Now()
bomb := NewBomb(c, i.System, target)
currentGame.Register(bomb)
c.game.Register(bomb)
}
func (i *IdleState) mine(c *Connection, args ...string) {
c.SetState(Mine(i.System))
}
func (i *IdleState) info(c *Connection, args ...string) {
c.Printf("Currently idle on system %v\n", i.System)
c.Printf("Space duckets available: %v\n", i.money)
}
func (i *IdleState) scan(c *Connection, args ...string) {
if time.Since(c.lastScan) < 1*time.Minute {
return
}
c.Printf("Scanning the galaxy for signs of life...\n")
currentGame.Register(NewScan(i.System))
c.game.Register(NewScan(i.System, c.game.galaxy.Neighborhood(i.System)))
}
// "make" is already a keyword
func (i *IdleState) maek(c *Connection, args ...string) {
if len(args) != 1 {
c.Printf("not sure what to do! Expecting a command like this: make [thing]\ne.g.:\nmake bomb\nmake colony")
return
}
switch args[0] {
case "bomb":
if c.money < options.bombCost {
@ -136,3 +129,8 @@ func (i *IdleState) maek(c *Connection, args ...string) {
c.Printf("I don't know how to make a %v.\n", args[0])
}
}
func (i *IdleState) FillStatus(c *Connection, s *status) {
s.Location = i.System.String()
s.Description = "Just hanging out, enjoying outer space."
}

@ -0,0 +1,185 @@
package main
import (
"strings"
"time"
)
var banner = `
##############################################################################################
/$$$$$$$$ /$$
| $$_____/ | $$
| $$ /$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$ | $$ /$$$$$$ /$$$$$$$ /$$ /$$ /$$$$$$$
| $$$$$ | $$ /$$/ /$$__ $$ /$$_____/ /$$__ $$| $$ /$$__ $$| $$__ $$| $$ | $$ /$$_____/
| $$__/ \ $$$$/ | $$ \ $$| $$ | $$ \ $$| $$| $$ \ $$| $$ \ $$| $$ | $$| $$$$$$
| $$ >$$ $$ | $$ | $$| $$ | $$ | $$| $$| $$ | $$| $$ | $$| $$ | $$ \____ $$
| $$$$$$$$ /$$/\ $$| $$$$$$/| $$$$$$$| $$$$$$/| $$| $$$$$$/| $$ | $$| $$$$$$/ /$$$$$$$/
|________/|__/ \__/ \______/ \_______/ \______/ |__/ \______/ |__/ |__/ \______/ |_______/
~+
* +
' |
() .-.,="''"=. - o -
'=/_ \ |
* | '=._ |
\ '=./', '
. '=.__.=' '=' *
+ +
O * ' .
A game of dark cunning in the vast unknown of space by Jordan Orelli.
##############################################################################################
`
type LobbyState struct {
CommandSuite
NopExit
}
func EnterLobby() ConnectionState {
return &LobbyState{
CommandSuite: CommandSet{
newGameCommand,
joinGameCommand,
listGamesCommand,
},
}
}
func (st *LobbyState) String() string { return "Lobby" }
func (st *LobbyState) Enter(c *Connection) {
c.Printf(strings.TrimSpace(banner))
time.Sleep(1 * time.Second)
for {
c.Printf("\n\nWhat is your name, adventurer?\n")
name, err := c.ReadString('\n')
if err == nil {
name = strings.TrimSpace(name)
} else {
log_error("player failed to connect: %v", err)
return
}
if !ValidName(name) {
c.Printf("that name is illegal.\n")
continue
}
log_info("player connected: %v", name)
profile, err := loadProfile(name)
if err != nil {
log_error("could not read profile: %v", err)
profile = &Profile{name: name}
if err := profile.Create(); err != nil {
log_error("unable to create profile record: %v", err)
}
c.Printf("you look new around these parts, %s.\n", profile.name)
c.Printf(`if you'd like a description of how to play, type the "help" command\n`)
c.profile = profile
} else {
c.profile = profile
c.Printf("Welcome back, %s.\n", profile.name)
}
break
}
c.ListCommands()
}
func (st *LobbyState) Tick(c *Connection, frame int64) ConnectionState { return st }
func (st *LobbyState) FillStatus(c *Connection, s *status) {
s.Description = strings.TrimSpace(`
Currently in the Lobby, waiting for you to issue a "new" command to start a new
game, or a "join" command to join an existing game.
`)
}
var newGameCommand = Command{
name: "new",
summary: "starts a new game",
arity: 0,
variadic: false,
handler: func(c *Connection, args ...string) {
c.Printf("Starting a new game...\n")
game := gm.NewGame()
log_info("%s Created game: %s", c.profile.name, game.id)
go game.Run()
c.game = game
c.Printf("Now playing in game: %s\n\n", game.id)
c.Line()
c.game.Join(c)
c.SetState(game.SpawnPlayer())
},
debug: false,
}
var joinGameCommand = Command{
name: "join",
summary: "joins an existing game",
usage: "join [game-code]",
arity: 1,
variadic: false,
handler: func(c *Connection, args ...string) {
if len(args) == 0 {
gm.Lock()
defer gm.Unlock()
if len(gm.games) == 1 {
for _, game := range gm.games {
c.game = game
log_info("%s Joining game: %s", c.profile.name, c.game.id)
c.Printf("You have joined game %s\n", game.id)
c.SetState(game.SpawnPlayer())
c.game.Join(c)
return
}
}
c.Printf(strings.TrimLeft(`
Missing game code! When a player starts a game, they will be given a code to
identify their game. Use this game to join the other player's game.
Usage: join [game-code]`, " \n\t"))
return
}
id := args[0]
game := gm.Get(id)
c.game = game
log_info("%s Joining game: %s", c.profile.name, c.game.id)
c.Printf("You have joined game %s\n", game.id)
c.SetState(game.SpawnPlayer())
c.game.Join(c)
},
debug: false,
}
var listGamesCommand = Command{
name: "list",
summary: "lists game lobbies that can be joined",
usage: "list",
arity: 0,
variadic: false,
handler: func(c *Connection, args ...string) {
gm.Lock()
defer gm.Unlock()
c.Line()
c.Printf("%-8s %-20s\n", "Game", "Player")
c.Line()
for id, game := range gm.games {
c.Printf("%-8s %-20s\n", id, "")
for conn, _ := range game.connections {
if conn.profile != nil {
c.Printf("%-8s %-20s\n", "", conn.profile.name)
}
}
c.Printf("--------------------\n")
}
},
}

@ -18,7 +18,7 @@ var options struct {
frameLength time.Duration
colonyCost int
frameRate int
lightSpeed float64
lightSpeed float64 // the distance that light travels in one tick
makeBombTime time.Duration
makeColonyTime time.Duration
makeShieldTime time.Duration
@ -36,7 +36,6 @@ var options struct {
var (
info_log *log.Logger
error_log *log.Logger
currentGame *Game
)
func log_error(template string, args ...interface{}) {
@ -56,9 +55,9 @@ func bail(status int, template string, args ...interface{}) {
os.Exit(status)
}
func handleConnection(conn *Connection) {
func handleConnection(sock net.Conn) {
conn := NewConnection(sock)
defer conn.Close()
conn.Login()
c := make(chan []string)
go conn.ReadLines(c)
@ -88,18 +87,12 @@ func main() {
error_log = log.New(os.Stderr, "[ERROR] ", 0)
setupDb()
listener, err := net.Listen("tcp", ":9220")
addr := ":9220"
listener, err := net.Listen("tcp", addr)
if err != nil {
bail(E_No_Port, "unable to start server: %v", err)
}
go func() {
for {
log_info("starting new game")
currentGame = NewGame()
currentGame.Run()
}
}()
log_info("listening on %s", addr)
for {
conn, err := listener.Accept()
@ -107,7 +100,7 @@ func main() {
log_error("error accepting connection: %v", err)
continue
}
go handleConnection(NewConnection(conn))
go handleConnection(conn)
}
}

@ -2,6 +2,7 @@ package main
import (
"fmt"
"strings"
)
type MiningState struct {
@ -14,22 +15,15 @@ func Mine(sys *System) ConnectionState {
m := &MiningState{System: sys}
m.CommandSuite = CommandSet{
balCommand,
helpCommand,
playersCommand,
BroadcastCommand(sys),
NearbyCommand(sys),
Command{
name: "stop",
help: "stops mining",
summary: "stops mining",
arity: 0,
handler: m.stop,
},
Command{
name: "info",
help: "gives you information about the current mining operation",
arity: 0,
handler: m.info,
},
}
return m
}
@ -66,8 +60,11 @@ func (m *MiningState) stop(c *Connection, args ...string) {
c.SetState(Idle(m.System))
}
func (m *MiningState) info(c *Connection, args ...string) {
c.Printf("Currently mining system %v\n", m.System)
c.Printf("Mined so far: %v\n", m.mined)
c.Printf("Remaining space duckets on %v: %v\n", m.System, m.money)
func (m *MiningState) FillStatus(c *Connection, s *status) {
s.Location = m.System.String()
s.Description = strings.TrimSpace(fmt.Sprintf(`
Currently mining on system: %s
Mined so far: %d
Available space duckets: %d
`, m.System.String(), m.mined, m.money))
}

@ -12,6 +12,7 @@ type scan struct {
nextHitIndex int
nextEchoIndex int
results []scanResult
neighborhood Neighborhood
}
type scanResult struct {
@ -38,36 +39,40 @@ func (r *scanResult) playerNames() []string {
return names
}
func NewScan(origin *System) *scan {
func NewScan(origin *System, n Neighborhood) *scan {
return &scan{
origin: origin,
start: time.Now(),
results: make([]scanResult, 0, len(origin.Distances())),
results: make([]scanResult, 0, len(n)),
neighborhood: n,
}
}
func (s *scan) Tick(frame int64) {
func (s *scan) Tick(game *Game) {
s.dist += options.lightSpeed
s.hits()
s.hits(game)
s.echos()
}
func (s *scan) Dead() bool {
return s.nextEchoIndex >= len(s.origin.Distances())
return s.neighborhood == nil
}
func (s *scan) String() string {
return fmt.Sprintf("[scan origin: %s start_time: %v]", s.origin.name, s.start)
}
func (s *scan) hits() {
for ; s.nextHitIndex < len(s.origin.Distances()); s.nextHitIndex += 1 {
candidate := s.origin.Distances()[s.nextHitIndex]
if s.dist < candidate.dist {
break
func (s *scan) hits(game *Game) {
for len(s.neighborhood) > 0 && s.neighborhood[0].distance <= s.dist {
sys := game.galaxy.GetSystemByID(s.neighborhood[0].id)
s.results = append(s.results, s.hitSystem(sys, s.neighborhood[0].distance))
log_info("scan hit %v. Traveled %v in %v", sys.name, s.neighborhood[0].distance, time.Since(s.start))
if len(s.neighborhood) > 1 {
s.neighborhood = s.neighborhood[1:]
} else {
s.neighborhood = nil
}
s.results = append(s.results, s.hitSystem(candidate.s, candidate.dist))
log_info("scan hit %v. Traveled %v in %v", candidate.s.name, candidate.dist, time.Since(s.start))
}
}

@ -10,7 +10,6 @@ func MakeShield(c *Connection, s *System) {
CommandSuite: CommandSet{
balCommand,
BroadcastCommand(s),
helpCommand,
NearbyCommand(s),
playersCommand,
},
@ -47,11 +46,15 @@ func (m *MakeShieldState) String() string {
return fmt.Sprintf("Making shield on %v", m.System)
}
func (m *MakeShieldState) FillStatus(c *Connection, s *status) {
s.Location = m.System.String()
}
type Shield struct {
energy float64
}
func (s *Shield) Tick(frame int64) {
func (s *Shield) Tick() {
if s.energy < 1000 {
s.energy += (1000 - s.energy) * 0.0005
}

@ -4,16 +4,9 @@ import (
"database/sql"
"fmt"
"math"
"math/rand"
"strconv"
"time"
)
var (
index map[int]*System
nameIndex map[string]*System
)
type System struct {
*Shield
id int
@ -26,29 +19,13 @@ type System struct {
money int64
}
func GetSystem(id string) (*System, error) {
idNum, err := strconv.Atoi(id)
if err == nil {
sys, ok := index[idNum]
if !ok {
return nil, fmt.Errorf("No such system: %v", idNum)
}
return sys, nil
}
sys, ok := nameIndex[id]
if !ok {
return nil, fmt.Errorf("No such system: %v", id)
}
return sys, nil
}
func (s *System) Tick(frame int64) {
func (s *System) Tick(game *Game) {
if s.colonizedBy != nil && s.money > 0 {
s.colonizedBy.Deposit(1)
s.money -= 1
}
if s.Shield != nil {
s.Shield.Tick(frame)
s.Shield.Tick()
}
}
@ -62,7 +39,6 @@ func (s *System) Reset() {
}
func (s *System) Arrive(conn *Connection) {
// conn.SetSystem(s)
if s.players[conn] {
return
}
@ -80,7 +56,6 @@ func (s *System) Arrive(conn *Connection) {
func (s *System) Leave(p *Connection) {
delete(s.players, p)
// p.location = nil
}
func (s *System) NotifyInhabitants(template string, args ...interface{}) {
@ -138,38 +113,7 @@ type Ray struct {
dist float64 // distance in parsecs
}
func (s *System) Distances() []Ray {
if s.distances == nil {
s.distances = make([]Ray, 0, 551)
rows, err := db.Query(`
select edges.id_2, edges.distance
from edges
where edges.id_1 = ?
order by distance
;`, s.id)
if err != nil {
log_error("unable to query for system distances: %v", err)
return nil
}
for rows.Next() {
var (
r Ray
id int
dist float64
)
if err := rows.Scan(&id, &dist); err != nil {
log_error("unable to unpack Ray from sql result: %v", err)
continue
}
r.s = index[id]
r.dist = dist
s.distances = append(s.distances, r)
}
}
return s.distances
}
func (s *System) Bombed(bomber *Connection, frame int64) {
func (s *System) Bombed(bomber *Connection, game *Game) {
if s.Shield != nil {
if s.Shield.Hit() {
s.EachConn(func(conn *Connection) {
@ -182,7 +126,7 @@ func (s *System) Bombed(bomber *Connection, frame int64) {
}
s.EachConn(func(conn *Connection) {
conn.Die(frame)
conn.Die(game.frame)
s.Leave(conn)
bomber.MadeKill(conn)
})
@ -191,21 +135,20 @@ func (s *System) Bombed(bomber *Connection, frame int64) {
s.colonizedBy = nil
}
for id, _ := range index {
for id, other := range game.galaxy.systems {
if id == s.id {
continue
}
delay := s.LightTimeTo(index[id])
id2 := id
delay := s.LightTimeTo(game.galaxy.systems[id])
from := s
to := other
time.AfterFunc(delay, func() {
bombNotice(id2, s.id)
bombNotice(to, from)
})
}
}
func bombNotice(to_id, from_id int) {
to := index[to_id]
from := index[from_id]
func bombNotice(to, from *System) {
to.EachConn(func(conn *Connection) {
conn.Printf("a bombing has been observed on %s\n", from.name)
})
@ -215,36 +158,17 @@ func (s System) String() string {
return fmt.Sprintf("%s (id: %v)", s.name, s.id)
}
type Neighborhood []Neighbor
func (n Neighborhood) Len() int { return len(n) }
func (n Neighborhood) Less(i, j int) bool { return n[i].distance < n[j].distance }
func (n Neighborhood) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
type Neighbor struct {
id int
distance float64
}
func (e *System) Nearby(n int) ([]Neighbor, error) {
rows, err := db.Query(`
select planets.id, edges.distance
from edges
join planets on edges.id_2 = planets.id
where edges.id_1 = ?
order by distance
limit ?
;`, e.id, n)
if err != nil {
log_error("unable to get nearby systems for %s: %v", e.name, err)
return nil, err
}
neighbors := make([]Neighbor, 0, n)
for rows.Next() {
var neighbor Neighbor
if err := rows.Scan(&neighbor.id, &neighbor.distance); err != nil {
log_error("error unpacking row from nearby neighbors query: %v", err)
continue
}
neighbors = append(neighbors, neighbor)
}
return neighbors, nil
}
func countSystems() (int, error) {
row := db.QueryRow(`select count(*) from planets`)
@ -261,35 +185,25 @@ func dist3d(x1, y1, z1, x2, y2, z2 float64) float64 {
return math.Sqrt(sq(x1-x2) + sq(y1-y2) + sq(z1-z2))
}
func indexSystems() map[int]*System {
rows, err := db.Query(`select * from planets`)
if err != nil {
log_error("unable to select all planets: %v", err)
return nil
}
defer rows.Close()
index = make(map[int]*System, 551)
nameIndex = make(map[string]*System, 551)
for rows.Next() {
p := System{}
if err := rows.Scan(&p.id, &p.name, &p.x, &p.y, &p.z, &p.planets); err != nil {
log_info("unable to scan planet row: %v", err)
continue
}
index[p.id] = &p
nameIndex[p.name] = &p
p.money = int64(rand.NormFloat64()*options.moneySigma + options.moneyMean)
log_info("seeded system %v with %v monies", p, p.money)
}
return index
}
func randomSystem() (*System, error) {
n := len(index)
if n == 0 {
return nil, fmt.Errorf("no planets are known to exist")
}
pick := rand.Intn(n)
sys := index[pick]
return sys, nil
}
// func indexSystems() map[int]*System {
// rows, err := db.Query(`select * from planets`)
// if err != nil {
// log_error("unable to select all planets: %v", err)
// return nil
// }
// defer rows.Close()
// index = make(map[int]*System, 551)
// nameIndex = make(map[string]*System, 551)
// for rows.Next() {
// p := System{}
// if err := rows.Scan(&p.id, &p.name, &p.x, &p.y, &p.z, &p.planets); err != nil {
// log_info("unable to scan planet row: %v", err)
// continue
// }
// index[p.id] = &p
// nameIndex[p.name] = &p
// p.money = int64(rand.NormFloat64()*options.moneySigma + options.moneyMean)
// // log_info("seeded system %v with %v monies", p, p.money)
// }
// return index
// }

@ -1,7 +1,9 @@
package main
import (
"bytes"
"fmt"
"text/template"
"time"
)
@ -20,39 +22,76 @@ func NewTravel(c *Connection, start, dest *System) ConnectionState {
dist: start.DistanceTo(dest),
}
t.CommandSuite = CommandSet{
helpCommand,
playersCommand,
balCommand,
Command{
name: "progress",
help: "displays how far you are along your travel",
summary: "displays how far you are along your travel",
arity: 0,
handler: t.progress,
},
Command{
name: "eta",
help: "displays estimated time of arrival",
summary: "displays estimated time of arrival",
arity: 0,
handler: func(c *Connection, args ...string) {
c.Printf("Remaining: %v\n", t.remaining())
c.Printf("Current time: %v\n", time.Now())
c.Printf("ETA: %v\n", t.eta())
c.Printf("%v\n", t.remaining())
},
},
}
return t
}
var enterTravelTemplate = template.Must(template.New("enter-travel").Parse(`
Departing: {{.Departing}}
Destination: {{.Destination}}
Total Trip Time: {{.Duration}}
`))
func (t *TravelState) Enter(c *Connection) {
c.Printf("Leaving %v, bound for %v.\n", t.start, t.dest)
c.Printf("Trip duration: %v\n", t.tripTime())
c.Printf("Current time: %v\n", time.Now())
c.Printf("ETA: %v\n", t.eta())
enterTravelTemplate.Execute(c, struct {
Departing *System
Destination *System
Duration time.Duration
}{
t.start,
t.dest,
t.tripTime(),
})
t.start.Leave(c)
}
func (t *TravelState) Tick(c *Connection, frame int64) ConnectionState {
t.travelled += options.playerSpeed * options.lightSpeed
dt := options.playerSpeed * options.lightSpeed
segmentLength := t.dist / 18
x := t.travelled
for x > segmentLength {
x -= segmentLength
}
if x < dt {
c.Printf("%v", t.start.name)
var buf bytes.Buffer
segment := int(t.travelled / t.dist * 18)
buf.WriteRune('|')
for i := 0; i < 18; i++ {
switch {
case i == segment:
buf.WriteRune('>')
case i == segment-1:
buf.WriteRune('=')
case i < segment:
buf.WriteRune('-')
default:
buf.WriteRune(' ')
}
}
buf.WriteRune('|')
c.Write(buf.Bytes())
c.Printf("at %v in %v\n", t.dest.name, t.remaining())
}
t.travelled += dt
if t.travelled >= t.dist {
return Idle(t.dest)
}
@ -68,6 +107,10 @@ func (t *TravelState) String() string {
return fmt.Sprintf("Traveling from %v to %v", t.start, t.dest)
}
func (t *TravelState) FillStatus(c *Connection, s *status) {
s.Location = fmt.Sprintf("between %s and %s", t.start, t.dest)
}
func (t *TravelState) progress(c *Connection, args ...string) {
c.Printf("%v\n", t.travelled/t.dist)
}

Loading…
Cancel
Save