Task: Hexapawn




Go: package main

import (
	"bytes"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"math/rand"
	"os"
	"time"
)



const (
	Rows = 3
	Cols = 3
)

var vlog *log.Logger

func main() {
	verbose := flag.Bool("v", false, "verbose")
	flag.Parse()
	if flag.NArg() != 0 {
		flag.Usage()
		os.Exit(2)
	}
	logOutput := ioutil.Discard
	if *verbose {
		logOutput = os.Stderr
	}
	vlog = log.New(logOutput, "hexapawn: ", 0)

	rand.Seed(time.Now().UnixNano())
	wins := make(map[spot]int, 2)
	for {
		h := New()
		var s herGameState
		for c := false; h[stateIdx] == empty; c = !c {
			if c {
				h = s.Move(h)
			} else {
				h = h.HumanMove()
			}
		}
		fmt.Printf("Board:\n%v is a win for %v\n", h, h[stateIdx])
		s.Result(h[stateIdx])
		wins[h[stateIdx]]++
		fmt.Printf("Wins: Black=%d, White=%d\n", wins[black], wins[white])
		fmt.Println()
	}
}

func (h Hexapawn) HumanMove() Hexapawn {
	fmt.Print("Board:\n", h, "\n")
	var from, to int
	for {
		fmt.Print("Your move: ")
		_, err := fmt.Scanln(&from, &to)
		if err != nil {
			fmt.Println(err)
			if err == io.EOF {
				os.Exit(0) 
			}
			continue
		}
		if err := h.doMove(white, from-1, to-1); err != nil {
			fmt.Println(err)
			continue
		}
		return h
	}
}

var herNextMove = make(map[Hexapawn][]move)

type herGameState struct {
	
	h Hexapawn
	i int
}

func (s *herGameState) Move(h Hexapawn) Hexapawn {
	known := false
	moves := herNextMove[h]
	if moves == nil { 
		moves = possibleMoves(black, h)
		herNextMove[h] = moves
	} else if len(moves) == 0 {
		
		vlog.Println("no good moves left to black, picking a random looser")
		known = true
		moves = possibleMoves(black, h)
	}
	vlog.Println("considering", moves)
	i := rand.Intn(len(moves))
	if !known {
		s.h = h
		s.i = i
	}
	fmt.Println("Computer moves", moves[i])
	if err := h.doMove(black, moves[i].from, moves[i].to); err != nil {
		panic(err)
	}
	return h
}

func (s herGameState) Result(winner spot) {
	if winner == black {
		return 
	}
	
	moves := herNextMove[s.h]
	vlog.Printf("Training:\n%v will no longer do %v\n", s.h, moves[s.i])
	herNextMove[s.h] = append(moves[:s.i], moves[s.i+1:]...)
	vlog.Println("will instead do one of:", herNextMove[s.h])
}

type move struct{ from, to int }

func (m move) String() string { return fmt.Sprintf("%d→%d", m.from+1, m.to+1) }

var cachedMoves = []map[Hexapawn][]move{
	black: make(map[Hexapawn][]move),
	white: make(map[Hexapawn][]move),
}

func possibleMoves(s spot, h Hexapawn) []move {
	m := cachedMoves[s][h]
	if m != nil {
		return m
	}
	
	
	
	m = make([]move, 0)
	for from := 0; from < Rows*Cols; from++ {
		for to := 0; to < Rows*Cols; to++ {
			if err := h.checkMove(s, from, to); err == nil {
				m = append(m, move{from, to})
			}
		}
	}
	cachedMoves[s][h] = m
	vlog.Printf("caclulated possible moves for %v\n%v as %v\n", s, h, m)
	return m
}

func (h *Hexapawn) doMove(p spot, from, to int) error {
	if err := h.checkMove(p, from, to); err != nil {
		return err
	}
	h[from] = empty
	h[to] = p
	if (p == white && to/Rows == Rows-1) || (p == black && to/Rows == 0) {
		h[stateIdx] = p
	} else if len(possibleMoves(p.Other(), *h)) == 0 {
		h[stateIdx] = p
	}
	return nil
}

func (h *Hexapawn) checkMove(p spot, from, to int) error {
	if h[from] != p {
		return fmt.Errorf("No %v located at spot %v", p, from+1)
	}
	if h[to] == p {
		return fmt.Errorf("%v already occupies spot %v", p, to+1)
	}
	Δr := from/Rows - to/Rows
	if (p == white && Δr != -1) || (p == black && Δr != 1) {
		return errors.New("must move forward one row")
	}
	Δc := from%Rows - to%Rows
	capture := h[to] != empty
	if (capture || Δc != 0) && (!capture || (Δc != 1 && Δc != -1)) {
		return errors.New("ilegal move")
	}
	return nil
}

type Hexapawn [Rows*Cols + 1]spot

func New() Hexapawn {
	
	return Hexapawn{
		white, white, white,
		empty, empty, empty,
		black, black, black,
	}
}

func idx(r, c int) int { return r*Cols + c }


const stateIdx = Rows * Cols

func (h Hexapawn) String() string {
	var b bytes.Buffer
	for r := Rows - 1; r >= 0; r-- {
		for c := 0; c < Cols; c++ {
			b.WriteByte(h[idx(r, c)].Byte())
		}
		b.WriteByte('\n')
	}
	
	return string(b.Next(Rows*(Cols+1) - 1))
}

type spot uint8

const (
	empty spot = iota
	black
	white
)

