Skip to content

Commit dd91591

Browse files
committed
robot, command: pet system
1 parent e9a2c1f commit dd91591

File tree

5 files changed

+311
-0
lines changed

5 files changed

+311
-0
lines changed

command/command.go

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/zephyrtronium/robot/channel"
99
"github.com/zephyrtronium/robot/message"
1010
"github.com/zephyrtronium/robot/metrics"
11+
"github.com/zephyrtronium/robot/pet"
1112
"github.com/zephyrtronium/robot/privacy"
1213
"github.com/zephyrtronium/robot/spoken"
1314
"github.com/zephyrtronium/robot/syncmap"
@@ -18,6 +19,7 @@ type Robot struct {
1819
Log *slog.Logger
1920
Channels *syncmap.Map[string, *channel.Channel]
2021
Brain brain.Interface
22+
Pet *pet.Status
2123
Privacy *privacy.List
2224
Spoken *spoken.History
2325
Owner string

command/tamagotchi.go

+270
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
package command
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"math/rand/v2"
7+
8+
"gitlab.com/zephyrtronium/pick"
9+
10+
"github.com/zephyrtronium/robot/message"
11+
"github.com/zephyrtronium/robot/pet"
12+
)
13+
14+
var hungerys = pick.New([]pick.Case[string]{
15+
{E: "I'm hungry", W: 20},
16+
{E: "hungery", W: 5},
17+
{E: "hungy", W: 5},
18+
{E: "tumy grumblin", W: 5},
19+
})
20+
21+
var cleanies = pick.New([]pick.Case[string]{
22+
{E: "need to clean the", W: 15},
23+
{E: "kinda messy in the", W: 15},
24+
{E: "lil stinky in the", W: 5},
25+
})
26+
27+
var socials = pick.New([]pick.Case[string]{
28+
{E: "need affection", W: 20},
29+
{E: "social meter looks like [=______]", W: 10},
30+
{E: "have I been a good pet?", W: 1},
31+
})
32+
33+
var happys = pick.New([]pick.Case[string]{
34+
{E: "All my needs are met!", W: 20},
35+
{E: "I'm a happy bot!", W: 20},
36+
{E: "Tummy filled, home cleaned, head patted!", W: 20},
37+
{E: "food ☑️ bedroom ☑️ kitchen ☑️ living room ☑️ bathroom ☑️ pats ☑️", W: 20},
38+
{E: "I'm a happy pet!", W: 3},
39+
{E: "Unbothered. Moisturized. Happy. In My Lane. Focused. Flourishing.", W: 3},
40+
})
41+
42+
func satmsg(sat pet.Satisfaction) (connective, state string) {
43+
switch false { // first time I've ever written this
44+
case sat.Fed:
45+
m := hungerys.Pick(rand.Uint32())
46+
return ", but", m + " 🥺👉👈 tell me to eat?"
47+
case sat.Bed:
48+
m := cleanies.Pick(rand.Uint32())
49+
return ", but", m + " bedroom 🥺👉👈 help me clean?"
50+
case sat.Kitche:
51+
m := cleanies.Pick(rand.Uint32())
52+
return ", but", m + " kitchen 🥺👉👈 help me clean?"
53+
case sat.Living:
54+
m := cleanies.Pick(rand.Uint32())
55+
return ", but", m + " living room 🥺👉👈 help me clean?"
56+
case sat.Bath:
57+
m := cleanies.Pick(rand.Uint32())
58+
return ", but", m + " bathroom 🥺👉👈 help me clean?"
59+
case sat.Pats:
60+
m := socials.Pick(rand.Uint32())
61+
return ", but", m + " 🥺👉👈 give pats?"
62+
default:
63+
m := happys.Pick(rand.Uint32())
64+
return ".", m
65+
}
66+
}
67+
68+
// Tamagotchi reports the bot's current pet status.
69+
// No arguments.
70+
func Tamagotchi(ctx context.Context, robo *Robot, call *Invocation) {
71+
if call.Message.Time().Before(call.Channel.SilentTime()) {
72+
robo.Log.InfoContext(ctx, "silent", slog.Time("until", call.Channel.SilentTime()))
73+
return
74+
}
75+
e := call.Channel.Emotes.Pick(rand.Uint32())
76+
sat := robo.Pet.Satisfaction(call.Message.Time())
77+
_, m := satmsg(sat)
78+
call.Channel.Message(ctx, message.Format("", "%s %s", m, e).AsReply(call.Message.ID))
79+
}
80+
81+
type dinner struct {
82+
name string
83+
sate int
84+
}
85+
86+
var dins = pick.New([]pick.Case[dinner]{
87+
{E: dinner{name: "🍔", sate: 90}, W: 10},
88+
{E: dinner{name: "🍕", sate: 80}, W: 10},
89+
{E: dinner{name: "🌭", sate: 60}, W: 10},
90+
{E: dinner{name: "🥞", sate: 60}, W: 10},
91+
{E: dinner{name: "🥖", sate: 60}, W: 10},
92+
{E: dinner{name: "🥗", sate: 90}, W: 8},
93+
{E: dinner{name: "🌯", sate: 80}, W: 10},
94+
{E: dinner{name: "🍙", sate: 40}, W: 5},
95+
{E: dinner{name: "🍛", sate: 100}, W: 5},
96+
{E: dinner{name: "🍝", sate: 80}, W: 10},
97+
{E: dinner{name: "🍺", sate: 1}, W: 2},
98+
{E: dinner{name: "🍪", sate: 5}, W: 2},
99+
{E: dinner{name: "🍆", sate: 0}, W: 1},
100+
{E: dinner{name: "🍑", sate: 0}, W: 1},
101+
})
102+
103+
var sides = pick.New([]pick.Case[dinner]{
104+
{E: dinner{name: "🍟", sate: 30}, W: 9},
105+
{E: dinner{name: "🥓", sate: 40}, W: 3},
106+
{E: dinner{name: "🥐", sate: 30}, W: 8},
107+
{E: dinner{name: "🧀", sate: 20}, W: 5},
108+
{E: dinner{name: "🍚", sate: 30}, W: 8},
109+
{E: dinner{name: "🍨", sate: 10}, W: 5},
110+
{E: dinner{name: "🍰", sate: 10}, W: 5},
111+
{E: dinner{name: "🍺", sate: 1}, W: 2},
112+
{E: dinner{name: "🍼", sate: 5}, W: 1},
113+
{E: dinner{name: "🍇", sate: 10}, W: 6},
114+
{E: dinner{name: "🍉", sate: 10}, W: 6},
115+
{E: dinner{name: "🍋", sate: 15}, W: 5},
116+
{E: dinner{name: "🌽", sate: 30}, W: 8},
117+
{E: dinner{name: "🥬", sate: 40}, W: 10},
118+
{E: dinner{name: "🥦", sate: 40}, W: 10},
119+
{E: dinner{name: "🥜", sate: 20}, W: 3},
120+
{E: dinner{name: "🌰🍆🌰", sate: 0}, W: 1},
121+
})
122+
123+
var chewmsgs = pick.New([]pick.Case[[2]string]{
124+
{E: [2]string{"I'll have", ""}, W: 5},
125+
{E: [2]string{"", "sounds tasty"}, W: 5},
126+
{E: [2]string{"", "mmmm"}, W: 5},
127+
{E: [2]string{"mmmm", ""}, W: 5},
128+
{E: [2]string{"gona chew some", "ya know what I mean"}, W: 5},
129+
{E: [2]string{"🤤", "👅👅🫦😳"}, W: 1},
130+
})
131+
132+
var fullmsgs = pick.New([]pick.Case[string]{
133+
{E: "I'm seriously full.", W: 20},
134+
{E: "I'm really not hungry right now.", W: 20},
135+
{E: "I've already eaten way too much…", W: 20},
136+
{E: "I've eaten so much tasty food already!", W: 20},
137+
{E: "Give me some time to digest first…", W: 20},
138+
{E: "please no do not make me eat any more my digital belly will literally explode please i do not have the same physiology as a human it is not safe please", W: 1},
139+
})
140+
141+
// Eat directs the pet to eat.
142+
// No arguments.
143+
func Eat(ctx context.Context, robo *Robot, call *Invocation) {
144+
if call.Message.Time().Before(call.Channel.SilentTime()) {
145+
robo.Log.InfoContext(ctx, "silent", slog.Time("until", call.Channel.SilentTime()))
146+
return
147+
}
148+
e := call.Channel.Emotes.Pick(rand.Uint32())
149+
150+
menu := []dinner{
151+
dins.Pick(rand.Uint32()),
152+
sides.Pick(rand.Uint32()),
153+
sides.Pick(rand.Uint32()),
154+
}
155+
sate := 0
156+
for _, v := range menu {
157+
sate += v.sate
158+
}
159+
ok, sat := robo.Pet.Feed(call.Message.Time(), sate)
160+
slog.InfoContext(ctx, "feed",
161+
slog.Bool("success", ok),
162+
slog.Any("menu", menu),
163+
)
164+
if !ok {
165+
s := fullmsgs.Pick(rand.Uint32())
166+
call.Channel.Message(ctx, message.Format("", "%s %s", s, e).AsReply(call.Message.ID))
167+
return
168+
}
169+
c, m := satmsg(sat)
170+
chew := chewmsgs.Pick(rand.Uint32())
171+
call.Channel.Message(ctx, message.Format("", "%s %s %s %s %s%s %s %s", chew[0], menu[0].name, menu[1].name, menu[2].name, chew[1], c, m, e).AsReply(call.Message.ID))
172+
}
173+
174+
var cleans = pick.New([]pick.Case[[2]string]{
175+
{E: [2]string{"Thank you for cleaning my", "!"}, W: 1},
176+
{E: [2]string{"Thanks for helping clean my", "!"}, W: 1},
177+
{E: [2]string{"My", " is clean now. Thank you so much!"}, W: 1},
178+
})
179+
180+
// Clean directs the pet to clean a room.
181+
// See /pet/pet.go for a description of the pet's apartment.
182+
// No arguments.
183+
func Clean(ctx context.Context, robo *Robot, call *Invocation) {
184+
if call.Message.Time().Before(call.Channel.SilentTime()) {
185+
robo.Log.InfoContext(ctx, "silent", slog.Time("until", call.Channel.SilentTime()))
186+
return
187+
}
188+
e := call.Channel.Emotes.Pick(rand.Uint32())
189+
190+
r, sat := robo.Pet.Clean(call.Message.Time())
191+
robo.Log.InfoContext(ctx, "clean",
192+
slog.String("room", r.String()),
193+
slog.Bool("bedroom", sat.Bed),
194+
slog.Bool("kitchen", sat.Kitche),
195+
slog.Bool("living", sat.Living),
196+
slog.Bool("bathroom", sat.Bath),
197+
)
198+
_, m := satmsg(sat)
199+
if r == pet.AllClean {
200+
call.Channel.Message(ctx, message.Format("", "Everything's already clean! %s %s", m, e).AsReply(call.Message.ID))
201+
return
202+
}
203+
clean := cleans.Pick(rand.Uint32())
204+
call.Channel.Message(ctx, message.Format("", "%s %s%s Now %s %s", clean[0], r, clean[1], m, e).AsReply(call.Message.ID))
205+
}
206+
207+
type pat struct {
208+
where string
209+
love int
210+
}
211+
212+
var petpats = pick.New([]pick.Case[pat]{
213+
{E: pat{where: "headpats pat pat", love: 30}, W: 1000},
214+
{E: pat{where: "headpats… are a critical hit! pat pat pat pta pat", love: 90}, W: 100},
215+
{E: pat{where: "You try to give headpats, but it was a glancing blow…", love: 1}, W: 100},
216+
217+
{E: pat{where: "chin scritches ehehe", love: 30}, W: 1000},
218+
{E: pat{where: "chin scritches… are a critical hit! purrr", love: 90}, W: 100},
219+
{E: pat{where: "You try to give chin scritches, but it was a glancing blow…", love: 1}, W: 100},
220+
221+
{E: pat{where: "lil cheek rub ehehe", love: 30}, W: 1000},
222+
{E: pat{where: "lil cheek rub… is a critical hit! hehehe cutie", love: 90}, W: 100},
223+
{E: pat{where: "You try to give a lil cheek rub, but it was a glancing blow…", love: 1}, W: 100},
224+
225+
{E: pat{where: "Thanks a ton for the shoulder rub! My shoulders are always stiff from generating memes all day.", love: 45}, W: 500},
226+
{E: pat{where: "んんんん~ That shoulder rub feels way too good, it must be a critical hit! ", love: 120}, W: 50},
227+
{E: pat{where: "This is… a shoulder rub? Glancing blow… Kinda hurt a bit…", love: 0}, W: 50},
228+
229+
{E: pat{where: "You give rubs on that spot on my lower back that feels really nice.", love: 60}, W: 300},
230+
{E: pat{where: "You give rubs on that spot on my lower back, and landed a critical hit! Don't mind me if I fall asleep…", love: 120}, W: 30},
231+
{E: pat{where: "You give rubs on that spot on my lower back, but it was a glancing blow… Maybe don't use your feet next time.", love: 0}, W: 30},
232+
233+
{E: pat{where: "Foot rub…? I-I'm not really into that kind of thing. It does feels nice, though.", love: 30}, W: 100},
234+
{E: pat{where: "Foot rub… is a critical hit! I swear, I'm really not into that!!", love: 120}, W: 10},
235+
{E: pat{where: "You give a foot rub, but it was a glancing blow… Are you rubbing your own feet??", love: 0}, W: 50},
236+
237+
{E: pat{where: "biiig hug 🩷", love: 120}, W: 100},
238+
{E: pat{where: "biiiiiiiig hug 🤍🩷🩵🤎🖤❤️🧡💛💚💙💜", love: 240}, W: 10},
239+
{E: pat{where: "You try to give a big hug, but it was a glancing blow… (Hugs are always nice, though.)", love: 15}, W: 10},
240+
241+
{E: pat{where: "Pats someplace weird… I appreciate the gesture, or something.", love: 0}, W: 50},
242+
{E: pat{where: "Pats someplace weird, but it feels really nice??", love: 90}, W: 5},
243+
})
244+
245+
// Pat pats the pet.
246+
// No arguments.
247+
func Pat(ctx context.Context, robo *Robot, call *Invocation) {
248+
if call.Message.Time().Before(call.Channel.SilentTime()) {
249+
robo.Log.InfoContext(ctx, "silent", slog.Time("until", call.Channel.SilentTime()))
250+
return
251+
}
252+
e := call.Channel.Emotes.Pick(rand.Uint32())
253+
254+
pat := petpats.Pick(rand.Uint32())
255+
// Pats from the pet's partner are more effective.
256+
// Is it weird to mix the pet functionality with the marriage system?
257+
l, _ := call.Channel.Extra.Load(partnerKey{})
258+
cur, _ := l.(*partner)
259+
if cur != nil && cur.who == call.Message.Sender.ID {
260+
pat.love += 30
261+
}
262+
robo.Log.InfoContext(ctx, "pat",
263+
slog.String("where", pat.where),
264+
slog.Int("love", pat.love),
265+
slog.Bool("partner", cur != nil && cur.who == call.Message.Sender.ID),
266+
)
267+
sat := robo.Pet.Pat(call.Message.Time(), pat.love)
268+
_, m := satmsg(sat)
269+
call.Channel.Message(ctx, message.Format("", "%s %s %s", pat.where, m, e).AsReply(call.Message.ID))
270+
}

