Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
calvinmclean committed Jan 15, 2024
0 parents commit 57c4413
Show file tree
Hide file tree
Showing 18 changed files with 1,232 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
farm.data
TODO.md
vendor/
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Terminal Tiller

Terminal Tiller is a simple idle game to grow, harvest, and sell crops from your terminal!

Built with tools from [charm.sh](https://charm.sh)!


## Install

```shell
go install github.com/calvinmclean/terminal-tiller@latest
```

## Play
After installing, simply run:

```shell
terminal-tiller
```

This will create a new save file in `~/.terminal-tiller`

See the on-screen usage instructions and start planting! When you close the game, the state is saved and your crops will continue growing!
193 changes: 193 additions & 0 deletions farm/crop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package farm

import "time"

const (
Broccoli CropType = iota
Carrot
Corn
Cucumber
Eggplant
Garlic
Ginger
Lettuce
Onion
Pea
Potato
Strawberry
Tomato
Watermelon
Yam
)

var (
CropTypes = map[CropType]CropTypeDetails{
Broccoli: {
Name: "Broccoli",
Representation: "🥦",
HarvestTime: 10,
SeedCost: 80,
MarketPrice: 150,
},
Carrot: {
Name: "Carrot",
Representation: "🥕",
HarvestTime: 4,
SeedCost: 20,
MarketPrice: 35,
},
Corn: {
Name: "Corn",
Representation: "🌽",
HarvestTime: 6,
SeedCost: 30,
MarketPrice: 55,
},
Cucumber: {
Name: "Cucumber",
Representation: "🥒",
HarvestTime: 5,
SeedCost: 150,
MarketPrice: 275,
},
Eggplant: {
Name: "Eggplant",
Representation: "🍆",
HarvestTime: 5,
SeedCost: 125,
MarketPrice: 200,
},
Garlic: {
Name: "Garlic",
Representation: "🧄",
HarvestTime: 15,
SeedCost: 100,
MarketPrice: 200,
},
Ginger: {
Name: "Ginger",
Representation: "🫚 ",
HarvestTime: 11,
SeedCost: 90,
MarketPrice: 175,
},
Lettuce: {
Name: "Lettuce",
Representation: "🥬",
HarvestTime: 3,
SeedCost: 9,
MarketPrice: 15,
},
Onion: {
Name: "Onion",
Representation: "🧅",
HarvestTime: 12,
SeedCost: 80,
MarketPrice: 155,
},
Pea: {
Name: "Pea",
Representation: "🫛 ",
HarvestTime: 3,
SeedCost: 25,
MarketPrice: 40,
},
Potato: {
Name: "Potato",
Representation: "🥔",
HarvestTime: 10,
SeedCost: 50,
MarketPrice: 80,
},
Strawberry: {
Name: "Strawberry",
Representation: "🍓",
HarvestTime: 6,
SeedCost: 40,
MarketPrice: 65,
},
Tomato: {
Name: "Tomato",
Representation: "🍅",
HarvestTime: 5,
SeedCost: 70,
MarketPrice: 130,
},
Watermelon: {
Name: "Watermelon",
Representation: "🍉",
HarvestTime: 8,
SeedCost: 60,
MarketPrice: 100,
},
Yam: {
Name: "Yam",
Representation: "🍠",
HarvestTime: 10,
SeedCost: 75,
MarketPrice: 160,
},
}
)

type CropType int

func (c CropType) Name() string {
return CropTypes[c].Name
}

func (c CropType) Representation() string {
return CropTypes[c].Representation
}

func (c CropType) HarvestTime(scale time.Duration) time.Duration {
return time.Duration(CropTypes[c].HarvestTime) * scale
}

func (c CropType) SeedCost() int {
return CropTypes[c].SeedCost
}

func (c CropType) MarketPrice() int {
return CropTypes[c].MarketPrice
}

type CropTypeDetails struct {
Name string
Representation string
HarvestTime int
SeedCost int
MarketPrice int
}

type Crop struct {
Type CropType
PlantedAt time.Time
}

func NewCrop(cropType CropType) *Crop {
_, ok := CropTypes[cropType]
if !ok {
panic("invalid crop type")
}

return &Crop{
Type: cropType,
PlantedAt: time.Now(),
}
}

func (c *Crop) String(scale time.Duration) string {
if !c.ReadyToHarvest(scale) {
return "🌱"
}
return c.Type.Representation()
}

func (c *Crop) ReadyToHarvest(scale time.Duration) bool {
return time.Now().After(c.HarvestTime(scale))
}

func (c *Crop) HarvestTime(scale time.Duration) time.Time {
return c.PlantedAt.Add(c.Type.HarvestTime(scale))
}
15 changes: 15 additions & 0 deletions farm/crop_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package farm

import (
"fmt"
"testing"
)

// This isn't a real test but is useful for checking crop details and balancing
func TestDisplayCropStats(t *testing.T) {
for cropType := range CropTypes {
profit := cropType.MarketPrice() - cropType.SeedCost()
ppd := float64(profit) / float64(cropType.HarvestTime(1))
fmt.Printf("%-12s: -%3dg / +%3dg = +%3d ppd: %.3f\n", cropType.Name(), cropType.SeedCost(), cropType.MarketPrice(), profit, ppd)
}
}
92 changes: 92 additions & 0 deletions farm/encode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package farm

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"time"
)

const encryptionKey = "F6FFAF11E0BC4B27ADDA7E4F4B91B4BA"

type encodeableFarm struct {
Name string
W, H int
TimeScale time.Duration
Field [][]*Crop
Money int
LastModified time.Time
}

func (f *Farm) Marshal() ([]byte, error) {
jsonData, err := json.Marshal(encodeableFarm{
f.name,
f.w, f.h,
f.timeScale,
f.field,
f.money,
time.Now(),
})
if err != nil {
return nil, fmt.Errorf("error marshaling json: %w", err)
}

encrypted, err := encrypt(jsonData)
if err != nil {
return nil, fmt.Errorf("error encrypting: %w", err)
}

return encrypted, nil
}

func createCipher() (cipher.AEAD, error) {
c, err := aes.NewCipher([]byte(encryptionKey))
if err != nil {
return nil, fmt.Errorf("error creating cipher: %w", err)
}

gcm, err := cipher.NewGCM(c)
if err != nil {
return nil, fmt.Errorf("error creating gcm: %w", err)
}

return gcm, nil
}

func decrypt(in []byte) ([]byte, error) {
gcm, err := createCipher()
if err != nil {
return nil, fmt.Errorf("error creating cipher: %w", err)
}

nonceSize := gcm.NonceSize()
if len(in) < nonceSize {
return nil, fmt.Errorf("wrong nonce size")
}

nonce, in := in[:nonceSize], in[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, in, nil)
if err != nil {
return nil, fmt.Errorf("error decrypting: %w", err)
}

return plaintext, nil
}

func encrypt(in []byte) ([]byte, error) {
gcm, err := createCipher()
if err != nil {
return nil, fmt.Errorf("error creating cipher: %w", err)
}

nonce := make([]byte, gcm.NonceSize())
_, err = io.ReadFull(rand.Reader, nonce)
if err != nil {
return nil, fmt.Errorf("error reading nonce: %w", err)
}

return gcm.Seal(nonce, nonce, in, nil), nil
}
Loading

0 comments on commit 57c4413

Please sign in to comment.