func (s spot) String() string {
	switch s {
	case black:
		return "Black"
	case white:
		return "White"
	}
	panic(s)
}

func (s spot) Byte() byte {
	switch s {
	case empty:
		return '.'
	case black:
		return 'B'
	case white:
		return 'W'
	}
	panic(s)
}

func (s spot) Other() spot {
	if s == black {
		return white
	}
	return black
}




Python: 
import sys

black_pawn = " \u265f  "
white_pawn = " \u2659  "
empty_square = "    "


def draw_board(board_data):
    
    bg_black = "\u001b[48;5;237m"
    
    bg_white = "\u001b[48;5;245m"

    clear_to_eol = "\u001b[0m\u001b[K\n"

    board = ["1 ", bg_black, board_data[0][0], bg_white, board_data[0][1], bg_black, board_data[0][2], clear_to_eol,
             "2 ", bg_white, board_data[1][0], bg_black, board_data[1][1], bg_white, board_data[1][2], clear_to_eol,
             "3 ", bg_black, board_data[2][0], bg_white, board_data[2][1], bg_black, board_data[2][2], clear_to_eol,
             "   A   B   C\n"];

    sys.stdout.write("".join(board))

def get_movement_direction(colour):
    direction = -1
    if colour == black_pawn:
        direction = 1
    elif colour == white_pawn:
        direction = -1
    else:
        raise ValueError("Invalid piece colour")

    return direction

def get_other_colour(colour):
    if colour == black_pawn:
        return white_pawn
    elif colour == white_pawn:
        return black_pawn
    else:
        raise ValueError("Invalid piece colour")

def get_allowed_moves(board_data, row, col):
    if board_data[row][col] == empty_square:
        return set()

    colour = board_data[row][col]
    other_colour = get_other_colour(colour)
    direction = get_movement_direction(colour)

    if (row + direction < 0 or row + direction > 2):
        return set()

    allowed_moves = set()
    if board_data[row + direction][col] == empty_square:
        allowed_moves.add('f')
    if col > 0 and board_data[row + direction][col - 1] == other_colour:
        allowed_moves.add('dl')
    if col < 2 and board_data[row + direction][col + 1] == other_colour:
        allowed_moves.add('dr')

    return allowed_moves

def get_human_move(board_data, colour):
    
    direction = get_movement_direction(colour)

    while True:
        piece_posn = input(f'What {colour} do you want to move? ')
        valid_inputs = {'a1': (0,0), 'b1': (0,1), 'c1': (0,2),
                        'a2': (1,0), 'b2': (1,1), 'c2': (1,2),
                        'a3': (2,0), 'b3': (2,1), 'c3': (2,2)}
        if piece_posn not in valid_inputs:
            print("LOL that's not a valid position! Try again.")
            continue

        (row, col) = valid_inputs[piece_posn]
        piece = board_data[row][col]
        if piece == empty_square:
            print("What are you trying to pull, there's no piece in that space!")
            continue

        if piece != colour:
            print("LOL that's not your piece, try again!")
            continue

        allowed_moves = get_allowed_moves(board_data, row, col)

        if len(allowed_moves) == 0:
            print('LOL nice try. That piece has no valid moves.')
            continue

        move = list(allowed_moves)[0]
        if len(allowed_moves) > 1:
            move = input(f'What move do you want to make ({",".join(list(allowed_moves))})? ')
            if move not in allowed_moves:
                print('LOL that move is not allowed. Try again.')
                continue

        if move == 'f':
            board_data[row + direction][col] = board_data[row][col]
        elif move == 'dl':
            board_data[row + direction][col - 1] = board_data[row][col]
        elif move == 'dr':
            board_data[row + direction][col + 1] = board_data[row][col]

        board_data[row][col] = empty_square
        return board_data


def is_game_over(board_data):
    if board_data[0][0] == white_pawn or board_data[0][1] == white_pawn or board_data[0][2] == white_pawn:
        return white_pawn

    if board_data[2][0] == black_pawn or board_data[2][1] == black_pawn or board_data[2][2] == black_pawn:
        return black_pawn

    white_count = 0
    black_count = 0
    black_allowed_moves = []
    white_allowed_moves = []
    for i in range(3):
        for j in range(3):
            moves = get_allowed_moves(board_data, i, j)

            if board_data[i][j] == white_pawn:
                white_count += 1
                if len(moves) > 0:
                    white_allowed_moves.append((i,j,moves))
            if board_data[i][j] == black_pawn:
                black_count += 1
                if len(moves) > 0:
                    black_allowed_moves.append((i,j,moves))

    if white_count == 0 or len(white_allowed_moves) == 0:
        return black_pawn
    if black_count == 0 or len(black_allowed_moves) == 0:
        return white_pawn

    return "LOL NOPE"

def play_game(black_move, white_move):

    board_data = [[black_pawn, black_pawn, black_pawn],
                  [empty_square, empty_square, empty_square],
                  [white_pawn, white_pawn, white_pawn]]

    last_player = black_pawn
    next_player = white_pawn
    while is_game_over(board_data) == "LOL NOPE":
        draw_board(board_data)

        if (next_player == black_pawn):
            board_data = black_move(board_data, next_player)
        else:
            board_data = white_move(board_data, next_player)

        temp = last_player
        last_player = next_player
        next_player = temp

    winner = is_game_over(board_data)
    print(f'Congratulations {winner}!')

play_game(get_human_move, get_human_move)