Task: Mastermind




Go: package main

import (
	"errors"
	"flag"
	"fmt"
	"log"
	"math/rand"
	"strings"
	"time"
)

func main() {
	log.SetPrefix("mastermind: ")
	log.SetFlags(0)
	colours := flag.Int("colours", 6, "number of colours to use (2-20)")
	flag.IntVar(colours, "colors", 6, "alias for colours")
	holes := flag.Int("holes", 4, "number of holes (the code length, 4-10)")
	guesses := flag.Int("guesses", 12, "number of guesses allowed (7-20)")
	unique := flag.Bool("unique", false, "disallow duplicate colours in the code")
	flag.Parse()

	rand.Seed(time.Now().UnixNano())
	m, err := NewMastermind(*colours, *holes, *guesses, *unique)
	if err != nil {
		log.Fatal(err)
	}
	err = m.Play()
	if err != nil {
		log.Fatal(err)
	}
}

type mastermind struct {
	colours int
	holes   int
	guesses int
	unique  bool

	code   string
	past   []string 
	scores []string 
}

func NewMastermind(colours, holes, guesses int, unique bool) (*mastermind, error) {
	if colours < 2 || colours > 20 {
		return nil, errors.New("colours must be between 2 and 20 inclusive")
	}
	if holes < 4 || holes > 10 {
		return nil, errors.New("holes must be between 4 and 10 inclusive")
	}
	if guesses < 7 || guesses > 20 {
		return nil, errors.New("guesses must be between 7 and 20 inclusive")
	}
	if unique && holes > colours {
		return nil, errors.New("holes must be > colours when using unique")
	}

	return &mastermind{
		colours: colours,
		holes:   holes,
		guesses: guesses,
		unique:  unique,
		past:    make([]string, 0, guesses),
		scores:  make([]string, 0, guesses),
	}, nil
}

func (m *mastermind) Play() error {
	m.generateCode()
	fmt.Printf("A set of %s has been selected as the code.\n", m.describeCode(m.unique))
	fmt.Printf("You have %d guesses.\n", m.guesses)
	for len(m.past) < m.guesses {
		guess, err := m.inputGuess()
		if err != nil {
			return err
		}
		fmt.Println()
		m.past = append(m.past, guess)
		str, won := m.scoreString(m.score(guess))
		if won {
			plural := "es"
			if len(m.past) == 1 {
				plural = ""
			}
			fmt.Printf("You found the code in %d guess%s.\n", len(m.past), plural)
			return nil
		}
		m.scores = append(m.scores, str)
		m.printHistory()
		fmt.Println()
	}
	fmt.Printf("You are out of guesses. The code was %s.\n", m.code)
	return nil
}

const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
const blacks = "XXXXXXXXXX"
const whites = "OOOOOOOOOO"
const nones = "----------"

func (m *mastermind) describeCode(unique bool) string {
	ustr := ""
	if unique {
		ustr = " unique"
	}
	return fmt.Sprintf("%d%s letters (from 'A' to %q)",
		m.holes, ustr, charset[m.colours-1],
	)
}

func (m *mastermind) printHistory() {
	for i, g := range m.past {
		fmt.Printf("-----%s---%[1]s--\n", nones[:m.holes])
		fmt.Printf("%2d:  %s : %s\n", i+1, g, m.scores[i])
	}
}

func (m *mastermind) generateCode() {
	code := make([]byte, m.holes)
	if m.unique {
		p := rand.Perm(m.colours)
		for i := range code {
			code[i] = charset[p[i]]
		}
	} else {
		for i := range code {
			code[i] = charset[rand.Intn(m.colours)]
		}
	}
	m.code = string(code)
	
}

func (m *mastermind) inputGuess() (string, error) {
	var input string
	for {
		fmt.Printf("Enter guess #%d: ", len(m.past)+1)
		if _, err := fmt.Scanln(&input); err != nil {
			return "", err
		}
		input = strings.ToUpper(strings.TrimSpace(input))
		if m.validGuess(input) {
			return input, nil
		}
		fmt.Printf("A guess must consist of %s.\n", m.describeCode(false))
	}
}

func (m *mastermind) validGuess(input string) bool {
	if len(input) != m.holes {
		return false
	}
	for i := 0; i < len(input); i++ {
		c := input[i]
		if c < 'A' || c > charset[m.colours-1] {
			return false
		}
	}
	return true
}

func (m *mastermind) score(guess string) (black, white int) {
	scored := make([]bool, m.holes)
	for i := 0; i < len(guess); i++ {
		if guess[i] == m.code[i] {
			black++
			scored[i] = true
		}
	}
	for i := 0; i < len(guess); i++ {
		if guess[i] == m.code[i] {
			continue
		}
		for j := 0; j < len(m.code); j++ {
			if i != j && !scored[j] && guess[i] == m.code[j] {
				white++
				scored[j] = true
			}
		}
	}
	return
}

func (m *mastermind) scoreString(black, white int) (string, bool) {
	none := m.holes - black - white
	return blacks[:black] + whites[:white] + nones[:none], black == m.holes
}