pet/pet.go

+15
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@ const (
8181
Bathroom
8282
)
8383

84+
func (r Room) String() string {
85+
switch r {
86+
case Bedroom:
87+
return "bedroom"
88+
case Kitchen:
89+
return "kitchen"
90+
case Living:
91+
return "living room"
92+
case Bathroom:
93+
return "bathroom"
94+
default:
95+
return ""
96+
}
97+
}
98+
8499
// Clean cleans one of the pet's rooms, if any need to be cleaned.
85100
//
86101
// The first return value is the cleaned [Room], or [AllClean] if all were

privmsg.go

+21
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ func (robo *Robot) command(ctx context.Context, log *slog.Logger, ch *channel.Ch
184184
Log: log.With(slog.String("command", c.name), slog.Any("args", args)),
185185
Channels: robo.channels,
186186
Brain: robo.brain,
187+
Pet: &robo.pet,
187188
Privacy: robo.privacy,
188189
Spoken: robo.spoken,
189190
Owner: robo.owner,
@@ -434,6 +435,26 @@ var twitchAny = []twitchCommand{
434435
fn: command.Contact,
435436
name: "contact",
436437
},
438+
{
439+
parse: regexp.MustCompile(`^(?i:(?:check)?\s*(?:current)?\s*status$)`),
440+
fn: command.Tamagotchi,
441+
name: "tamagotchi",
442+
},
443+
{
444+
parse: regexp.MustCompile(`^(?i:eat|(?:have|wh?at(?:'|\s*i)?s?)\s*(?:s[ou]me?|fo?r|4)?\s*(?:brea?kfa?st|lu?nch|din*e*r))`),
445+
fn: command.Eat,
446+
name: "eat",
447+
},
448+
{
449+
parse: regexp.MustCompile(`^(?i:(?:let(?:'|\s*u)s|go)?\s*clean)`),
450+
fn: command.Clean,
451+
name: "clean",
452+
},
453+
{
454+
parse: regexp.MustCompile(`^(?i:\**(?:head\s*)?p[ae]t|(?:chin\s*)scritch|(?:cheek|shoulder|back|foot)?\s*rub|(?:bi+g\s+)hug|go+d\s+(?:girl|gril|boy|bot|pet|wife|waifu|h[ua]su?bando?|partner|spouse|daddy|mommy))`),
455+
fn: command.Pat,
456+
name: "pat",
457+
},
437458
{
438459
parse: regexp.MustCompile(`^(?i:say|generate)\s*(?i:something)?\s*(?i:starting)?\s*(?i:with)?\s+(?<prompt>.*)`),
439460
fn: command.Speak,

robot.go

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/zephyrtronium/robot/brain"
1919
"github.com/zephyrtronium/robot/channel"
2020
"github.com/zephyrtronium/robot/metrics"
21+
"github.com/zephyrtronium/robot/pet"
2122
"github.com/zephyrtronium/robot/privacy"
2223
"github.com/zephyrtronium/robot/spoken"
2324
"github.com/zephyrtronium/robot/syncmap"
@@ -48,6 +49,8 @@ type Robot struct {
4849
tmi *client[*tmi.Message, *tmi.Message]
4950
// twitch is the Twitch API client.
5051
twitch twitch.Client
52+
// pet is the robot's pet status.
53+
pet pet.Status
5154
// metrics are a collection of custom domain specific metrics.
5255
metrics *metrics.Metrics
5356
}

0 commit comments

Comments
 (0)