Last active
December 31, 2025 14:41
-
-
Save rluders/6cb0b586c04968402b3ef75ba4ade868 to your computer and use it in GitHub Desktop.
A simple, idiomatic Go implementation of the Monty Hall problem with clean structure, input validation, and minimal state.
This file contains hidden or 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
| // NOTE: | |
| // Revision 1 contains a much simpler and more idiomatic version of this program. | |
| // Revision 2 intentionally over-engineers the solution using an event bus and channels. | |
| // This revision exists mostly as a joke and as an experiment in architecture, not as | |
| // a recommendation for how to write small Go programs. | |
| package main | |
| import ( | |
| "bufio" | |
| "context" | |
| "fmt" | |
| "math/rand" | |
| "os" | |
| "strconv" | |
| "strings" | |
| "sync" | |
| "time" | |
| ) | |
| const numDoors = 3 | |
| // Bus is a very small pub/sub event bus built on channels. | |
| // - Topics are strings | |
| // - Each subscriber gets its own channel | |
| // - Publish is non-blocking (slow subscribers drop events) | |
| type Bus struct { | |
| mu sync.RWMutex | |
| subs map[string][]chan any | |
| } | |
| func NewBus() *Bus { | |
| return &Bus{subs: make(map[string][]chan any)} | |
| } | |
| func (b *Bus) Subscribe(topic string, buffer int) <-chan any { | |
| ch := make(chan any, buffer) | |
| b.mu.Lock() | |
| b.subs[topic] = append(b.subs[topic], ch) | |
| b.mu.Unlock() | |
| return ch | |
| } | |
| func (b *Bus) Publish(topic string, ev any) { | |
| b.mu.RLock() | |
| targets := append([]chan any(nil), b.subs[topic]...) | |
| b.mu.RUnlock() | |
| for _, ch := range targets { | |
| select { | |
| case ch <- ev: | |
| default: | |
| // drop instead of blocking the publisher | |
| } | |
| } | |
| } | |
| const ( | |
| TopicCmd = "cmd" // user intent | |
| TopicEvt = "evt" // things that happened | |
| ) | |
| // Commands drive the engine (input side) | |
| type CmdStart struct{} | |
| type CmdPickDoor struct{ Door int } | |
| type CmdSwitchDecision struct{ Choice int } | |
| type CmdQuit struct{} | |
| // Events are emitted by the engine (output side) | |
| type EvtGameStarted struct{} | |
| type EvtHostOpened struct{ Door int } | |
| type EvtSwitched struct{ Door int } | |
| type EvtStayed struct{ Door int } | |
| type EvtWon struct{} | |
| type EvtLost struct{ Prize int } | |
| // Game holds only pure game logic. | |
| // No IO, no channels, no concurrency. | |
| type Game struct { | |
| rng *rand.Rand | |
| prize int | |
| } | |
| func NewGame() *Game { | |
| return &Game{ | |
| rng: rand.New(rand.NewSource(time.Now().UnixNano())), | |
| prize: 1 + rand.Intn(numDoors), | |
| } | |
| } | |
| func (g *Game) hostOpens(pick int) int { | |
| for { | |
| d := 1 + g.rng.Intn(numDoors) | |
| if d != pick && d != g.prize { | |
| return d | |
| } | |
| } | |
| } | |
| func otherClosedDoor(pick, opened int) int { | |
| for d := 1; d <= numDoors; d++ { | |
| if d != pick && d != opened { | |
| return d | |
| } | |
| } | |
| return -1 | |
| } | |
| func readIntInRange(r *bufio.Reader, prompt string, min, max int) int { | |
| for { | |
| fmt.Print(prompt) | |
| line, err := r.ReadString('\n') | |
| if err != nil { | |
| fmt.Println("\n Input error. Exiting.") | |
| os.Exit(1) | |
| } | |
| n, err := strconv.Atoi(strings.TrimSpace(line)) | |
| if err != nil || n < min || n > max { | |
| fmt.Printf("Please enter a number from %d to %d.\n", min, max) | |
| continue | |
| } | |
| return n | |
| } | |
| } | |
| // Engine is the single owner of all mutable game state. | |
| // It runs in exactly one goroutine and processes commands sequentially. | |
| type Engine struct { | |
| bus *Bus | |
| cmds <-chan any // command subscription (created before Run starts) | |
| game *Game | |
| pick int | |
| opened int | |
| stage string | |
| } | |
| func NewEngine(bus *Bus) *Engine { | |
| return &Engine{ | |
| bus: bus, | |
| cmds: bus.Subscribe(TopicCmd, 32), // IMPORTANT: subscribe synchronously | |
| stage: "idle", | |
| } | |
| } | |
| func (e *Engine) Run(ctx context.Context) { | |
| for { | |
| select { | |
| case <-ctx.Done(): | |
| return | |
| // Only this goroutine ever reads commands or mutates state | |
| case c := <-e.cmds: | |
| switch cmd := c.(type) { | |
| case CmdStart: | |
| e.game = NewGame() | |
| e.pick, e.opened = 0, 0 | |
| e.stage = "idle" | |
| e.bus.Publish(TopicEvt, EvtGameStarted{}) | |
| case CmdPickDoor: | |
| if e.game == nil || e.stage != "idle" { | |
| continue | |
| } | |
| if cmd.Door < 1 || cmd.Door > numDoors { | |
| continue | |
| } | |
| e.pick = cmd.Door | |
| e.opened = e.game.hostOpens(e.pick) | |
| e.stage = "opened" | |
| e.bus.Publish(TopicEvt, EvtHostOpened{Door: e.opened}) | |
| case CmdSwitchDecision: | |
| if e.game == nil || e.stage != "opened" { | |
| continue | |
| } | |
| finalPick := e.pick | |
| if cmd.Choice == 1 { | |
| finalPick = otherClosedDoor(e.pick, e.opened) | |
| e.bus.Publish(TopicEvt, EvtSwitched{Door: finalPick}) | |
| } else { | |
| e.bus.Publish(TopicEvt, EvtStayed{Door: finalPick}) | |
| } | |
| e.stage = "done" | |
| if finalPick == e.game.prize { | |
| e.bus.Publish(TopicEvt, EvtWon{}) | |
| } else { | |
| e.bus.Publish(TopicEvt, EvtLost{Prize: e.game.prize}) | |
| } | |
| case CmdQuit: | |
| return | |
| } | |
| } | |
| } | |
| } | |
| func main() { | |
| ctx, cancel := context.WithCancel(context.Background()) | |
| defer cancel() | |
| bus := NewBus() | |
| engine := NewEngine(bus) | |
| // Engine must be running before we publish CmdStart | |
| go engine.Run(ctx) | |
| events := bus.Subscribe(TopicEvt, 64) | |
| // Safe now: engine is already subscribed | |
| bus.Publish(TopicCmd, CmdStart{}) | |
| in := bufio.NewReader(os.Stdin) | |
| for { | |
| // Wait for game start event before prompting the user | |
| for { | |
| if _, ok := (<-events).(EvtGameStarted); ok { | |
| fmt.Println("Welcome to the Game of Doors!") | |
| fmt.Printf("Pick a door (1-%d):\n", numDoors) | |
| break | |
| } | |
| } | |
| pick := readIntInRange(in, "Your choice: ", 1, numDoors) | |
| bus.Publish(TopicCmd, CmdPickDoor{Door: pick}) | |
| // Wait until the engine tells us which door the host opened | |
| for { | |
| if e, ok := (<-events).(EvtHostOpened); ok { | |
| fmt.Printf("\n Host opens door %d: it's a goat.\n", e.Door) | |
| break | |
| } | |
| } | |
| switchChoice := readIntInRange(in, "Switch doors? (1=yes, 2=no): ", 1, 2) | |
| bus.Publish(TopicCmd, CmdSwitchDecision{Choice: switchChoice}) | |
| // Observe outcome events until the game ends | |
| for { | |
| switch e := (<-events).(type) { | |
| case EvtSwitched: | |
| fmt.Printf("You switched to door %d.\n", e.Door) | |
| case EvtStayed: | |
| fmt.Printf("You stayed with door %d.\n", e.Door) | |
| case EvtWon: | |
| fmt.Println("\n Congratulations, you won the car!") | |
| goto next | |
| case EvtLost: | |
| fmt.Printf("\n Sorry, you lost. The car was behind door %d.\n", e.Prize) | |
| goto next | |
| } | |
| } | |
| next: | |
| bus.Publish(TopicCmd, CmdStart{}) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment