You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

551 lines
13 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
var paikaMoveCounter int // Keeps track of 5 paika moves
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")
}
return nil
}
func (g *Game) MoveAndDestroyStones(m move) (int, error) {
dead := g.RemoveDeadStones(m, false, false)
UpdatePaikaCounter(dead)
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
}
// This is not working right and keeps saying that an attack
// is available when one is not
func (g Game) invalidPaika(m move) bool {
na := g.CountPotentialDead(m, false)
nw := g.CountPotentialDead(m, true)
if na + nw > 0 {
return false
}
var ap, wd bool
for r := range g.board {
for c := range g.board[r] {
if g.board[r][c].state != g.turn {
continue
}
for _, d := range weakDirs {
ap = g.approachAvailable(move{r, c, r+d.rowOff, c+d.colOff})
if ap {
return true
}
wd = g.withdrawAvailable(move{r, c, r+d.rowOff, c+d.colOff})
if wd {
return true
}
}
if g.board[r][c].strong {
for _, d := range strongDirs {
ap = g.approachAvailable(move{r, c, r+d.rowOff, c+d.colOff})
if ap {
return true
}
wd = g.withdrawAvailable(move{r, c, r+d.rowOff, c+d.colOff})
if wd {
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() {
winner := g.Player()
if paikaMoveCounter >= 5 {
var counter = make(map[int]int)
for _, row := range g.board {
for _, space := range row {
counter[space.state]++
}
}
if counter[Black] > counter[White] {
winner = "Black"
} else if counter[White] > counter[Black] {
winner = "White"
} else {
winner = "No one"
}
} else {
enemy := g.Enemy()
for _, row := range g.board {
for _, space := range row {
if space.state == enemy {
return
}
}
}
}
fmt.Printf("GAME OVER! %s wins!\n", winner)
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 {
// TODO make this doable, but have it forfeit?
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
}
// FIXME this does not work
// if g.invalidPaika(m) {
// ShowError("Must attack when an attack is available")
// 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 UpdatePaikaCounter(dead int) {
if dead == 0 {
paikaMoveCounter += 1
} else {
paikaMoveCounter = 0
}
}
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 {
UpdatePaikaCounter(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})
UpdatePaikaCounter(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()
}