Skip to content

Instantly share code, notes, and snippets.

@rluders
Last active December 31, 2025 14:41
Show Gist options
  • Select an option

  • Save rluders/6cb0b586c04968402b3ef75ba4ade868 to your computer and use it in GitHub Desktop.

Select an option

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.
// 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