package main
import (
"flag"
"fmt"
"github.com/Kilemonn/MarkovChainSimulator/config"
)
const (
OPTION_FILE_PATH = "f"
)
func main() {
var configFilePath string
flag.StringVar(&configFilePath, OPTION_FILE_PATH, "", "configuration file path")
flag.Parse()
if configFilePath == "" {
fmt.Printf("No file path provided with [-%s] argument option\n", OPTION_FILE_PATH)
return
}
chain, err := config.FromYaml(configFilePath)
if err != nil {
fmt.Printf("Failed to read in yaml file at path [%s]. Error: %s\n", configFilePath, err.Error())
return
}
err = chain.Validate()
if err != nil {
fmt.Printf("Configuration validation failed with error: %s\n", err.Error())
return
}
result := chain.Simulate()
for k, v := range result {
fmt.Printf("%s - %d/%d = %.2f%%\n", k, v, chain.StepCount, (float64(v)/float64(chain.StepCount))*100)
}
}
package config
import (
"fmt"
"math/rand/v2"
"os"
"gopkg.in/yaml.v3"
)
type MarkovChainConfig struct {
InitialState string `yaml:"initialState"`
StepCount uint `yaml:"stepCount"`
States []MarkovChainState
}
type MarkovChainState struct {
Id string
Transitions []MarkovChainTransition
}
type MarkovChainTransition struct {
State string
Chance float64
}
func (config MarkovChainConfig) Validate() error {
if len(config.States) == 0 {
return fmt.Errorf("no states provided in configuration")
}
if !config.hasState(config.InitialState) {
return fmt.Errorf("initial state [%s] does not exist in states list", config.InitialState)
}
for _, state := range config.States {
chanceTotal := 0.0
if len(state.Transitions) == 0 {
return fmt.Errorf("no transitions from state [%s] are defined", state.Id)
}
for _, transition := range state.Transitions {
if !config.hasState(transition.State) {
return fmt.Errorf("transition to state [%s] defined in transition from state [%s] does not exist",
transition.State, state.Id)
}
if transition.Chance <= 0 {
return fmt.Errorf("undefined or non-positive chance value [%f] set for transition to state [%s] from state [%s]",
transition.Chance, transition.State, state.Id)
}
chanceTotal += transition.Chance
}
if chanceTotal != 1.0 {
return fmt.Errorf("combined chance total does not equal 1.0 but adds up to [%f] for transitions defined for state [%s]",
chanceTotal, state.Id)
}
}
return nil
}
func (config MarkovChainConfig) hasState(stateId string) bool {
if len(config.States) == 0 {
return false
}
for _, state := range config.States {
if state.Id == stateId {
return true
}
}
return false
}
func (config MarkovChainConfig) Simulate() map[string]int {
state := config.getStateById(config.InitialState)
var count uint = 0
stateCount := make(map[string]int)
for count < config.StepCount {
if val, exists := stateCount[state.Id]; exists {
stateCount[state.Id] = val + 1
} else {
stateCount[state.Id] = 1
}
state = state.getNextState(config)
count += 1
}
return stateCount
}
func (config MarkovChainConfig) getStateById(stateId string) MarkovChainState {
for _, state := range config.States {
if state.Id == stateId {
return state
}
}
return MarkovChainState{}
}
func (state MarkovChainState) getNextState(config MarkovChainConfig) MarkovChainState {
rng := rand.Float64()
val := 0.0
lastStateName := ""
for _, transition := range state.Transitions {
val += transition.Chance
if rng <= val {
return config.getStateById(transition.State)
}
lastStateName = transition.State
}
return config.getStateById(lastStateName)
}
func FromYaml(configFilePath string) (config MarkovChainConfig, err error) {
data, err := os.ReadFile(configFilePath)
if err != nil {
fmt.Printf("Failed to read in file [%s]. Error: [%s].\n", configFilePath, err.Error())
return
}
err = yaml.Unmarshal(data, &config)
if err != nil {
fmt.Printf("Failed to unmarshal data from file [%s]. Error: [%s].\n", configFilePath, err.Error())
}
return
}