-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 57c4413
Showing
18 changed files
with
1,232 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
farm.data | ||
TODO.md | ||
vendor/ |
This file contains 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
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! |
This file contains 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
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)) | ||
} |
This file contains 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
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) | ||
} | ||
} |
This file contains 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
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 | ||
} |
Oops, something went wrong.