A lightweight Go package for simulating large numbers of concurrent "players" that perform weighted, state-dependent actions over discrete turns.
Useful for generating realistic event streams (e.g. for load testing, observability pipelines, or product analytics) without a real backend.
- Configurable number of players and initial state distributions
- Weighted random action selection + per-action conditions
- Mutable player state (
map[string]any) - Emits structured
Events (turn, action name, player ID, attributes) - Supports single-turn, multi-turn, and continuous simulation
- Optional OTLP receiver endpoint for telemetry export
import "github.com/jimtang2/simulator"go mod tidyclassDiagram
class Simulator {
-Config cfg
-[]Player players
-int64 turn
+NewSimulator(configPath) Simulator
+NextTurn(ctx, out) error
+NextNTurn(n, out) error
+Continuous(ctx, out, errCh)
+Now() int64
}
class Config {
+int ActionPerTurn
+int TimeBetweenTurn
+string OTLPReceiverEndpoint
+map[string]ActionConfig Actions
+PlayersConfig Players
}
class Player {
-int id
-map[string]any state
+State(key) any
+StateBool(key) bool
+StateString(key) string
+StateInt(key) int
+SetState(key, value)
}
class Action {
<<interface>>
+Name() string
+Condition(Player, Simulator) bool
+CreateEvent(Player, Simulator) (Event, error)
}
class Event {
+int64 Turn
+string ActionName
+int PlayerID
+map[string]any Attributes
}
class ActionConfig {
+int Weight
}
class PlayersConfig {
+int Count
+[]StateConfig InitStates
}
Simulator *-- Config : owns
Simulator *-- Player : manages many
Config *-- ActionConfig : weights named actions
Config *-- PlayersConfig : player population setup
Simulator ..> Action : selects and executes
Action ..> Player : reads and mutates state
Action ..> Simulator : evaluates context
Simulator ..> Event : emits after success
Event ..> Action : records name
Event ..> Player : records ID
sequenceDiagram
autonumber
participant C as Config
participant S as Simulator
participant P as Player
participant A as Action
participant E as Event
participant O as Output channel / OTLP pipeline
Note over C,S: NewSimulator loads Config and creates Players
C-->>S: ActionPerTurn, TimeBetweenTurn, action weights, initial states
S->>P: Initialize players from configured states
loop Each turn / action slot
S->>S: Select random Player and Action
S->>S: Apply configured Action weight
S->>A: Condition(P, S)
S->>A: Condition(P, S)
alt Condition is false
A-->>S: Skip action
else Condition is true
S->>A: CreateEvent(P, S)
A->>P: Read/update mutable state
P-->>A: Updated state
A-->>S: Event (with attributes)
S->>O: Emit Event
end
end
Built-in actions: onboarding, login, logout.
Additional actions can be registered with simulator.AddAction(...) or via blank imports from companion packages.
package main
import (
"context"
"fmt"
"time"
"github.com/jimtang2/simulator"
// blank-import extra actions if desired
// _ "github.com/jimtang2/simulator/actions/cex"
)
func main() {
s, err := simulator.NewSimulator("config.yaml")
if err != nil {
panic(err)
}
out := make(chan simulator.Event, 1024)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go func() {
for e := range out {
fmt.Println(e)
}
}()
// Run continuously until context is cancelled
errCh := make(chan error, 1)
go s.Continuous(ctx, out, errCh)
<-ctx.Done()
fmt.Println("simulation finished")
}action_per_turn: 100
time_between_turn: 50 # ms
players:
count: 1000
init_states:
- name: brand_new
weight: 30
state:
onboarded: false
logged_in: false
- name: active
weight: 70
state:
onboarded: true
logged_in: true
actions:
- name: onboarding
weight: 10
- name: login
weight: 30
- name: logout
weight: 20s, err := simulator.NewSimulator(cfgPath)
s.NextTurn(ctx, out) // one turn
s.NextNTurn(n, out) // n turns (with sleep)
s.Continuous(ctx, out, errCh) // runs until ctx cancelledMIT