Skip to content

Commit 800724e

Browse files
committed
Add parsers for string representations of positions, and add a rudimentary console-based game
1 parent 6fb499c commit 800724e

File tree

7 files changed

+265
-17
lines changed

7 files changed

+265
-17
lines changed

.resources/doc_domain-model_chesseract-mermaid.svg

Lines changed: 1 addition & 1 deletion
Loading

chesseract/boring2D.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,26 @@ func (Boring2D) AllPositions() []Position {
7979
return rv
8080
}
8181

82+
// ParsePosition converts a string representation into a Position of the correct type
83+
func (Boring2D) ParsePosition(s string) (Position, error) {
84+
if len(s) != 2 {
85+
return invalidPosition{}, errInvalidFormat
86+
}
87+
88+
rv := position2D{}
89+
for i, r := range s {
90+
if i%2 == 0 {
91+
rv[i] = int(r - 'a')
92+
} else {
93+
rv[i] = int(r - '1')
94+
}
95+
if rv[i] < 0 || rv[i] >= 8 {
96+
return invalidPosition{}, errInvalidFormat
97+
}
98+
}
99+
return rv, nil
100+
}
101+
82102
// CanMove tests whether a piece can move to the specified new position on the board.
83103
// Note: this only tests movement rules; the check check is performed elsewhere.
84104
func (Boring2D) CanMove(board Board, piece Piece, pos Position) bool {
@@ -202,6 +222,25 @@ func normalise2d(dx, dy int) (vx, vy, r int) {
202222
}
203223

204224
// ApplyMove performs a move on the board, and returns the resulting board
205-
func (Boring2D) ApplyMove(Board, Move) (Board, error) {
206-
return Board{}, fmt.Errorf("not implemented")
225+
func (rs Boring2D) ApplyMove(board Board, move Move) (Board, error) {
226+
piece, ok := board.At(move.From)
227+
if !ok {
228+
return Board{}, errIllegalMove
229+
}
230+
231+
if !rs.CanMove(board, piece, move.To) {
232+
return Board{}, errIllegalMove
233+
}
234+
235+
newBoard := board.movePiece(move)
236+
237+
// TODO: check if this results in the player being in check
238+
239+
if newBoard.Turn == BLACK {
240+
newBoard.Turn = WHITE
241+
} else {
242+
newBoard.Turn = BLACK
243+
}
244+
245+
return newBoard, nil
207246
}

chesseract/boring2D_test.go

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package chesseract
22

33
import (
44
"bytes"
5+
"math/rand"
56
"testing"
7+
"time"
68
)
79

810
func TestEquality(t *testing.T) {
@@ -43,6 +45,35 @@ func logMatch(t *testing.T, m Match, highlight []Position) {
4345
t.Logf("\n%s", &b)
4446
}
4547

48+
func TestPositionParser(t *testing.T) {
49+
rs := Boring2D{}
50+
invalidValues := []string{
51+
"",
52+
"a1a1",
53+
"e9",
54+
" c",
55+
}
56+
for _, s := range invalidValues {
57+
p, err := rs.ParsePosition(s)
58+
if err == nil {
59+
t.Logf("String '%s' decodes into '%s' - not good", s, p)
60+
t.Fail()
61+
}
62+
}
63+
64+
// Test every valid value
65+
for _, p := range rs.AllPositions() {
66+
q, err := rs.ParsePosition(p.String())
67+
if err != nil {
68+
t.Logf("Error parsing position '%s': %v", p, err)
69+
t.Fail()
70+
} else if !p.Equals(q) {
71+
t.Logf("Position '%s' turns into '%s'", p, q)
72+
t.Fail()
73+
}
74+
}
75+
}
76+
4677
func TestBoring2DDefaultBoard(t *testing.T) {
4778
rs := Boring2D{}
4879
board := rs.DefaultBoard()
@@ -86,7 +117,9 @@ func TestBoring2DDefaultBoard(t *testing.T) {
86117

87118
func TestVeryInvalidMoves(t *testing.T) {
88119
rs := Boring2D{}
89-
board := Board{Pieces: []Piece{}}
120+
board := Board{Pieces: []Piece{
121+
{KING, WHITE, invalidPosition{}},
122+
}}
90123

91124
type testCase struct {
92125
PieceType PieceType
@@ -100,6 +133,8 @@ func TestVeryInvalidMoves(t *testing.T) {
100133
{19, position2D{3, 3}, position2D{5, 3}},
101134
{BISHOP, position2D{3, 3}, position4D{3, 3, 3, 3}},
102135
{BISHOP, position4D{3, 3, 3, 3}, position2D{3, 3}},
136+
{BISHOP, invalidPosition{}, position2D{3, 3}},
137+
{BISHOP, position2D{3, 3}, invalidPosition{}},
103138
}
104139

105140
for _, tc := range suite {
@@ -112,7 +147,7 @@ func TestVeryInvalidMoves(t *testing.T) {
112147
t.Logf("%s at %s shouldn't move to %s", tc.PieceType, tc.From, tc.To)
113148
t.Fail()
114149
} else {
115-
t.Logf("%s at %s cannot move to %s, which is as it should be", tc.PieceType, tc.From, tc.To)
150+
t.Logf("%s at %s (%s) cannot move to %s (%s), which is as it should be", tc.PieceType, tc.From, tc.From.CellColour(), tc.To, tc.To.CellColour())
116151
}
117152
}
118153
}
@@ -237,3 +272,67 @@ func TestMovementRules(t *testing.T) {
237272
}
238273
}
239274
}
275+
276+
func Test2DMatch(t *testing.T) {
277+
type moov struct {
278+
From, To string
279+
}
280+
moves := []moov{
281+
{"e2", "e4"}, {"c7", "c5"},
282+
{"g1", "f3"}, {"d7", "d6"},
283+
{"d2", "d4"}, {"c5", "d4"},
284+
{"f3", "d4"}, {"g8", "f6"},
285+
{"b1", "c3"}, {"a7", "a6"},
286+
{"c1", "e3"}, {"e7", "e6"},
287+
{"g2", "g4"}, {"e6", "e5"},
288+
{"d4", "f5"}, {"g7", "g6"},
289+
{"g4", "g5"}, {"g6", "f5"},
290+
{"e4", "f5"}, {"d6", "d5"},
291+
{"d1", "f3"}, {"d5", "d4"},
292+
}
293+
294+
rs := Boring2D{}
295+
match := Match{
296+
RuleSet: rs,
297+
Board: rs.DefaultBoard(),
298+
}
299+
for _, m := range moves {
300+
from, err := rs.ParsePosition(m.From)
301+
if err != nil {
302+
t.Logf("error parsing '%s': %v", m.From, err)
303+
t.Fail()
304+
break
305+
}
306+
piece, _ := match.Board.At(from)
307+
to, err := rs.ParsePosition(m.To)
308+
if err != nil {
309+
t.Logf("error parsing '%s': %v", m.To, err)
310+
t.Fail()
311+
break
312+
}
313+
314+
move := Move{piece.PieceType, from, to, time.Duration(int64(rand.Intn(90000))) * time.Millisecond}
315+
newBoard, err := rs.ApplyMove(match.Board, move)
316+
if err != nil {
317+
t.Logf("applying move '%s'-'%s': %v", m.From, m.To, err)
318+
t.Fail()
319+
break
320+
}
321+
322+
match.Moves = append(match.Moves, move)
323+
match.Board = newBoard
324+
}
325+
326+
logMatch(t, match, nil)
327+
328+
_, err := rs.ApplyMove(match.Board, Move{QUEEN, position2D{0, 3}, position2D{3, 3}, 0})
329+
if err == nil {
330+
t.Logf("This is not the Queen you were looking for")
331+
t.Fail()
332+
}
333+
_, err = rs.ApplyMove(match.Board, Move{QUEEN, position2D{5, 2}, position2D{3, 3}, 0})
334+
if err == nil {
335+
t.Logf("The Queen does not horse around")
336+
t.Fail()
337+
}
338+
}

chesseract/chesseract.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package chesseract
22

33
import (
4+
"errors"
45
"fmt"
56
"time"
67
)
78

89
var PackageVersion string
910

11+
var errInvalidFormat = errors.New("error parsing grid position")
12+
13+
var errIllegalMove = errors.New("illegal move")
14+
1015
// A Colour defines a chesspiece's colour
1116
type Colour int8
1217

@@ -24,6 +29,19 @@ type Position interface {
2429
CellColour() Colour
2530
}
2631

32+
// The invalidPosition type can be used in conjunction with throwing a parse error.
33+
type invalidPosition struct{}
34+
35+
func (invalidPosition) String() string {
36+
return "invalid position"
37+
}
38+
func (invalidPosition) Equals(Position) bool {
39+
return false
40+
}
41+
func (invalidPosition) CellColour() Colour {
42+
return BLACK
43+
}
44+
2745
// A Piece represents a chess piece on a board
2846
type Piece struct {
2947
// The PieceType represents the type of a chesspiece
@@ -55,6 +73,27 @@ func (b Board) At(pos Position) (Piece, bool) {
5573
return Piece{}, false
5674
}
5775

76+
// movePiece applies a move to a Board, and returns the resulting board.
77+
// The last piece to have moved is always at the end of the Pieces list
78+
func (b Board) movePiece(move Move) Board {
79+
rv := Board{
80+
Pieces: make([]Piece, 0, len(b.Pieces)),
81+
Turn: b.Turn,
82+
}
83+
oldPiece, ok := b.At(move.From)
84+
85+
for _, p := range b.Pieces {
86+
if !p.Position.Equals(move.From) && !p.Position.Equals(move.To) {
87+
rv.Pieces = append(rv.Pieces, p)
88+
}
89+
}
90+
if ok {
91+
oldPiece.Position = move.To
92+
rv.Pieces = append(rv.Pieces, oldPiece)
93+
}
94+
return rv
95+
}
96+
5897
// A Move wraps a single chess move
5998
type Move struct {
6099
// PieceType contains the chess piece type that's moving
@@ -70,6 +109,19 @@ type Move struct {
70109
Time time.Duration
71110
}
72111

112+
func (m Move) String() string {
113+
if m.Time == 0 {
114+
return fmt.Sprintf("%s %s %s", m.PieceType, m.From, m.To)
115+
}
116+
117+
t0 := m.Time.Truncate(100 * time.Millisecond)
118+
if m.Time.Seconds() > 45 {
119+
t0 = m.Time.Truncate(time.Second)
120+
}
121+
122+
return fmt.Sprintf("%s %s %s +%s", m.PieceType, m.From, m.To, t0)
123+
}
124+
73125
// The RuleSet captures the details in a chess variant
74126
type RuleSet interface {
75127
// DefaultBoard sets up the initial board configuration
@@ -78,6 +130,9 @@ type RuleSet interface {
78130
// AllPositions returns an iterator that allows one to range over all possible positions on the board in this variant
79131
AllPositions() []Position
80132

133+
// ParsePosition converts a string representation into a Position of the correct type
134+
ParsePosition(string) (Position, error)
135+
81136
// CanMove tests whether a piece can move to the specified new position on the board
82137
// Note: this only tests movement rules; the check check is performed in ApplyMove.
83138
CanMove(Board, Piece, Position) bool

chesseract/debug-dump.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@ import (
66
)
77

88
func (match Match) DebugDump(w io.Writer, highlight []Position) {
9-
if _, ok := match.RuleSet.(Boring2D); ok {
10-
match.dumpBoring2DBoard(w, highlight)
11-
} else {
12-
match.dumpUnknownBoard(w, highlight)
13-
}
14-
159
for i, m := range match.Moves {
1610
if i%2 == 0 {
1711
fmt.Fprintf(w, " %3d: %s\n", 1+i/2, m)
1812
} else {
1913
fmt.Fprintf(w, " %s\n", m)
2014
}
2115
}
16+
17+
if _, ok := match.RuleSet.(Boring2D); ok {
18+
match.dumpBoring2DBoard(w, highlight)
19+
} else {
20+
match.dumpUnknownBoard(w, highlight)
21+
}
2222
}
2323

2424
func (match Match) dumpCell(w io.Writer, p Position, highlight []Position) {

cmd/chesseract/chesseract.go

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"fmt"
55
"os"
6+
"time"
67

78
"github.com/thijzert/chesseract/chesseract"
89
)
@@ -20,10 +21,65 @@ func run() error {
2021

2122
rs := chesseract.Boring2D{}
2223
match := chesseract.Match{
23-
RuleSet: rs,
24-
Board: rs.DefaultBoard(),
24+
RuleSet: rs,
25+
Board: rs.DefaultBoard(),
26+
StartTime: time.Now(),
2527
}
26-
match.DebugDump(os.Stdout, nil)
2728

28-
return nil
29+
for {
30+
match.DebugDump(os.Stdout, nil)
31+
32+
var move chesseract.Move
33+
var newBoard chesseract.Board
34+
35+
for {
36+
fmt.Printf("Enter move for %6s: ", match.Board.Turn)
37+
38+
var sFrom, sTo string
39+
n, _ := fmt.Scanf("%s %s\n", &sFrom, &sTo)
40+
if n == 0 {
41+
continue
42+
}
43+
if n == 1 {
44+
if sFrom == "forfeit" || sFrom == "quit" {
45+
fmt.Printf("%s forfeits", match.Board.Turn)
46+
return nil
47+
}
48+
}
49+
50+
from, err := rs.ParsePosition(sFrom)
51+
if err != nil {
52+
fmt.Printf("error parsing '%s': %v\n", sFrom, err)
53+
continue
54+
}
55+
piece, _ := match.Board.At(from)
56+
to, err := rs.ParsePosition(sTo)
57+
if err != nil {
58+
fmt.Printf("error parsing '%s': %v\n", sTo, err)
59+
continue
60+
}
61+
62+
moveTime := time.Since(match.StartTime)
63+
for _, m := range match.Moves {
64+
moveTime -= m.Time
65+
}
66+
67+
move = chesseract.Move{
68+
PieceType: piece.PieceType,
69+
From: from,
70+
To: to,
71+
Time: moveTime,
72+
}
73+
newBoard, err = rs.ApplyMove(match.Board, move)
74+
if err != nil {
75+
fmt.Printf("applying move '%s'-'%s': %v\n", sFrom, sTo, err)
76+
continue
77+
}
78+
79+
break
80+
}
81+
82+
match.Moves = append(match.Moves, move)
83+
match.Board = newBoard
84+
}
2985
}

0 commit comments

Comments
 (0)