Skip to content

Commit 7978f93

Browse files
committed
play against random move engine
1 parent 78eae34 commit 7978f93

File tree

3 files changed

+163
-1
lines changed

3 files changed

+163
-1
lines changed

cmd/game/engine.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Package game provides a simple Go engine interface and a random-move engine.
2+
package game
3+
4+
import (
5+
"math/rand"
6+
"time"
7+
)
8+
9+
// Engine is an interface for Go engines.
10+
type Engine interface {
11+
// Move returns the next move as a Point, or nil if passing.
12+
Move(board Board, player FieldState, ko *Point) *Point
13+
}
14+
15+
// RandomEngine implements Engine by picking a random legal move.
16+
type RandomEngine struct{}
17+
18+
func (e *RandomEngine) Move(board Board, player FieldState, ko *Point) *Point {
19+
empty := []Point{}
20+
for i := 0; i < 9; i++ {
21+
for j := 0; j < 9; j++ {
22+
if board[i][j] == Empty {
23+
empty = append(empty, Point{Row: i, Col: j})
24+
}
25+
}
26+
}
27+
rand.Seed(time.Now().UnixNano())
28+
perm := rand.Perm(len(empty))
29+
for _, idx := range perm {
30+
pt := empty[idx]
31+
// Ko rule
32+
if ko != nil && pt.Row == ko.Row && pt.Col == ko.Col {
33+
continue
34+
}
35+
var nextBoard Board
36+
copy(nextBoard[:], board[:])
37+
nextBoard[pt.Row][pt.Col] = player
38+
opp := Black
39+
if player == Black {
40+
opp = White
41+
}
42+
captured := []Point{}
43+
for _, n := range Neighbors(pt) {
44+
if nextBoard[n.Row][n.Col] == opp {
45+
group, libs := Group(nextBoard, n)
46+
if len(libs) == 0 {
47+
for stonePt := range group {
48+
nextBoard[stonePt.Row][stonePt.Col] = Empty
49+
captured = append(captured, stonePt)
50+
}
51+
}
52+
}
53+
}
54+
_, libs := Group(nextBoard, pt)
55+
if len(libs) == 0 {
56+
continue
57+
}
58+
// Legal move found
59+
return &pt
60+
}
61+
// No legal move, pass
62+
return nil
63+
}

config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"quit": "q",
88
"save": "w",
99
"placeStone": "p",
10-
"passTurn": "x"
10+
"passTurn": "x",
11+
"enableEngine": "e"
1112
}
1213
}

main.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ var (
2525
koPoint *game.Point // Track Ko point (nil if no Ko)
2626
passCount int // Track consecutive passes
2727
gameOver bool // Track if the game is over
28+
engineEnabled bool // Play against engine if true
29+
engine game.Engine // The engine instance
2830
)
2931

3032
func loadConfig(path string) (Config, error) {
@@ -232,6 +234,17 @@ func placeStone(g *gocui.Gui, v *gocui.View) error {
232234
passCount = 0 // Reset pass count on a move
233235

234236
currentPlayer = 3 - currentPlayer // Switch player only after a legal move
237+
238+
// If engine is enabled and it's the engine's turn, make engine move
239+
if engineEnabled && !gameOver && currentPlayer == 2 {
240+
go func() {
241+
time.Sleep(300 * time.Millisecond)
242+
g.Update(func(g *gocui.Gui) error {
243+
engineMove(g)
244+
return nil
245+
})
246+
}()
247+
}
235248
return nil
236249
}
237250

@@ -287,14 +300,75 @@ func quit(g *gocui.Gui, v *gocui.View) error {
287300
return gocui.ErrQuit
288301
}
289302

303+
func engineMove(g *gocui.Gui) {
304+
// Use the engine interface to get a move for White
305+
if engine == nil {
306+
return
307+
}
308+
move := engine.Move(gui.Grid, game.White, koPoint)
309+
if move != nil {
310+
// Do not move the cursor for the engine, just place the stone directly
311+
row, col := move.Row, move.Col
312+
gui.Grid[row][col] = game.White
313+
314+
// Simulate captures and ko logic as in placeStone
315+
var nextBoard game.Board
316+
copy(nextBoard[:], gui.Grid[:])
317+
opp := game.Black
318+
captured := []game.Point{}
319+
for _, n := range game.Neighbors(game.Point{Row: row, Col: col}) {
320+
if nextBoard[n.Row][n.Col] == opp {
321+
group, libs := game.Group(nextBoard, n)
322+
if len(libs) == 0 {
323+
for stonePt := range group {
324+
nextBoard[stonePt.Row][stonePt.Col] = game.Empty
325+
captured = append(captured, stonePt)
326+
}
327+
}
328+
}
329+
}
330+
for _, pt := range captured {
331+
gui.Grid[pt.Row][pt.Col] = game.Empty
332+
}
333+
// Ko rule: set koPoint if exactly one stone was captured and the group size is 1
334+
if len(captured) == 1 {
335+
koPoint = &captured[0]
336+
} else {
337+
koPoint = nil
338+
}
339+
// Update prevBoard for next move (for Ko rule)
340+
if prevBoard == nil {
341+
prev := game.Board{}
342+
copy(prev[:], gui.Grid[:])
343+
prevBoard = &prev
344+
}
345+
copy(prevBoard[:], gui.Grid[:])
346+
passCount = 0
347+
currentPlayer = 1 // Switch back to player
348+
} else {
349+
_ = passTurn(g, nil)
350+
}
351+
}
352+
290353
func main() {
291354
g, err := gocui.NewGui(gocui.OutputNormal)
355+
if err != nil {
356+
log.Panicln(err)
357+
}
292358
cfg, err := loadConfig("config.json")
293359
if err != nil {
294360
log.Panicln("Failed to load config:", err)
295361
}
296362
keybindings = cfg.Keybindings
297363

364+
engine = &game.RandomEngine{}
365+
engineEnabled = true // Enable engine by default
366+
367+
defer g.Close()
368+
keybindings = cfg.Keybindings
369+
370+
engine = &game.RandomEngine{}
371+
298372
if err != nil {
299373
log.Panicln(err)
300374
}
@@ -359,8 +433,32 @@ func main() {
359433
if err := g.SetKeybinding("", quitKey, gocui.ModNone, quit); err != nil {
360434
log.Panicln(err)
361435
}
436+
// Engine toggle: enable for second player
437+
enableEngineKey := []rune(keybindings["enableEngine"])[0]
438+
if err := g.SetKeybinding("", enableEngineKey, gocui.ModNone, toggleEngine); err != nil {
439+
log.Panicln(err)
440+
}
362441

363442
if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
364443
log.Panicln(err)
365444
}
366445
}
446+
447+
func toggleEngine(g *gocui.Gui, v *gocui.View) error {
448+
engineEnabled = !engineEnabled
449+
if v, err := g.View("prompt"); err == nil && v != nil {
450+
v.Clear()
451+
printMovePrompt(v)
452+
}
453+
// If toggled on and it's engine's turn (player 2/White), make engine move
454+
if engineEnabled && !gameOver && currentPlayer == 2 {
455+
go func() {
456+
time.Sleep(300 * time.Millisecond)
457+
g.Update(func(g *gocui.Gui) error {
458+
engineMove(g)
459+
return nil
460+
})
461+
}()
462+
}
463+
return nil
464+
}

0 commit comments

Comments
 (0)