-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
143 lines (124 loc) · 3.33 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package main
import (
"fmt"
"github.com/notnil/chess"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
"log"
"os"
"regexp"
"strconv"
"strings"
)
const DefaultRating = 1200 // Default rating assigned to unrated players for the purpose of applying rating filters
type Config struct {
Positions []Position
}
type Position struct {
FEN string
Filter Filter
}
type Rating struct {
One int
White int
Black int
Average int
}
type Filter struct {
Rating
}
// Simplify any given position in Forsyth–Edwards Notation by stripping the half move and full move numbers
func simplify(fen string) string {
rgx := regexp.MustCompile(`(?i)(^[rnbqk1-8]{1,8}\/[rnbqkp1-8]{1,8}\/[rnbqkp1-8]{1,8}\/[rnbqkp1-8]{1,8}\/[rnbqkp1-8]{1,8}\/[rnbqkp1-8]{1,8}\/[rnbqkp1-8]{1,8}\/[rnbqk1-8]{1,8}\s[wb]{1}\s[kq-]{1,4}\s[a-h1-8-]{1,2})\s\d+\s\d+$`)
if rgx.MatchString(fen) == false {
panic("Invalid FEN")
}
return rgx.FindStringSubmatch(fen)[1]
}
func main() {
var (
db string
conf string
)
app := &cli.App{
Name: "tabiya",
Usage: "Advanced position search utility for PGN chess databases",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pgn",
Aliases: []string{"p"},
Usage: "Path to PGN database to be scanned",
Required: true,
Destination: &db,
},
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Usage: "Path to file that contains list of positions in FEN notation",
Required: true,
Destination: &conf,
},
},
Action: func(*cli.Context) error {
// Read the given PGN database file
f, err := os.Open(db)
if err != nil {
return cli.Exit("Could not read PGN database", 1)
}
defer f.Close()
// Read the given configuration file
y, err := os.ReadFile(conf)
if err != nil {
return cli.Exit("Could not read positions file", 1)
}
var c Config
err = yaml.Unmarshal(y, &c)
if err != nil {
return cli.Exit("Could not parse positions file", 1)
}
scanner := chess.NewScanner(f)
outer:
for scanner.Scan() {
game := scanner.Next()
for _, position := range game.Positions() {
sfen := simplify(position.String())
// Search for the positions irrespective of move number
for _, p := range c.Positions {
if strings.HasPrefix(p.FEN, sfen) {
// Apply rating filters
if p.Filter.Rating != (Rating{}) {
white, black := DefaultRating, DefaultRating
if game.GetTagPair("WhiteElo") != nil {
white, _ = strconv.Atoi(game.GetTagPair("WhiteElo").Value)
}
if game.GetTagPair("BlackElo") != nil {
black, _ = strconv.Atoi(game.GetTagPair("BlackElo").Value)
}
if p.Filter.Rating.Average > 0 && (white+black)/2 < p.Filter.Rating.Average {
continue
}
if p.Filter.Rating.One > 0 && white < p.Filter.Rating.One && black < p.Filter.Rating.One {
continue
}
if p.Filter.Rating.White > 0 && white < p.Filter.Rating.White {
continue
}
if p.Filter.Rating.Black > 0 && black < p.Filter.Rating.Black {
continue
}
}
// Output PGN of matched game
fmt.Println(game, "\n")
// Skip already matches games
continue outer
}
}
}
}
return nil
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}