// Package emotion implements the persona emotion state machine and affection // tracking. Emotion and affection are always keyed by persona id so that each // persona's internal state stays isolated — they follow the persona/character, // never a behavior mode (see DevDocs/design/01). package emotion import ( "fmt" "sync" ) // EmotionState is one state in the emotion state machine. type EmotionState string // Standard emotion states. More states can be added at any time by registering // transitions via AddTransition, so the machine is extensible rather than a // closed enum. const ( Calm EmotionState = "calm" Happy EmotionState = "happy" Excited EmotionState = "excited" Sad EmotionState = "sad" Angry EmotionState = "angry" ) // Params configures the state machine for one persona (情绪状态机参数). It is the // static, loadable description of how that persona's emotion may move. type Params struct { // InitialState is the emotion a freshly created machine starts in. When it // is empty the machine keeps it empty until Set is called explicitly. InitialState EmotionState `yaml:"initialState"` // DecayRate is the per-Decay step magnitude. A value greater than zero // enables the drift toward calm; zero or negative disables it. The actual // step is one position on the toward-calm ladder (see closerToCalm). DecayRate float64 `yaml:"decayRate"` // Transitions maps a source state to the set of states it may transition // to. When empty, DefaultTransitions is used. Transitions map[EmotionState][]EmotionState `yaml:"transitions"` } // Machine is a persona's live emotion state machine. All methods are safe for // concurrent use. type Machine struct { mu sync.RWMutex params Params current EmotionState transitions map[EmotionState]map[EmotionState]struct{} } // DefaultTransitions returns the built-in transition table used when a Params // instance carries no explicit transitions. func DefaultTransitions() map[EmotionState][]EmotionState { return map[EmotionState][]EmotionState{ Calm: {Happy, Sad}, Happy: {Excited, Calm, Sad}, Excited: {Happy, Calm, Angry}, Sad: {Calm, Happy, Angry}, Angry: {Calm, Excited}, } } // NewMachine builds a Machine from params. When params.Transitions is empty the // default transition table is installed. An empty InitialState is allowed, but // callers should normally Set it before use. func NewMachine(params Params) (*Machine, error) { if params.DecayRate < 0 { return nil, fmt.Errorf("emotion: negative decay rate %v", params.DecayRate) } base := params.Transitions if len(base) == 0 { base = DefaultTransitions() } return &Machine{ params: params, current: params.InitialState, transitions: normalizeTransitions(base), }, nil } // normalizeTransitions flattens the slice-based table into a membership set so // lookups are O(1) and symmetric. func normalizeTransitions(src map[EmotionState][]EmotionState) map[EmotionState]map[EmotionState]struct{} { out := make(map[EmotionState]map[EmotionState]struct{}, len(src)) for from, tos := range src { set := make(map[EmotionState]struct{}, len(tos)) for _, to := range tos { set[to] = struct{}{} } out[from] = set } return out } // Current returns the machine's present emotion state. func (m *Machine) Current() EmotionState { m.mu.RLock() defer m.mu.RUnlock() return m.current } // Params returns a copy of the machine's configuration. func (m *Machine) Params() Params { m.mu.RLock() defer m.mu.RUnlock() return m.params } // CanTransition reports whether the machine may move to to from its current // state. func (m *Machine) CanTransition(to EmotionState) bool { m.mu.RLock() defer m.mu.RUnlock() return m.allowed(m.current, to) } // allowed reports whether from may transition to to under the installed table. func (m *Machine) allowed(from, to EmotionState) bool { _, ok := m.transitions[from][to] return ok } // Transition moves the machine to to when the installed table permits it. It // returns an error when the move is not allowed. func (m *Machine) Transition(to EmotionState) error { m.mu.Lock() defer m.mu.Unlock() if !m.allowed(m.current, to) { return fmt.Errorf("emotion: cannot transition from %q to %q", m.current, to) } m.current = to return nil } // Set forces the machine to state regardless of the transition table. It is // used for initialization, restore, and resetting a persona's emotion. func (m *Machine) Set(state EmotionState) { m.mu.Lock() defer m.mu.Unlock() m.current = state } // AddTransition registers a new allowed edge, extending the table. func (m *Machine) AddTransition(from, to EmotionState) { m.mu.Lock() defer m.mu.Unlock() m.ensure(from) m.transitions[from][to] = struct{}{} } // ensure returns the membership set for from, creating it when absent. func (m *Machine) ensure(from EmotionState) map[EmotionState]struct{} { if m.transitions[from] == nil { m.transitions[from] = make(map[EmotionState]struct{}) } return m.transitions[from] } // Decay drifts the emotion one position toward calm whenever DecayRate is // positive; otherwise it leaves the current state unchanged. The new state is // returned. func (m *Machine) Decay() EmotionState { m.mu.Lock() defer m.mu.Unlock() if m.params.DecayRate <= 0 { return m.current } m.current = closerToCalm(m.current) return m.current } // closerToCalm returns the single relaxed step toward calm for a state. States // at or below calm are returned unchanged. func closerToCalm(s EmotionState) EmotionState { switch s { case Excited: return Happy case Happy, Sad, Angry: return Calm default: return s } }