|
|
|
@ -0,0 +1,478 @@
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"math/rand"
|
|
|
|
|
"os"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"git.rawtext.club/sloum/fanorona/qline"
|
|
|
|
|
"git.rawtext.club/sloum/fanorona/termios"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
Black int = iota
|
|
|
|
|
White
|
|
|
|
|
Empty
|
|
|
|
|
For rune = '╱'
|
|
|
|
|
Bak rune = '╲'
|
|
|
|
|
Ver rune = '│'
|
|
|
|
|
Hor string = "\033[38;5;234m─"
|
|
|
|
|
Wh string = "\033[38;5;255m█"
|
|
|
|
|
Bl string = "\033[38;5;234m█"
|
|
|
|
|
BoardBG string = "\033[48;5;215m"
|
|
|
|
|
ShadowBG string = "\033[48;5;95m"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var againstComputer bool = false
|
|
|
|
|
|
|
|
|
|
type pos struct {
|
|
|
|
|
strong bool // Strong if true, weak if not
|
|
|
|
|
state int // Valid: Black. Black, Empty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type move struct {
|
|
|
|
|
fromRow int
|
|
|
|
|
fromCol int
|
|
|
|
|
toRow int
|
|
|
|
|
toCol int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type dir struct {
|
|
|
|
|
rowOff int
|
|
|
|
|
colOff int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (d dir) Equal(other dir) bool {
|
|
|
|
|
if d.rowOff == other.rowOff && d.colOff == other.colOff {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Game struct {
|
|
|
|
|
board [5][9]pos
|
|
|
|
|
turn int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func MakeMove(s string) (move, error) {
|
|
|
|
|
s = strings.ToUpper(s)
|
|
|
|
|
o := move{}
|
|
|
|
|
n, err := fmt.Sscanf(s, "%1c%d-%1c%d", &o.fromRow, &o.fromCol, &o.toRow, &o.toCol)
|
|
|
|
|
if n < 2 || err != nil {
|
|
|
|
|
return o, fmt.Errorf("Invalid coordinate (%q)", s)
|
|
|
|
|
}
|
|
|
|
|
o.fromRow -= 65
|
|
|
|
|
o.toRow -= 65
|
|
|
|
|
o.fromCol -= 1
|
|
|
|
|
o.toCol -= 1
|
|
|
|
|
if o.fromRow > 4 || o.fromCol > 8 || o.fromRow < 0 || o.fromCol < 0 || o.toRow > 4 || o.toCol > 8 || o.toRow < 0 || o.toCol < 0 {
|
|
|
|
|
return o, fmt.Errorf("Invalid coordinate (%q)", s)
|
|
|
|
|
}
|
|
|
|
|
return o, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m move) Direction() dir {
|
|
|
|
|
return dir{m.toRow-m.fromRow, m.toCol-m.fromCol}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m move) String() (string, string) {
|
|
|
|
|
return fmt.Sprintf("%c%d", m.fromRow+65, m.fromCol+1), fmt.Sprintf("%c%d", m.toRow+65, m.toCol+1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *Game) ValidateMove(c move) error {
|
|
|
|
|
// Validate coordinates
|
|
|
|
|
if c.fromRow > 4 || c.fromCol > 8 || c.fromRow < 0 || c.fromCol < 0 || c.toRow > 4 || c.toCol > 8 || c.toRow < 0 || c.toCol < 0 {
|
|
|
|
|
return fmt.Errorf("Invalid coordinates")
|
|
|
|
|
}
|
|
|
|
|
// Validate the from position has a piece in it
|
|
|
|
|
if g.board[c.fromRow][c.fromCol].state == Empty {
|
|
|
|
|
from, _ := c.String()
|
|
|
|
|
return fmt.Errorf("There is no stone at %s", from)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate that the correct piece is being moved
|
|
|
|
|
if g.board[c.fromRow][c.fromCol].state != g.turn {
|
|
|
|
|
return fmt.Errorf("That is not your stone to move")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate that the move only moves one space away
|
|
|
|
|
if abs(c.fromRow-c.toRow) > 1 || abs(c.fromCol-c.toCol) > 1 {
|
|
|
|
|
return fmt.Errorf("You cannot move more than one space away")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate that the new space is not occupied
|
|
|
|
|
if g.board[c.toRow][c.toCol].state != Empty {
|
|
|
|
|
return fmt.Errorf("You cannot move to an occupied space")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate that a weak point does not try to perform a strong move
|
|
|
|
|
if (c.fromRow != c.toRow && c.fromCol != c.toCol) && !g.board[c.fromRow][c.fromCol].strong {
|
|
|
|
|
return fmt.Errorf("You cannot move diagonally from that space")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO check if the move is non-capturing. If so, see if there
|
|
|
|
|
// are available capturing moves that could be played. This will
|
|
|
|
|
// be semi-involved as it will require checking all other available
|
|
|
|
|
// pieces by the same player
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *Game) MoveAndDestroyStones(m move) (int, error) {
|
|
|
|
|
dead := g.RemoveDeadStones(m, false, false)
|
|
|
|
|
|
|
|
|
|
g.board[m.toRow][m.toCol].state = g.turn
|
|
|
|
|
g.board[m.fromRow][m.fromCol].state = Empty
|
|
|
|
|
return dead, nil
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g Game) withdrawAvailable(m move) bool {
|
|
|
|
|
d := m.Direction()
|
|
|
|
|
d.rowOff *= -1
|
|
|
|
|
d.colOff *= -1
|
|
|
|
|
nextRow, nextCol := m.fromRow+d.rowOff, m.fromCol+d.colOff
|
|
|
|
|
if nextRow < 0 || nextCol < 0 || nextRow > 4 || nextCol > 8 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if g.board[nextRow][nextCol].state == g.Enemy() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g Game) approachAvailable(m move) bool {
|
|
|
|
|
d := m.Direction()
|
|
|
|
|
nextRow, nextCol := m.toRow+d.rowOff, m.toCol+d.colOff
|
|
|
|
|
if nextRow < 0 || nextCol < 0 || nextRow > 4 || nextCol > 8 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if g.board[nextRow][nextCol].state == g.Enemy() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g Game) Enemy() int {
|
|
|
|
|
if g.turn == Black {
|
|
|
|
|
return White
|
|
|
|
|
}
|
|
|
|
|
return Black
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *Game) CountPotentialDead(m move, approach bool) int {
|
|
|
|
|
enemy := g.Enemy()
|
|
|
|
|
var count int
|
|
|
|
|
d := m.Direction()
|
|
|
|
|
var nextRow, nextCol int
|
|
|
|
|
if !approach {
|
|
|
|
|
nextRow, nextCol = m.fromRow, m.fromCol
|
|
|
|
|
d.rowOff *= -1
|
|
|
|
|
d.colOff *= -1
|
|
|
|
|
} else {
|
|
|
|
|
nextRow, nextCol = m.toRow, m.toCol
|
|
|
|
|
}
|
|
|
|
|
for {
|
|
|
|
|
nextRow += d.rowOff
|
|
|
|
|
nextCol += d.colOff
|
|
|
|
|
if nextRow < 0 || nextCol < 0 || nextRow > 4 || nextCol > 8 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if g.board[nextRow][nextCol].state == enemy {
|
|
|
|
|
count++
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *Game) RemoveDeadStones(m move, cpu bool, approach bool) int {
|
|
|
|
|
enemy := g.Enemy()
|
|
|
|
|
var count int
|
|
|
|
|
d := m.Direction()
|
|
|
|
|
var canWithdraw, canApproach bool
|
|
|
|
|
if !cpu {
|
|
|
|
|
canWithdraw = g.withdrawAvailable(m)
|
|
|
|
|
canApproach = g.approachAvailable(m)
|
|
|
|
|
if canWithdraw && canApproach {
|
|
|
|
|
request: for {
|
|
|
|
|
fmt.Println("(W)ithdraw or (A)pproach attack?")
|
|
|
|
|
k, _ := Getch()
|
|
|
|
|
switch k {
|
|
|
|
|
case 'w', 'W':
|
|
|
|
|
canApproach = false
|
|
|
|
|
case 'a', 'A':
|
|
|
|
|
canWithdraw = false
|
|
|
|
|
default:
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
break request
|
|
|
|
|
}
|
|
|
|
|
} else if !canWithdraw && !canApproach {
|
|
|
|
|
// This is a non-capturing move, validating this should have happened in
|
|
|
|
|
// `validateAndMovePiece`, so we accept it here without checking
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if approach {
|
|
|
|
|
canWithdraw = false
|
|
|
|
|
canApproach = true
|
|
|
|
|
} else {
|
|
|
|
|
canWithdraw = true
|
|
|
|
|
canApproach = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var nextRow, nextCol int
|
|
|
|
|
if canWithdraw {
|
|
|
|
|
nextRow, nextCol = m.fromRow, m.fromCol
|
|
|
|
|
d.rowOff *= -1
|
|
|
|
|
d.colOff *= -1
|
|
|
|
|
} else {
|
|
|
|
|
nextRow, nextCol = m.toRow, m.toCol
|
|
|
|
|
}
|
|
|
|
|
for {
|
|
|
|
|
nextRow += d.rowOff
|
|
|
|
|
nextCol += d.colOff
|
|
|
|
|
if nextRow < 0 || nextCol < 0 || nextRow > 4 || nextCol > 8 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if g.board[nextRow][nextCol].state == enemy {
|
|
|
|
|
count++
|
|
|
|
|
g.board[nextRow][nextCol].state = Empty
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return count
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g Game) GameOver() {
|
|
|
|
|
enemy := g.Enemy()
|
|
|
|
|
for _, row := range g.board {
|
|
|
|
|
for _, space := range row {
|
|
|
|
|
if space.state == enemy {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("GAME OVER! %s wins!\n", g.Player())
|
|
|
|
|
termios.Restore()
|
|
|
|
|
os.Exit(0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *Game) String() string {
|
|
|
|
|
var out strings.Builder
|
|
|
|
|
out.WriteString("\033[2J\033[0;0H")
|
|
|
|
|
var interline strings.Builder
|
|
|
|
|
bak := false
|
|
|
|
|
for i, _ := range g.board {
|
|
|
|
|
interline.Reset()
|
|
|
|
|
interline.WriteString("\033[38;5;234m")
|
|
|
|
|
if i == 0 {
|
|
|
|
|
out.WriteString(fmt.Sprintf(" \033[38;5;95m▟\033[0m%s%*.*s\033[0m\n", ShadowBG, 19, 19, ""))
|
|
|
|
|
out.WriteString(fmt.Sprintf(" %s%*.*s%s %s\033[0m\n", BoardBG, 19, 19, "", ShadowBG, BoardBG))
|
|
|
|
|
}
|
|
|
|
|
if i % 2 == 0 {
|
|
|
|
|
bak = true
|
|
|
|
|
} else {
|
|
|
|
|
bak = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for y, p := range g.board[i] {
|
|
|
|
|
if y == 0 {
|
|
|
|
|
out.WriteString(fmt.Sprintf("\033[1m%c\033[0m", rune(i+65)))
|
|
|
|
|
out.WriteString(fmt.Sprintf("%s ", BoardBG))
|
|
|
|
|
interline.WriteString(fmt.Sprintf(" %s ", BoardBG))
|
|
|
|
|
}
|
|
|
|
|
switch p.state {
|
|
|
|
|
case Black:
|
|
|
|
|
out.WriteString(Bl)
|
|
|
|
|
case White:
|
|
|
|
|
out.WriteString(Wh)
|
|
|
|
|
default:
|
|
|
|
|
out.WriteRune(' ')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interline.WriteRune(Ver)
|
|
|
|
|
|
|
|
|
|
if y < 8 {
|
|
|
|
|
out.WriteString(Hor)
|
|
|
|
|
if bak {
|
|
|
|
|
interline.WriteRune(Bak)
|
|
|
|
|
} else {
|
|
|
|
|
interline.WriteRune(For)
|
|
|
|
|
}
|
|
|
|
|
bak = !bak
|
|
|
|
|
} else {
|
|
|
|
|
out.WriteString(fmt.Sprintf(" %s %s\033[0m\n", ShadowBG, BoardBG))
|
|
|
|
|
interline.WriteString(fmt.Sprintf(" %s %s\033[0m\n", ShadowBG, BoardBG))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if i < 4 {
|
|
|
|
|
out.WriteString(interline.String())
|
|
|
|
|
} else {
|
|
|
|
|
out.WriteString(fmt.Sprintf(" %s%*.*s\033[0m\033[38;5;95m▛\033[0m\n", BoardBG, 19, 19, ""))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
out.WriteString("\033[0m\033[1m 1 2 3 4 5 6 7 8 9\033[0m\n\n")
|
|
|
|
|
return out.String()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Getch() (rune, error) {
|
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
|
return qline.ReadKey(reader)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g Game) Player() string {
|
|
|
|
|
if g.turn == Black {
|
|
|
|
|
return "Black"
|
|
|
|
|
}
|
|
|
|
|
return "White"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *Game) TakeTurn() {
|
|
|
|
|
var input string
|
|
|
|
|
var m move
|
|
|
|
|
var err error
|
|
|
|
|
firstMove := true
|
|
|
|
|
cont := ""
|
|
|
|
|
piece := ""
|
|
|
|
|
spaceList := make(map[string]bool)
|
|
|
|
|
var lastDir dir
|
|
|
|
|
for {
|
|
|
|
|
fmt.Print(g.String())
|
|
|
|
|
prompt := fmt.Sprintf("%s %s> ", g.Player(), cont)
|
|
|
|
|
input = qline.GetInput(prompt, piece, len(prompt)+10)
|
|
|
|
|
fmt.Print("\n")
|
|
|
|
|
if lowIn := strings.ToLower(input); strings.HasPrefix("pass", lowIn) || strings.HasPrefix("done", lowIn) {
|
|
|
|
|
if firstMove {
|
|
|
|
|
ShowError("Cannot pass before making any moves on your turn")
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
m, err = MakeMove(input)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ShowError(err.Error())
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
fromString, toString := m.String()
|
|
|
|
|
if !firstMove && fromString != piece {
|
|
|
|
|
ShowError("Cannot switch pieces mid-turn")
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, ok := spaceList[toString]; ok {
|
|
|
|
|
ShowError("Cannot revisit a space on the same turn")
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if !firstMove && lastDir.Equal(m.Direction()) {
|
|
|
|
|
ShowError("Cannot move twice in the same direction")
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
err := g.ValidateMove(m)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ShowError(err.Error())
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
dead, err := g.MoveAndDestroyStones(m)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ShowError(err.Error())
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if firstMove {
|
|
|
|
|
firstMove = false
|
|
|
|
|
spaceList[fromString] = true
|
|
|
|
|
cont = "(continuing) "
|
|
|
|
|
}
|
|
|
|
|
spaceList[toString] = true
|
|
|
|
|
piece = toString
|
|
|
|
|
lastDir = m.Direction()
|
|
|
|
|
if dead == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ShowError(e string) {
|
|
|
|
|
fmt.Printf("\033[1m\033[38;5;124m%s\033[0m\n...Press any key to continue...\n", e)
|
|
|
|
|
Getch()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (g *Game) Play() {
|
|
|
|
|
var c cpu
|
|
|
|
|
for {
|
|
|
|
|
if g.Player() == "Black" || !againstComputer {
|
|
|
|
|
g.TakeTurn()
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Print(g.String())
|
|
|
|
|
m, approach, attacking, continuation := c.PickMove(g)
|
|
|
|
|
if attacking {
|
|
|
|
|
g.RemoveDeadStones(m, true, approach)
|
|
|
|
|
}
|
|
|
|
|
g.board[m.toRow][m.toCol].state = g.turn
|
|
|
|
|
g.board[m.fromRow][m.fromCol].state = Empty
|
|
|
|
|
|
|
|
|
|
time.Sleep(time.Second * 1)
|
|
|
|
|
fmt.Print(g.String())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if continuation {
|
|
|
|
|
var cont move
|
|
|
|
|
var err error
|
|
|
|
|
cont = m
|
|
|
|
|
bl := make([]point, 2, 5)
|
|
|
|
|
bl[0] = point{m.fromRow, m.fromCol}
|
|
|
|
|
bl[1] = point{m.toRow, m.toCol}
|
|
|
|
|
for {
|
|
|
|
|
cont, approach, err = ScoreContinuation(cont, g, bl)
|
|
|
|
|
if err != nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
bl = append(bl, point{cont.toRow, cont.toCol})
|
|
|
|
|
g.RemoveDeadStones(cont, true, approach)
|
|
|
|
|
g.board[cont.toRow][cont.toCol].state = g.turn
|
|
|
|
|
g.board[cont.fromRow][cont.fromCol].state = Empty
|
|
|
|
|
|
|
|
|
|
time.Sleep(time.Second * 1)
|
|
|
|
|
fmt.Print(g.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
g.GameOver()
|
|
|
|
|
g.turn = abs(g.turn-1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func abs(n int) int {
|
|
|
|
|
if n < 0 {
|
|
|
|
|
n *= -1
|
|
|
|
|
}
|
|
|
|
|
return n
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var b = [5][9]pos {
|
|
|
|
|
{pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black},pos{true, Black}},
|
|
|
|
|
{pos{false, Black}, pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black}, pos{true, Black}, pos{false, Black}},
|
|
|
|
|
{pos{true, Black}, pos{false, White}, pos{true, Black}, pos{false, White}, pos{true, Empty}, pos{false, Black}, pos{true, White}, pos{false, Black},pos{true, White}},
|
|
|
|
|
{pos{false, White}, pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White}},
|
|
|
|
|
{pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White}, pos{true, White}, pos{false, White},pos{true, White}},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
termios.SetCharMode()
|
|
|
|
|
defer termios.Restore()
|
|
|
|
|
flag.BoolVar(&againstComputer, "cpu", false, "Play against a computer oponent")
|
|
|
|
|
flag.Parse()
|
|
|
|
|
rand.Seed(time.Now().UnixNano())
|
|
|
|
|
g := Game{b, Black}
|
|
|
|
|
g.Play()
|
|
|
|
|
}
|