├── CODEOWNERS ├── go.mod ├── assets └── logo.png ├── .gitignore ├── pkg ├── board │ ├── zobrist │ │ └── zobrist.go │ ├── fen_test.go │ ├── bitboard │ │ ├── hyperbola.go │ │ ├── bitboard.go │ │ └── useful.go │ ├── perft.go │ ├── square │ │ ├── file.go │ │ ├── rank.go │ │ ├── diagonals.go │ │ └── square.go │ ├── piece │ │ ├── color.go │ │ └── piece.go │ ├── move │ │ ├── variation.go │ │ ├── castling │ │ │ ├── rooks.go │ │ │ └── castling.go │ │ ├── ordered.go │ │ ├── attacks │ │ │ ├── attack.go │ │ │ └── magic │ │ │ │ └── magic.go │ │ └── move.go │ ├── mailbox │ │ └── mailbox.go │ ├── fen.go │ └── move.go ├── search │ ├── evaluation.go │ ├── eval │ │ ├── classical │ │ │ ├── score.go │ │ │ └── phase.go │ │ ├── evaluation.go │ │ ├── move.go │ │ └── see.go │ ├── board.go │ ├── heuristics.go │ ├── deepning.go │ ├── stats.go │ ├── limits.go │ ├── aspiration.go │ ├── manager.go │ ├── quiescence.go │ ├── search.go │ └── tt │ │ └── table.go ├── formats │ └── fen │ │ └── fen.go └── uci │ ├── defaults.go │ ├── cmd │ └── command.go │ ├── uci.go │ └── flag │ └── flag.go ├── internal ├── util │ ├── ternary.go │ ├── prng.go │ └── math.go ├── build │ └── info.go ├── engine │ ├── cmd │ │ ├── d.go │ │ ├── stop.go │ │ ├── ponderhit.go │ │ ├── uci.go │ │ ├── ucinewgame.go │ │ ├── setoption.go │ │ ├── position.go │ │ └── bench.go │ ├── options │ │ ├── threads.go │ │ ├── ponder.go │ │ └── hash.go │ ├── context │ │ └── context.go │ └── engine.go └── generator │ ├── generate.go │ ├── reductions │ ├── .gotemplate │ └── generate.go │ ├── classical │ ├── .gotemplate │ ├── generate.go │ └── tables.go │ ├── attack │ ├── sliding.go │ ├── generate.go │ ├── nonSliding.go │ └── .gotemplate │ ├── zobrist │ ├── generate.go │ └── .gotemplate │ └── bitboard │ ├── .gotemplate │ └── generate.go ├── scripts ├── util │ └── cmd.go ├── build │ └── main.go └── datagen │ └── main.go ├── .github ├── workflows │ └── ci.yml └── pull_request_template.md ├── main.go ├── makefile └── README.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @raklaptudirm 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module laptudirm.com/x/mess 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raklaptudirm/mess/HEAD/assets/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ##################### 2 | # Go Language Files # 3 | ##################### 4 | 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, 16 | # specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories 20 | vendor/ 21 | 22 | # Go workspace file 23 | go.work 24 | 25 | ############## 26 | # Chess Data # 27 | ############## 28 | 29 | # chess data files 30 | *.pgn 31 | *.epd 32 | 33 | # engine data files 34 | engine-data/ 35 | -------------------------------------------------------------------------------- /pkg/board/zobrist/zobrist.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package zobrist 15 | 16 | //go:generate go run laptudirm.com/x/mess/internal/generator/zobrist 17 | 18 | type Key uint64 19 | -------------------------------------------------------------------------------- /internal/util/ternary.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | // Ternary is an implementation of the useful ternary operator. 17 | func Ternary[T any](condition bool, onTrue, onFalse T) T { 18 | if condition { 19 | return onTrue 20 | } 21 | 22 | return onFalse 23 | } 24 | -------------------------------------------------------------------------------- /scripts/util/cmd.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strings" 7 | ) 8 | 9 | // RunNormal runs the given command with the standard input and output. 10 | func RunNormal(args ...string) error { 11 | cmd := exec.Command(args[0], args[1:]...) 12 | 13 | cmd.Stdin = os.Stdin 14 | cmd.Stdout = os.Stdout 15 | cmd.Stderr = os.Stderr 16 | 17 | return cmd.Run() 18 | } 19 | 20 | // RunQuiet runs the given command with output silenced. 21 | func RunQuiet(args ...string) error { 22 | cmd := exec.Command(args[0], args[1:]...) 23 | cmd.Stdin = os.Stdin 24 | 25 | return cmd.Run() 26 | } 27 | 28 | // RunWithOutput runs the given command and returns the output. 29 | func RunWithOutput(args ...string) (string, error) { 30 | cmd := exec.Command(args[0], args[1:]...) 31 | 32 | out, err := cmd.Output() // copy the stdout 33 | 34 | return strings.TrimSuffix(string(out), "\n"), err 35 | } 36 | -------------------------------------------------------------------------------- /internal/build/info.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Code generated by go generate; DO NOT EDIT THE CONTENTS OF THIS FILE 15 | // The source code for the generator can be found at generator/build 16 | 17 | package build 18 | 19 | // Current version of Mess. Usually overwritten by the build script. 20 | const Version = "v0.3.0" 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | # prevent duplication of tests with 5 | # `pull_request` event 6 | branches: 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | golang_lint_and_test: 12 | name: Code style and tests 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | # setup Go for testing 22 | - name: Setup Go 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: '^1.18' 26 | 27 | # check code formatting 28 | - name: Run Golang CI Lint 29 | uses: golangci/golangci-lint-action@v2 30 | with: 31 | version: latest 32 | args: -E gofmt 33 | 34 | # run tests 35 | - name: Run tests 36 | run: go test ./... 37 | 38 | - name: Benchmark Engine 39 | run: go run . bench 40 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Changes (v0.0.0) 2 | 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 4 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 5 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 6 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 7 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat 8 | cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id. 9 | 10 | ### [Short Time Control]() 11 | 12 | ``` 13 | ELO | 21.36 +- 10.42 (95%) 14 | SPRT | 10.0+0.10s Threads=1 Hash=16MB 15 | LLR | 2.96 (-2.94, 2.94) [0.00, 5.00] 16 | GAMES | N: 2736 W: 957 L: 789 D: 990 17 | ``` 18 | 19 | ### [Long Time Control]() 20 | 21 | ``` 22 | ELO | 14.01 +- 7.90 (95%) 23 | SPRT | 60.0+0.60s Threads=1 Hash=256MB 24 | LLR | 2.96 (-2.94, 2.94) [0.00, 5.00] 25 | GAMES | N: 4120 W: 1227 L: 1061 D: 1832 26 | ``` 27 | -------------------------------------------------------------------------------- /pkg/board/fen_test.go: -------------------------------------------------------------------------------- 1 | package board_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "laptudirm.com/x/mess/pkg/board" 7 | "laptudirm.com/x/mess/pkg/formats/fen" 8 | ) 9 | 10 | func TestFEN(t *testing.T) { 11 | tests := []string{ 12 | "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 13 | "rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", 14 | "r1bqk1nr/pppp1ppp/2n5/2b1p3/2B1P3/5N2/PPPP1PPP/RNBQ1RK1 b kq - 5 4", 15 | "rnbq1rk1/ppp1bppp/4pn2/3p2B1/2PP4/2N2N2/PP2PPPP/R2QKB1R w KQ - 6 6", 16 | "rnbqkbnr/ppp2ppp/8/2Ppp3/8/8/PP1PPPPP/RNBQKBNR w KQkq d6 0 3", 17 | "rnbqkbnr/pp1ppppp/8/8/2pPP3/5N2/PPP2PPP/RNBQKB1R b KQkq d3 0 3", 18 | "rn3rk1/pbp1qpp1/1p5p/3p4/3P4/3BPN2/PP3PPP/R2Q1RK1 b - - 3 12", 19 | } 20 | 21 | for n, test := range tests { 22 | t.Run(test, func(t *testing.T) { 23 | // go magic 24 | fen := fen.FromString(test) 25 | 26 | b := board.New(board.FEN(fen)) 27 | newFEN := b.FEN().String() 28 | if test != newFEN { 29 | t.Errorf("test %d: wrong fen\n%s\n%s\n", n, test, newFEN) 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/search/evaluation.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package search 15 | 16 | import "laptudirm.com/x/mess/pkg/search/eval" 17 | 18 | // score return the static evaluation of the current context's internal 19 | // board. Any changes to the evaluation function should be done here. 20 | func (search *Context) score() eval.Eval { 21 | return search.evaluator.Accumulate(search.board.SideToMove) 22 | } 23 | 24 | // draw returns a randomized draw score to prevent threefold-repetition 25 | // blindness while searching. 26 | func (search *Context) draw() eval.Eval { 27 | return eval.RandDraw(search.stats.Nodes) 28 | } 29 | -------------------------------------------------------------------------------- /internal/engine/cmd/d.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | "laptudirm.com/x/mess/internal/engine/context" 18 | "laptudirm.com/x/mess/pkg/uci/cmd" 19 | ) 20 | 21 | // Custom command d 22 | // 23 | // This command prints out the current position using ascii art, along with 24 | // it's fen string, and zobrist key. 25 | func NewD(engine *context.Engine) cmd.Command { 26 | return cmd.Command{ 27 | Name: "d", 28 | Run: func(interaction cmd.Interaction) error { 29 | // print the current board with ascii art 30 | interaction.Reply(engine.Search.String()) 31 | return nil 32 | }, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/engine/options/threads.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package options 15 | 16 | import ( 17 | "laptudirm.com/x/mess/internal/engine/context" 18 | "laptudirm.com/x/mess/pkg/uci/option" 19 | ) 20 | 21 | // UCI option Threads, type spin 22 | // 23 | // The number of threads the engine should use while searching. 24 | func NewThreads(engine *context.Engine) option.Option { 25 | return &option.Spin{ 26 | // multi-threading not implemented, so fix value at 1 27 | Default: 1, 28 | Min: 1, Max: 1, 29 | 30 | Storage: func(threads int) error { 31 | engine.Options.Threads = threads 32 | return nil 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/generator/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package generator implements utility function used by code generators. 15 | package generator 16 | 17 | import ( 18 | "os" 19 | "text/template" 20 | ) 21 | 22 | // Generate evaluates the given template string t with the data v and 23 | // writes it to a new generated file with the name .go in the cwd. 24 | func Generate(name, t string, v any) { 25 | f, err := os.Create(name + ".go") 26 | if err != nil { 27 | panic(err) 28 | } 29 | defer f.Close() 30 | 31 | if err := template.Must(template.New(name).Parse(t)).Execute(f, v); err != nil { 32 | panic(err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/generator/reductions/.gotemplate: -------------------------------------------------------------------------------- 1 | {{- /* 2 | This is a template file used for code generation with go generate. 3 | The notices given in the comment below only applies to the files 4 | generated with this template. This file can be freely edited when 5 | updating the code generator. 6 | */ -}} 7 | 8 | // Copyright © 2023 Rak Laptudirm 9 | // 10 | // Licensed under the Apache License, Version 2.0 (the "License"); 11 | // you may not use this file except in compliance with the License. 12 | // You may obtain a copy of the License at 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | 21 | // Code generated by go generate; DO NOT EDIT THE CONTENTS OF THIS FILE 22 | // The source code for the generator can be found at generator/reductions 23 | 24 | package search 25 | 26 | // search reductions for various heuristics 27 | var reductions = [MaxDepth + 1][256]int{ {{- range .Reductions }} 28 | { {{ range . }}{{ printf "%#v" . }}, {{ end }} },{{ end }} 29 | } 30 | -------------------------------------------------------------------------------- /pkg/formats/fen/fen.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package fen 15 | 16 | import ( 17 | "fmt" 18 | "strings" 19 | ) 20 | 21 | // FromString returns a FEN parsed from the given string. 22 | func FromString(fen string) String { 23 | return FromSlice(strings.Fields(fen)) 24 | } 25 | 26 | // FromSlice returns a FEN parsed from the given slice. 27 | func FromSlice(fen []string) String { 28 | if len(fen) == 4 { 29 | fen = append(fen, "0", "1") 30 | } 31 | return [6]string(fen) 32 | } 33 | 34 | // String represents a String position string. 35 | type String [6]string 36 | 37 | // String returns the string representation of the given fen string. 38 | func (fen String) String() string { 39 | fenString := fmt.Sprint([6]string(fen)) 40 | return fenString[1 : len(fenString)-1] 41 | } 42 | -------------------------------------------------------------------------------- /pkg/search/eval/classical/score.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package classical 15 | 16 | import "laptudirm.com/x/mess/pkg/search/eval" 17 | 18 | // S creates a new Score encapsulating the given mg and eg evaluations. 19 | func S(mg, eg eval.Eval) Score { 20 | return Score(uint64(eg)<<32) + Score(mg) 21 | } 22 | 23 | // Score encapsulates the PeSTO middle game and end game stores into a 24 | // single value of a single type. 25 | type Score int64 26 | 27 | // MG returns the given score's middle game evaluation. 28 | func (score Score) MG() eval.Eval { 29 | return eval.Eval(int32(uint32(uint64(score)))) 30 | } 31 | 32 | // EG return the given score's end game evaluation. 33 | func (score Score) EG() eval.Eval { 34 | return eval.Eval(int32(uint32(uint64(score+(1<<31)) >> 32))) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/board/bitboard/hyperbola.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package bitboard 15 | 16 | import ( 17 | "math/bits" 18 | 19 | "laptudirm.com/x/mess/pkg/board/square" 20 | ) 21 | 22 | // Hyperbola implements hyperbola quintessence given a from square, 23 | // occupancy, and occupancy mask on the given bitboard.Board. 24 | // https://www.chessprogramming.org/Hyperbola_Quintessence 25 | func Hyperbola(s square.Square, occ, mask Board) Board { 26 | r := Square(s) 27 | o := occ & mask // masked occupancy 28 | return ((o - 2*r) ^ reverse(reverse(o)-2*reverse(r))) & mask 29 | } 30 | 31 | // reverse is a simple function to reduce the verbosity of the code. 32 | // It is inlined by the go compiler during compilation into a binary. 33 | func reverse(b Board) Board { 34 | return Board(bits.Reverse64(uint64(b))) 35 | } 36 | -------------------------------------------------------------------------------- /internal/engine/options/ponder.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package options 15 | 16 | import ( 17 | "laptudirm.com/x/mess/internal/engine/context" 18 | "laptudirm.com/x/mess/pkg/uci/option" 19 | ) 20 | 21 | // UCI option Ponder, type check 22 | // 23 | // This means that the engine is able to ponder. The GUI will send this 24 | // whenever pondering is possible or not. 25 | // 26 | // Note: The engine should not start pondering on its own if this is 27 | // enabled, this option is only needed because the engine might change its 28 | // time management algorithm when pondering is allowed. 29 | func NewPonder(engine *context.Engine) option.Option { 30 | return &option.Check{ 31 | Default: false, 32 | Storage: func(ponder bool) error { 33 | engine.Options.Ponder = ponder 34 | return nil 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/generator/reductions/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | _ "embed" 18 | "math/bits" 19 | 20 | "laptudirm.com/x/mess/internal/generator" 21 | "laptudirm.com/x/mess/pkg/search" 22 | ) 23 | 24 | type reductionsStruct struct { 25 | Reductions [search.MaxDepth + 1][256]int 26 | } 27 | 28 | //go:embed .gotemplate 29 | var template string 30 | 31 | func main() { 32 | var reductions reductionsStruct 33 | 34 | log := func(n int) int { 35 | // fast log2 approximation 36 | return 63 - bits.LeadingZeros64(uint64(n)) 37 | } 38 | 39 | for depth := 1; depth <= search.MaxDepth; depth++ { 40 | for moves := 1; moves < 256; moves++ { 41 | reductions.Reductions[depth][moves] = 1 + log(depth)*log(moves)/2 42 | } 43 | } 44 | 45 | generator.Generate("reductions", template, reductions) 46 | } 47 | -------------------------------------------------------------------------------- /internal/engine/cmd/stop.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | "errors" 18 | 19 | "laptudirm.com/x/mess/internal/engine/context" 20 | "laptudirm.com/x/mess/pkg/uci/cmd" 21 | ) 22 | 23 | // UCI command stop 24 | // 25 | // Stop calculating as soon as possible. 26 | func NewStop(engine *context.Engine) cmd.Command { 27 | return cmd.Command{ 28 | Name: "stop", 29 | Run: func(interaction cmd.Interaction) error { 30 | // check if any search is ongoing 31 | if !engine.Searching { 32 | return errors.New("stop: no search ongoing") 33 | } 34 | 35 | for !engine.Search.InProgress() { 36 | // wait for search to start before stopping it 37 | // cause otherwise parallelization issues will occur 38 | } 39 | 40 | // stop the search 41 | engine.Search.Stop() 42 | return nil 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/board/perft.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package board 15 | 16 | import "fmt" 17 | 18 | func Perft(b *Board, depth int) int { 19 | if depth == 0 { 20 | return 1 21 | } 22 | 23 | var nodes int 24 | moves := b.GenerateMoves(false) 25 | 26 | for _, move := range moves { 27 | b.MakeMove(move) 28 | newNodes := perft(b, depth-1) 29 | fmt.Printf("%s: %d\n", move, newNodes) 30 | nodes += newNodes 31 | b.UnmakeMove() 32 | } 33 | 34 | return nodes 35 | } 36 | 37 | func perft(b *Board, depth int) int { 38 | 39 | switch depth { 40 | case 0: 41 | return 1 42 | case 1: 43 | return len(b.GenerateMoves(false)) 44 | default: 45 | var nodes int 46 | moves := b.GenerateMoves(false) 47 | 48 | for _, move := range moves { 49 | b.MakeMove(move) 50 | nodes += perft(b, depth-1) 51 | b.UnmakeMove() 52 | } 53 | 54 | return nodes 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/generator/classical/.gotemplate: -------------------------------------------------------------------------------- 1 | {{- /* 2 | This is a template file used for code generation with go generate. 3 | The notices given in the comment below only applies to the files 4 | generated with this template. This file can be freely edited when 5 | updating the code generator. 6 | */ -}} 7 | 8 | // Copyright © 2023 Rak Laptudirm 9 | // 10 | // Licensed under the Apache License, Version 2.0 (the "License"); 11 | // you may not use this file except in compliance with the License. 12 | // You may obtain a copy of the License at 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | 21 | // Code generated by go generate; DO NOT EDIT THE CONTENTS OF THIS FILE 22 | // The source code for the generator can be found at generator/classical 23 | 24 | package classical 25 | 26 | import ( 27 | "laptudirm.com/x/mess/pkg/board/piece" 28 | "laptudirm.com/x/mess/pkg/board/square" 29 | ) 30 | 31 | // PeSTO tables 32 | 33 | var stackedPawnPenalty = [7]Score{ {{ range .StackedPawn }}{{ printf "%0#16x" . }}, {{ end }} } 34 | 35 | var table = [piece.N][square.N]Score{ {{- range .PeSTO }} 36 | { {{ range . }}{{ printf "%0#16x" . }}, {{ end }} },{{ end }} 37 | } 38 | -------------------------------------------------------------------------------- /pkg/search/eval/classical/phase.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package classical 15 | 16 | import ( 17 | "laptudirm.com/x/mess/pkg/board/piece" 18 | "laptudirm.com/x/mess/pkg/search/eval" 19 | ) 20 | 21 | // game phase increment of each piece type 22 | const ( 23 | pawnPhaseInc eval.Eval = 0 24 | knightPhaseInc eval.Eval = 1 25 | bishopPhaseInc eval.Eval = 1 26 | rookPhaseInc eval.Eval = 2 27 | queenPhaseInc eval.Eval = 4 28 | ) 29 | 30 | // phaseInc maps each piece type to it's phase increment. 31 | var phaseInc = [piece.TypeN]eval.Eval{ 32 | piece.Pawn: pawnPhaseInc, 33 | piece.Knight: knightPhaseInc, 34 | piece.Bishop: bishopPhaseInc, 35 | piece.Rook: rookPhaseInc, 36 | piece.Queen: queenPhaseInc, 37 | } 38 | 39 | // startposPhase is the phase of the starting position. 40 | const startposPhase = 16*pawnPhaseInc + 41 | 4*knightPhaseInc + 4*bishopPhaseInc + 42 | 4*rookPhaseInc + 2*queenPhaseInc 43 | -------------------------------------------------------------------------------- /internal/engine/context/context.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package context 15 | 16 | import ( 17 | "laptudirm.com/x/mess/pkg/search" 18 | "laptudirm.com/x/mess/pkg/uci" 19 | "laptudirm.com/x/mess/pkg/uci/option" 20 | ) 21 | 22 | // Engine represents the context containing the engine's information which 23 | // is shared among it's UCI commands to store state. 24 | type Engine struct { 25 | // engine's uci client 26 | Client uci.Client 27 | 28 | // current search context 29 | Search *search.Context 30 | Searching bool 31 | 32 | Pondering bool 33 | PonderLimits search.Limits 34 | 35 | // uci options 36 | OptionSchema option.Schema 37 | Options options 38 | } 39 | 40 | // options contains the values of the UCI options supported by the engine. 41 | type options struct { 42 | Ponder bool // name Ponder type check 43 | Hash int // name Hash type spin 44 | Threads int // name Threads type spin 45 | } 46 | -------------------------------------------------------------------------------- /pkg/board/square/file.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package square 15 | 16 | // File represents a file on the chessboard. 17 | // Every vertical line of squares is called a file. 18 | // 19 | // a b c d e f g h 20 | // a b c d e f g h 21 | // a b c d e f g h 22 | // a b c d e f g h 23 | // a b c d e f g h 24 | // a b c d e f g h 25 | // a b c d e f g h 26 | // a b c d e f g h 27 | type File int8 28 | 29 | // constants representing various files 30 | const ( 31 | FileA File = iota 32 | FileB 33 | FileC 34 | FileD 35 | FileE 36 | FileF 37 | FileG 38 | FileH 39 | ) 40 | 41 | // FileN is the number of files. 42 | const FileN = 8 43 | 44 | // String converts a File into it's string representation. 45 | func (f File) String() string { 46 | const fileToStr = "abcdefgh" 47 | return string(fileToStr[f]) 48 | } 49 | 50 | // FileFrom creates an instance of a File from the given file id. 51 | func FileFrom(id string) File { 52 | return File(id[0] - 'a') 53 | } 54 | -------------------------------------------------------------------------------- /pkg/board/piece/color.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package piece 15 | 16 | // NewColor creates an instance of color from the given id. 17 | func NewColor(id string) Color { 18 | switch id { 19 | case "w": 20 | return White 21 | case "b": 22 | return Black 23 | default: 24 | panic("new color: invalid color id") 25 | } 26 | } 27 | 28 | // Color represents the color of a Piece. 29 | type Color int 30 | 31 | // constants representing various piece colors 32 | const ( 33 | White Color = iota 34 | Black 35 | ) 36 | 37 | // ColorN is the number of colors there are. 38 | const ColorN = 2 39 | 40 | // Other returns the color opposite to the given one. For White, 41 | // it returns Black and for Black, it returns White. 42 | func (c Color) Other() Color { 43 | return 1 ^ c 44 | } 45 | 46 | // String converts a Color to it's string representation. 47 | func (c Color) String() string { 48 | const colorToStr = "wb" 49 | return string(colorToStr[c]) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/board/square/rank.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package square 15 | 16 | // Rank represents a rank on the chessboard. 17 | // Every horizontal line of squares is called a rank. 18 | // 19 | // 8 8 8 8 8 8 8 8 20 | // 7 7 7 7 7 7 7 7 21 | // 6 6 6 6 6 6 6 6 22 | // 5 5 5 5 5 5 5 5 23 | // 4 4 4 4 4 4 4 4 24 | // 3 3 3 3 3 3 3 3 25 | // 2 2 2 2 2 2 2 2 26 | // 1 1 1 1 1 1 1 1 27 | type Rank int8 28 | 29 | // constants representing various ranks 30 | const ( 31 | Rank8 Rank = iota 32 | Rank7 33 | Rank6 34 | Rank5 35 | Rank4 36 | Rank3 37 | Rank2 38 | Rank1 39 | ) 40 | 41 | // RankN is the number of ranks. 42 | const RankN = 8 43 | 44 | // String converts a Rank into it's string representation. 45 | func (r Rank) String() string { 46 | const rankToStr = "87654321" 47 | return string(rankToStr[r]) 48 | } 49 | 50 | // RankFrom creates an instance of Rank from the given id. 51 | func RankFrom(id string) Rank { 52 | return Rank1 - Rank(id[0]-'1') 53 | } 54 | -------------------------------------------------------------------------------- /internal/engine/options/hash.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package options 15 | 16 | import ( 17 | "laptudirm.com/x/mess/internal/engine/context" 18 | "laptudirm.com/x/mess/pkg/uci/option" 19 | ) 20 | 21 | // UCI option Hash, type spin 22 | // 23 | // The value in MB allocated for hash tables. 24 | // This should be answered with the first "setoption" command at program 25 | // boot if the engine has sent the appropriate option name Hash command, 26 | // which should be supported by all engines! So the engine should use a 27 | // very small hash value as default. 28 | func NewHash(engine *context.Engine) option.Option { 29 | return &option.Spin{ 30 | Default: 16, // default from stockfish 31 | Min: 1, 32 | // use stockfish value to suppress cutechess warnings 33 | Max: 33554432, 34 | Storage: func(hash int) error { 35 | engine.Options.Hash = hash 36 | 37 | // resize hash table 38 | engine.Search.ResizeTT(hash) 39 | 40 | return nil 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/engine/cmd/ponderhit.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | "errors" 18 | 19 | "laptudirm.com/x/mess/internal/engine/context" 20 | "laptudirm.com/x/mess/pkg/uci/cmd" 21 | ) 22 | 23 | func NewPonderHit(engine *context.Engine) cmd.Command { 24 | return cmd.Command{ 25 | Name: "ponderhit", 26 | Run: func(interaction cmd.Interaction) error { 27 | // check if any ponder search is ongoing 28 | if !engine.Pondering { 29 | return errors.New("stop: no ponder search ongoing") 30 | } 31 | 32 | for !engine.Search.InProgress() { 33 | // wait for search to start before updating limits 34 | // cause otherwise parallelization issues will occur 35 | } 36 | 37 | // stop pondering but continue search with updated limits 38 | engine.Pondering = false // search is now normal 39 | // update to previously stored limits for normal search 40 | engine.Search.UpdateLimits(engine.PonderLimits) 41 | return nil 42 | }, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | 20 | "laptudirm.com/x/mess/internal/build" 21 | "laptudirm.com/x/mess/internal/engine" 22 | ) 23 | 24 | func main() { 25 | // run engine 26 | if err := run(); err != nil { 27 | // exit with error 28 | fmt.Fprintln(os.Stderr, err) 29 | os.Exit(1) 30 | } 31 | 32 | // quiet exit 33 | } 34 | 35 | func run() error { 36 | // create new UCI client 37 | client, err := engine.NewClient() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | // engine header with name, version, and author 43 | fmt.Printf("Mess %s by Rak Laptudirm\n", build.Version) 44 | 45 | switch args := os.Args[1:]; { 46 | case len(args) == 0: 47 | // no command-line arguments: start repl 48 | return client.Start() 49 | 50 | default: 51 | // command-line arguments: evaluate arguments as an UCI command 52 | // since we are not in a repl don't run any commands in parallel 53 | return client.RunWith(args, false) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/search/board.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package search 15 | 16 | import ( 17 | "laptudirm.com/x/mess/pkg/board" 18 | "laptudirm.com/x/mess/pkg/board/move" 19 | "laptudirm.com/x/mess/pkg/formats/fen" 20 | ) 21 | 22 | // String returns a human-readable ascii art representation of the search 23 | // board, along with it's fen string and zobrist hash. 24 | func (search *Context) String() string { 25 | return search.board.String() 26 | } 27 | 28 | // UpdatePosition updates the search board with the given fen. 29 | func (search *Context) UpdatePosition(fen fen.String) { 30 | search.board.UpdateWithFEN(fen) 31 | search.tt.Clear() 32 | } 33 | 34 | func (search *Context) Board() *board.Board { 35 | return search.board 36 | } 37 | 38 | func (search *Context) MakeMove(m move.Move) { 39 | search.board.MakeMove(m) 40 | } 41 | 42 | // MakeMoves makes the given moves on the search board. 43 | func (search *Context) MakeMoves(moves ...string) { 44 | for _, m := range moves { 45 | search.board.MakeMove(search.board.NewMoveFromString(m)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/board/move/variation.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package move 15 | 16 | import "fmt" 17 | 18 | // Variation represents a variation or a list of moves that can be played 19 | // one after the other on a position. 20 | type Variation struct { 21 | moves []Move 22 | } 23 | 24 | // Move return's the ith move of the variation. It returns move.Null if 25 | // the ith move doesn't exist. 26 | func (v *Variation) Move(i int) Move { 27 | if len(v.moves) <= i { 28 | return Null 29 | } 30 | 31 | return v.moves[i] 32 | } 33 | 34 | // Clear clears the variation. 35 | func (v *Variation) Clear() { 36 | v.moves = v.moves[:0] 37 | } 38 | 39 | // Update updates the variation with the new move and it's child variation. 40 | func (v *Variation) Update(pMove Move, line Variation) { 41 | v.Clear() 42 | v.moves = append(v.moves, pMove) 43 | v.moves = append(v.moves, line.moves...) 44 | } 45 | 46 | // String converts the variation into a human readable string. 47 | func (v Variation) String() string { 48 | str := fmt.Sprintf("%v", v.moves) 49 | return str[1 : len(str)-1] 50 | } 51 | -------------------------------------------------------------------------------- /internal/generator/attack/sliding.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "laptudirm.com/x/mess/pkg/board/bitboard" 18 | "laptudirm.com/x/mess/pkg/board/square" 19 | ) 20 | 21 | func bishop(s square.Square, occ bitboard.Board, isMask bool) bitboard.Board { 22 | diagonalMask := bitboard.Diagonals[s.Diagonal()] 23 | diagonalAttack := bitboard.Hyperbola(s, occ, diagonalMask) 24 | 25 | antiDiagonalMask := bitboard.AntiDiagonals[s.AntiDiagonal()] 26 | antiDiagonalAttack := bitboard.Hyperbola(s, occ, antiDiagonalMask) 27 | 28 | attacks := diagonalAttack | antiDiagonalAttack 29 | if isMask { 30 | attacks &^= bitboard.Rank1 | bitboard.Rank8 | bitboard.FileA | bitboard.FileH 31 | } 32 | 33 | return attacks 34 | } 35 | 36 | func rook(s square.Square, occ bitboard.Board, isMask bool) bitboard.Board { 37 | fileMask := bitboard.Files[s.File()] 38 | fileAttacks := bitboard.Hyperbola(s, occ, fileMask) 39 | 40 | rankMask := bitboard.Ranks[s.Rank()] 41 | rankAttacks := bitboard.Hyperbola(s, occ, rankMask) 42 | 43 | if isMask { 44 | fileAttacks &^= bitboard.Rank1 | bitboard.Rank8 45 | rankAttacks &^= bitboard.FileA | bitboard.FileH 46 | } 47 | 48 | return fileAttacks | rankAttacks 49 | } 50 | -------------------------------------------------------------------------------- /pkg/board/move/castling/rooks.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package castling 15 | 16 | import ( 17 | "laptudirm.com/x/mess/pkg/board/piece" 18 | "laptudirm.com/x/mess/pkg/board/square" 19 | ) 20 | 21 | // RookInfo is a struct which contains information about castling a rook. 22 | type RookInfo struct { 23 | From, To square.Square // source and target squares of the rook 24 | RookType piece.Piece // piece.Piece representation of the rook 25 | } 26 | 27 | // Rooks is a look up table which provides information about castling a 28 | // rook when a king castles. The table is indexed using the king's target 29 | // square. Squares other than the king's target squares during castling 30 | // contains the zero-value of RookInfo: RookInfo{}. 31 | var Rooks = [square.N]RookInfo{ 32 | square.G1: { 33 | From: square.H1, 34 | To: square.F1, 35 | RookType: piece.WhiteRook, 36 | }, 37 | square.C1: { 38 | From: square.A1, 39 | To: square.D1, 40 | RookType: piece.WhiteRook, 41 | }, 42 | square.G8: { 43 | From: square.H8, 44 | To: square.F8, 45 | RookType: piece.BlackRook, 46 | }, 47 | square.C8: { 48 | From: square.A8, 49 | To: square.D8, 50 | RookType: piece.BlackRook, 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /internal/generator/classical/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | _ "embed" 18 | 19 | "laptudirm.com/x/mess/internal/generator" 20 | "laptudirm.com/x/mess/pkg/board/piece" 21 | "laptudirm.com/x/mess/pkg/board/square" 22 | "laptudirm.com/x/mess/pkg/search/eval" 23 | "laptudirm.com/x/mess/pkg/search/eval/classical" 24 | ) 25 | 26 | type classicalStruct struct { 27 | PeSTO [piece.N][square.N]classical.Score 28 | StackedPawn [7]classical.Score 29 | } 30 | 31 | //go:embed .gotemplate 32 | var template string 33 | 34 | func main() { 35 | var table classicalStruct 36 | 37 | for i := 2; i < 6; i++ { 38 | table.StackedPawn[i] = classical.S( 39 | eval.Eval(-15*(i-1)), 40 | eval.Eval(-20*(i-1)), 41 | ) 42 | } 43 | 44 | // initialize PESTO tables 45 | for s := square.A8; s < square.N; s++ { 46 | for p := piece.Pawn; p <= piece.King; p++ { 47 | white := piece.New(p, piece.White) 48 | black := piece.New(p, piece.Black) 49 | 50 | table.PeSTO[white][s] = classical.S( 51 | mgPieceValues[p]+mgPieceTable[p][s], 52 | egPieceValues[p]+egPieceTable[p][s], 53 | ) 54 | 55 | table.PeSTO[black][s] = classical.S( 56 | mgPieceValues[p]+mgPieceTable[p][s^56], 57 | egPieceValues[p]+egPieceTable[p][s^56], 58 | ) 59 | } 60 | } 61 | 62 | generator.Generate("tables", template, table) 63 | } 64 | -------------------------------------------------------------------------------- /internal/engine/cmd/uci.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | "laptudirm.com/x/mess/internal/build" 18 | "laptudirm.com/x/mess/internal/engine/context" 19 | "laptudirm.com/x/mess/pkg/uci/cmd" 20 | ) 21 | 22 | // UCI command uci: 23 | // 24 | // Tells engine to use the uci (universal chess interface), this will be 25 | // sent once as a first command after program boot to tell the engine to 26 | // switch to uci mode. 27 | // 28 | // After receiving the uci command the engine must identify itself with the 29 | // id command and send the option commands to tell the GUI which engine 30 | // settings the engine supports if any. 31 | // 32 | // After that the engine should send uciok to acknowledge the uci mode. If 33 | // no uciok is sent within a certain time period, the engine task will be 34 | // killed by the GUI. 35 | func NewUci(engine *context.Engine) cmd.Command { 36 | return cmd.Command{ 37 | Name: "uci", 38 | Run: func(interaction cmd.Interaction) error { 39 | 40 | // identify engine 41 | interaction.Replyf("id name Mess %s", build.Version) 42 | interaction.Reply("id author Rak Laptudirm") 43 | interaction.Reply() 44 | 45 | // print the supported options 46 | interaction.Reply(engine.OptionSchema.String()) 47 | 48 | // declare uci support 49 | interaction.Reply("uciok") 50 | 51 | return nil 52 | }, 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/build/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "laptudirm.com/x/mess/scripts/util" 9 | ) 10 | 11 | func main() { 12 | var args []string 13 | 14 | // set env variables from args 15 | for _, arg := range os.Args[1:] { 16 | name, value, found := strings.Cut(arg, "=") 17 | if !found { 18 | args = append(args, arg) 19 | continue 20 | } 21 | 22 | // var=value 23 | os.Setenv(name, value) 24 | } 25 | 26 | // remaining args are tasks 27 | for _, arg := range args { 28 | task, ok := tasks[arg] 29 | if !ok { 30 | fmt.Fprintf(os.Stderr, "Invalid task %v.\n", arg) 31 | continue 32 | } 33 | 34 | if err := task(); err != nil { 35 | fmt.Fprintln(os.Stderr, err) 36 | } 37 | } 38 | } 39 | 40 | var tasks = map[string]func() error{ 41 | "--": nullTask, // used as separator for readability 42 | 43 | "dev-build": devBuild, // build a development binary 44 | "release-build": releaseBuild, // build a release binary 45 | } 46 | 47 | func nullTask() error { 48 | return nil 49 | } 50 | 51 | func devBuild() error { 52 | // version is latest tag-commits after tag-current commit hash 53 | version, err := util.RunWithOutput("git", "describe", "--tags") 54 | if err != nil { 55 | return build("v0.0.0") 56 | } 57 | 58 | return build(version) 59 | } 60 | 61 | func releaseBuild() error { 62 | // version is latest tag 63 | version, err := util.RunWithOutput("git", "describe", "--tags", "--abbrev=0") 64 | if err != nil { 65 | return build("v0.0.0") 66 | } 67 | 68 | return build(version) 69 | } 70 | 71 | func build(version string) error { 72 | project := "laptudirm.com/x/mess" 73 | ldflags := fmt.Sprintf("-X %s/internal/build.Version=%s", project, version) 74 | 75 | var exe string 76 | if exe = os.Getenv("EXE"); exe == "" { 77 | exe = "mess" 78 | } 79 | 80 | if os.Getenv("GOOS") == "windows" { 81 | exe += ".exe" 82 | } 83 | 84 | return util.RunNormal("go", "build", "-ldflags", ldflags, "-o", exe) 85 | } 86 | -------------------------------------------------------------------------------- /internal/generator/attack/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // This is a generator package used to generate go files containing data 15 | // pertaining to attack bitboards of chess pieces. 16 | package main 17 | 18 | import ( 19 | _ "embed" 20 | 21 | "laptudirm.com/x/mess/internal/generator" 22 | "laptudirm.com/x/mess/pkg/board/bitboard" 23 | "laptudirm.com/x/mess/pkg/board/move/attacks/magic" 24 | "laptudirm.com/x/mess/pkg/board/piece" 25 | "laptudirm.com/x/mess/pkg/board/square" 26 | ) 27 | 28 | type attackStruct struct { 29 | King [square.N]bitboard.Board 30 | Knight [square.N]bitboard.Board 31 | Pawn [piece.ColorN][square.N]bitboard.Board 32 | 33 | Rook magic.Table 34 | Bishop magic.Table 35 | } 36 | 37 | //go:embed .gotemplate 38 | var template string 39 | 40 | func main() { 41 | var a attackStruct 42 | 43 | // initialize standard lookup tables for non-sliding pieces 44 | for s := square.A8; s <= square.H1; s++ { 45 | // compute attack bitboards for current square 46 | a.King[s] = kingAttacksFrom(s) 47 | a.Knight[s] = knightAttacksFrom(s) 48 | a.Pawn[piece.White][s] = whitePawnAttacksFrom(s) 49 | a.Pawn[piece.Black][s] = blackPawnAttacksFrom(s) 50 | } 51 | 52 | // initialize magic lookup tables for sliding pieces 53 | a.Rook = *magic.NewTable(4096, rook) 54 | a.Bishop = *magic.NewTable(512, bishop) 55 | 56 | generator.Generate("tables", template, a) 57 | } 58 | -------------------------------------------------------------------------------- /internal/generator/zobrist/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | _ "embed" 18 | 19 | "laptudirm.com/x/mess/internal/generator" 20 | "laptudirm.com/x/mess/internal/util" 21 | "laptudirm.com/x/mess/pkg/board/move/castling" 22 | "laptudirm.com/x/mess/pkg/board/piece" 23 | "laptudirm.com/x/mess/pkg/board/square" 24 | "laptudirm.com/x/mess/pkg/board/zobrist" 25 | ) 26 | 27 | type zobristStruct struct { 28 | PieceSquare [piece.N][square.N]zobrist.Key 29 | EnPassant [square.FileN]zobrist.Key 30 | Castling [castling.N]zobrist.Key 31 | SideToMove zobrist.Key 32 | } 33 | 34 | //go:embed .gotemplate 35 | var template string 36 | 37 | func main() { 38 | var z zobristStruct 39 | 40 | var rng util.PRNG 41 | rng.Seed(1070372) // seed used from Stockfish 42 | 43 | // piece square numbers 44 | for p := 0; p < piece.N; p++ { 45 | for s := square.A8; s <= square.H1; s++ { 46 | z.PieceSquare[p][s] = zobrist.Key(rng.Uint64()) 47 | } 48 | } 49 | 50 | // en passant file numbers 51 | for f := square.FileA; f <= square.FileH; f++ { 52 | z.EnPassant[f] = zobrist.Key(rng.Uint64()) 53 | } 54 | 55 | // castling right numbers 56 | for r := castling.NoCasl; r <= castling.All; r++ { 57 | z.Castling[r] = zobrist.Key(rng.Uint64()) 58 | } 59 | 60 | // black to move 61 | z.SideToMove = zobrist.Key(rng.Uint64()) 62 | 63 | generator.Generate("keys", template, z) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/uci/defaults.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package uci 15 | 16 | import ( 17 | "errors" 18 | 19 | "laptudirm.com/x/mess/pkg/uci/cmd" 20 | ) 21 | 22 | // errQuit is the error returned to quit the client 23 | var errQuit = errors.New("client: quit") 24 | 25 | // default/preloaded commands 26 | 27 | // This is used to synchronize the engine with the GUI. When the GUI has 28 | // sent a command or multiple commands that can take some time to complete, 29 | // this command can be used to wait for the engine to be ready again or to 30 | // ping the engine to find out if it is still alive. E.g. this should be sent 31 | // after setting the path to the tablebases as this can take some time. 32 | // 33 | // This command is also required once before the engine is asked to do any 34 | // search to wait for the engine to finish initializing. 35 | // 36 | // This command must always be answered with readyok and can be sent also when 37 | // the engine is calculating in which case the engine should also immediately 38 | // answer with readyok without stopping the search. 39 | var cmdIsReady = cmd.Command{ 40 | Name: "isready", 41 | Run: func(interaction cmd.Interaction) error { 42 | interaction.Reply("readyok") 43 | return nil 44 | }, 45 | } 46 | 47 | // quit the program as soon as possible 48 | var cmdQuit = cmd.Command{ 49 | Name: "quit", 50 | Run: func(cmd.Interaction) error { 51 | return errQuit 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /internal/generator/zobrist/.gotemplate: -------------------------------------------------------------------------------- 1 | {{- /* 2 | This is a template file used for code generation with go generate. 3 | The notices given in the comment below only applies to the files 4 | generated with this template. This file can be freely edited when 5 | updating the code generator. 6 | */ -}} 7 | 8 | // Copyright © 2023 Rak Laptudirm 9 | // 10 | // Licensed under the Apache License, Version 2.0 (the "License"); 11 | // you may not use this file except in compliance with the License. 12 | // You may obtain a copy of the License at 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | 21 | // Code generated by go generate; DO NOT EDIT THE CONTENTS OF THIS FILE 22 | // The source code for the generator can be found at generator/zobrist 23 | 24 | package zobrist 25 | 26 | // PieceSquare contains zobrist keys for each type of piece on every 27 | // square. It can be index by PieceSquare[pieceType][pieceSquare]. 28 | var PieceSquare = [16][64]Key{ {{- range .PieceSquare }} 29 | { {{ range . }}{{ printf "%0#16v" . }}, {{ end }} },{{ end }} 30 | } 31 | 32 | // EnPassant contains the zobrist keys for all possible en passant 33 | // capture squares on each file. It can be indexed by EnPassant[file]. 34 | var EnPassant = [8]Key{ {{ range .EnPassant }}{{ printf "%#v" .}}, {{ end }}} 35 | 36 | // Castling contains zobrist keys for every possible permutation of 37 | // castling rights. It can be indexed by Castling[castlingRights]. 38 | var Castling = [16]Key{ {{ range .Castling }}{{ printf "%#v" .}}, {{ end }}} 39 | 40 | // SideToMove represents the zobrist key for when the side to move is 41 | // black. There is no zobrist key for when the side to move is white. 42 | var SideToMove Key = {{ printf "%#v" .SideToMove }} 43 | -------------------------------------------------------------------------------- /internal/engine/cmd/ucinewgame.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | "laptudirm.com/x/mess/internal/engine/context" 18 | "laptudirm.com/x/mess/pkg/search" 19 | "laptudirm.com/x/mess/pkg/uci/cmd" 20 | ) 21 | 22 | // UCI command ucinewgame 23 | // 24 | // This is sent to the engine when the next search (started with position 25 | // and go) will be from a different game. This can be a new game the 26 | // engine should play or a new game it should analyze but also the next 27 | // position from a test suite with positions only. 28 | // 29 | // [this clause is ignored and Mess depends on this command] 30 | // If the GUI hasn't sent a ucinewgame before the first position command, 31 | // the engine shouldn't expect any further ucinewgame commands as the GUI 32 | // is probably not supporting the ucinewgame command. So the engine should 33 | // not rely on this command even though all new GUIs should support it. 34 | // 35 | // As the engine's reaction to ucinewgame can take some time the GUI should 36 | // always send isready after ucinewgame to wait for the engine to finish its 37 | // operation. 38 | func NewUciNewGame(engine *context.Engine) cmd.Command { 39 | return cmd.Command{ 40 | Name: "ucinewgame", 41 | Run: func(interaction cmd.Interaction) error { 42 | // new context for new game 43 | engine.Search = search.NewContext(func(r search.Report) { 44 | interaction.Reply(r) 45 | }, engine.Options.Hash) 46 | return nil 47 | }, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pkg/search/heuristics.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package search 15 | 16 | import ( 17 | "laptudirm.com/x/mess/internal/util" 18 | "laptudirm.com/x/mess/pkg/board/move" 19 | "laptudirm.com/x/mess/pkg/search/eval" 20 | ) 21 | 22 | // storeKiller tries to store the given move from the given depth as one 23 | // of the two killer moves. 24 | func (search *Context) storeKiller(plys int, killer move.Move) { 25 | if !killer.IsCapture() && killer != search.killers[plys][0] { 26 | // different move in killer 1 27 | // move it to killer 2 position 28 | search.killers[plys][1] = search.killers[plys][0] 29 | search.killers[plys][0] = killer // new killer 1 30 | } 31 | } 32 | 33 | // updateHistory updates the history score of the given move with the given 34 | // bonus. It also verifies that the move is a quiet move. 35 | func (search *Context) updateHistory(m move.Move, bonus eval.Move) { 36 | if !m.IsCapture() { 37 | entry := &search.history[search.board.SideToMove][m.Source()][m.Target()] 38 | *entry += bonus - *entry*util.Abs(bonus)/32768 39 | } 40 | } 41 | 42 | // depthBonus returns the the history bonus for a particular depth. 43 | func depthBonus(depth int) eval.Move { 44 | return eval.Move(util.Min(2000, depth*155)) 45 | } 46 | 47 | // seeMargins returns the see pruning thresholds for the given depth. 48 | func seeMargins(depth int) (quiet, noisy eval.Eval) { 49 | quiet = eval.Eval(-64 * depth) 50 | noisy = eval.Eval(-19 * depth * depth) 51 | return quiet, noisy 52 | } 53 | -------------------------------------------------------------------------------- /internal/generator/bitboard/.gotemplate: -------------------------------------------------------------------------------- 1 | {{- /* 2 | This is a template file used for code generation with go generate. 3 | The notices given in the comment below only applies to the files 4 | generated with this template. This file can be freely edited when 5 | updating the code generator. 6 | */ -}} 7 | 8 | // Copyright © 2023 Rak Laptudirm 9 | // 10 | // Licensed under the Apache License, Version 2.0 (the "License"); 11 | // you may not use this file except in compliance with the License. 12 | // You may obtain a copy of the License at 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | 21 | // Code generated by go generate; DO NOT EDIT THE CONTENTS OF THIS FILE 22 | // The source code for the generator can be found at generator/bitboard 23 | 24 | package bitboard 25 | 26 | // Between contains bitboards which have the path between two squares set. 27 | // The definition of path is only valid for squares which lie on the same 28 | // file, rank, diagonal, or anti-diagonal. For all other square 29 | // combinations, the path is Empty. 30 | var Between = [64][64]Board{ {{- range .Between }} 31 | { {{ range . }}{{ printf "%0#16v" . }}, {{ end }} },{{ end }} 32 | } 33 | 34 | var KingAreas = [2][64]Board{ {{- range .KingAreas }} 35 | { {{ range . }}{{ printf "%0#16v" . }}, {{ end }} },{{ end }} 36 | } 37 | 38 | var AdjacentFiles = [8]Board{ {{ range .AdjacentFiles }}{{ printf "%0#16v" . }}, {{ end }} } 39 | 40 | var PassedPawnMask = [2][64]Board{ {{- range .PassedPawnMask }} 41 | { {{ range . }}{{ printf "%0#16v" . }}, {{ end }} },{{ end }} 42 | } 43 | 44 | var ForwardFileMask = [2][64]Board{ {{- range .ForwardFileMask }} 45 | { {{ range . }}{{ printf "%0#16v" . }}, {{ end }} },{{ end }} 46 | } 47 | 48 | var ForwardRanksMask = [2][8]Board{ {{- range .ForwardRanksMask }} 49 | { {{ range . }}{{ printf "%0#16v" . }}, {{ end }} },{{ end }} 50 | } 51 | -------------------------------------------------------------------------------- /internal/util/prng.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | // xorshift64star Pseudo-Random Number Generator 17 | // This struct is based on original code written and dedicated 18 | // to the public domain by Sebastiano Vigna (2014). 19 | // It has the following characteristics: 20 | // 21 | // - Outputs 64-bit numbers 22 | // - Passes Dieharder and SmallCrush test batteries 23 | // - Does not require warm-up, no zeroland to escape 24 | // - Internal state is a single 64-bit integer 25 | // - Period is 2^64 - 1 26 | // - Speed: 1.60 ns/call (Core i7 @3.40GHz) 27 | // 28 | // For further analysis see 29 | // 30 | // 31 | type PRNG struct { 32 | seed uint64 33 | } 34 | 35 | // Seed seeds the pseudo-random number generator with the given uint. 36 | func (p *PRNG) Seed(s uint64) { 37 | p.seed = s 38 | } 39 | 40 | // Uint64 generates a new pseudo-random uint64. 41 | func (p *PRNG) Uint64() uint64 { 42 | // linear feedback shifts 43 | p.seed ^= p.seed >> 12 44 | p.seed ^= p.seed << 25 45 | p.seed ^= p.seed >> 27 46 | 47 | // scramble result with non-linear function 48 | return p.seed * 2685821657736338717 49 | } 50 | 51 | // SparseUint64 generates a pseudo-random sparse uint64, i.e, a number 52 | // with very few set bits. This is useful in magic table generation. 53 | func (p *PRNG) SparseUint64() uint64 { 54 | // bitwise and three pseudo-random uint64s together 55 | // only the bits set in all three are set in the result 56 | 57 | //nolint:staticcheck // Uint64 is an impure function 58 | return p.Uint64() & p.Uint64() & p.Uint64() 59 | } 60 | -------------------------------------------------------------------------------- /internal/util/math.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package util 15 | 16 | // Type number represents every value that can be represented as an number. 17 | type number interface { 18 | integer | ~float32 | ~float64 19 | } 20 | 21 | type integer interface { 22 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | 23 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 24 | } 25 | 26 | // Max returns the larger value between the integers a and b. 27 | func Max[T number](a, b T) T { 28 | if a > b { 29 | return a 30 | } 31 | 32 | return b 33 | } 34 | 35 | // Min returns the smaller value between the integers a and b. 36 | func Min[T number](a, b T) T { 37 | if a < b { 38 | return a 39 | } 40 | 41 | return b 42 | } 43 | 44 | // Abs returns the absolute value of the integer x. 45 | func Abs[T number](x T) T { 46 | if x < 0 { 47 | return -x 48 | } 49 | 50 | return x 51 | } 52 | 53 | // Clamp returns a value between min and max. If n is between min and max, 54 | // n is returned, otherwise the closest limit is returned. 55 | func Clamp[T number](n, min, max T) T { 56 | return Max(min, Min(n, max)) 57 | } 58 | 59 | // Lerp does a linear interpolation between the given start and stop 60 | // numbers by the ratio numerator/denominator(better precision for ints). 61 | func Lerp[T number](start, stop, numerator, denominator T) T { 62 | return (stop*numerator + start*(denominator-numerator)) / denominator 63 | } 64 | 65 | // Btoi converts the given boolean value to a number. The number is 66 | // equivalent to 1 if the value is true and 0 if it is false. 67 | func Btoi[T number](condition bool) T { 68 | if condition { 69 | return 1 70 | } 71 | 72 | return 0 73 | } 74 | -------------------------------------------------------------------------------- /pkg/board/square/diagonals.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package square 15 | 16 | // Diagonal represents a diagonal on the chessboard. 17 | // Every NE-SW diagonal is called a diagonal. 18 | // 19 | // e d c b a 9 8 7 20 | // d c b a 9 8 7 6 21 | // c b a 9 8 7 6 5 22 | // b a 9 8 7 6 5 4 23 | // a 9 8 7 6 5 4 3 24 | // 9 8 7 6 5 4 3 2 25 | // 8 7 6 5 4 3 2 1 26 | // 7 6 5 4 3 2 1 0 27 | type Diagonal int 28 | 29 | // constants representing various diagonals 30 | const ( 31 | // bottom diagonals 32 | DiagonalH1H1 Diagonal = iota 33 | DiagonalH2G1 34 | DiagonalH3F1 35 | DiagonalH4E1 36 | DiagonalH5D1 37 | DiagonalH6C1 38 | DiagonalH7B1 39 | 40 | // main diagonal 41 | DiagonalH8A1 42 | 43 | // top diagonals 44 | DiagonalG8A2 45 | DiagonalF8A3 46 | DiagonalE8A4 47 | DiagonalD8A5 48 | DiagonalC8A6 49 | DiagonalB8A7 50 | DiagonalA8A8 51 | ) 52 | 53 | // AntiDiagonal represents an anti-diagonal on the chessboard. 54 | // Every NW-SE diagonal is called an anti-diagonal. 55 | // 56 | // 7 8 9 a b c d e 57 | // 6 7 8 9 a b c d 58 | // 5 6 7 8 9 a b c 59 | // 4 5 6 7 8 9 a b 60 | // 3 4 5 6 7 8 9 a 61 | // 2 3 4 5 6 7 8 9 62 | // 1 2 3 4 5 6 7 8 63 | // 0 1 2 3 4 5 6 7 64 | type AntiDiagonal int 65 | 66 | // constants representing various anti-diagonals 67 | const ( 68 | // bottom anti-diagonals 69 | DiagonalA1A1 AntiDiagonal = iota 70 | DiagonalA2B1 71 | DiagonalA3C1 72 | DiagonalA4D1 73 | DiagonalA5E1 74 | DiagonalA6F1 75 | DiagonalA7G1 76 | 77 | // main anti-diagonal 78 | DiagonalA8H1 79 | 80 | // top anti-diagonals 81 | DiagonalB8H2 82 | DiagonalC8H3 83 | DiagonalD8H4 84 | DiagonalE8H5 85 | DiagonalF8H6 86 | DiagonalG8H7 87 | DiagonalH8H8 88 | ) 89 | -------------------------------------------------------------------------------- /internal/engine/cmd/setoption.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | "errors" 18 | 19 | "laptudirm.com/x/mess/internal/engine/context" 20 | "laptudirm.com/x/mess/pkg/uci/cmd" 21 | "laptudirm.com/x/mess/pkg/uci/flag" 22 | ) 23 | 24 | // UCI command setoption 25 | // 26 | // This is sent to the engine when the user wants to change the internal 27 | // parameters of the engine. For the button type no value is needed. 28 | // 29 | // One string will be sent for each parameter and this will only be sent 30 | // when the engine is waiting. The name and value of the option in id 31 | // should not be case sensitive and can include spaces. 32 | // 33 | // The substrings value and name should be avoided in id and x to allow 34 | // unambiguous parsing, for example do not use name = draw value. 35 | func NewSetOption(engine *context.Engine) cmd.Command { 36 | flags := flag.NewSchema() 37 | 38 | flags.Single("name") 39 | flags.Variadic("value") 40 | 41 | return cmd.Command{ 42 | Name: "setoption", 43 | Run: func(interaction cmd.Interaction) error { 44 | name, value, err := parseSetOptionOptions(interaction.Values) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return engine.OptionSchema.SetOption(name, value) 50 | }, 51 | Flags: flags, 52 | } 53 | } 54 | 55 | func parseSetOptionOptions(values flag.Values) (string, []string, error) { 56 | if !values["name"].Set { 57 | return "", nil, errors.New("setoption: name flag not found") 58 | } 59 | 60 | name := values["name"].Value.(string) 61 | 62 | value := []string{} 63 | if values["value"].Set { 64 | value = values["value"].Value.([]string) 65 | } 66 | 67 | return name, value, nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/engine/engine.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package engine 15 | 16 | import ( 17 | "laptudirm.com/x/mess/internal/engine/cmd" 18 | "laptudirm.com/x/mess/internal/engine/context" 19 | "laptudirm.com/x/mess/internal/engine/options" 20 | "laptudirm.com/x/mess/pkg/uci" 21 | "laptudirm.com/x/mess/pkg/uci/option" 22 | ) 23 | 24 | // NewClient returns a new uci.Client containing all of the engine's 25 | // supported commands. The commands share a context.Engine among them. 26 | func NewClient() (uci.Client, error) { 27 | 28 | // initialize engine context 29 | engine := &context.Engine{} 30 | 31 | // add uci commands to engine 32 | engine.Client = uci.NewClient() 33 | engine.Client.AddCommand(cmd.NewD(engine)) 34 | engine.Client.AddCommand(cmd.NewGo(engine)) 35 | engine.Client.AddCommand(cmd.NewUci(engine)) 36 | engine.Client.AddCommand(cmd.NewStop(engine)) 37 | engine.Client.AddCommand(cmd.NewBench(engine)) 38 | engine.Client.AddCommand(cmd.NewPosition(engine)) 39 | engine.Client.AddCommand(cmd.NewSetOption(engine)) 40 | engine.Client.AddCommand(cmd.NewPonderHit(engine)) 41 | engine.Client.AddCommand(cmd.NewUciNewGame(engine)) 42 | 43 | // run ucinewgame to initialize position 44 | if err := engine.Client.Run("ucinewgame"); err != nil { 45 | return uci.Client{}, err 46 | } 47 | 48 | // add uci options to engine 49 | engine.OptionSchema = option.NewSchema() 50 | engine.OptionSchema.AddOption("Hash", options.NewHash(engine)) 51 | engine.OptionSchema.AddOption("Ponder", options.NewPonder(engine)) 52 | engine.OptionSchema.AddOption("Threads", options.NewThreads(engine)) 53 | 54 | // initialize options 55 | if err := engine.OptionSchema.SetDefaults(); err != nil { 56 | return uci.Client{}, err 57 | } 58 | 59 | return engine.Client, nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/generator/attack/nonSliding.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "laptudirm.com/x/mess/pkg/board/bitboard" 18 | "laptudirm.com/x/mess/pkg/board/square" 19 | ) 20 | 21 | func whitePawnAttacksFrom(s square.Square) bitboard.Board { 22 | pawnUp := bitboard.Square(s).North() 23 | return pawnUp.East() | pawnUp.West() 24 | } 25 | 26 | func blackPawnAttacksFrom(s square.Square) bitboard.Board { 27 | pawnUp := bitboard.Square(s).South() 28 | return pawnUp.East() | pawnUp.West() 29 | } 30 | 31 | // knightAttacksFrom generates an attack bitboard containing all the 32 | // possible squares a knight can move to from the given square. 33 | func knightAttacksFrom(from square.Square) bitboard.Board { 34 | knight := bitboard.Square(from) 35 | 36 | knightNorth := knight.North().North() 37 | knightSouth := knight.South().South() 38 | 39 | knightEast := knight.East().East() 40 | knightWest := knight.West().West() 41 | 42 | knightAttacks := knightNorth.East() | knightNorth.West() 43 | knightAttacks |= knightSouth.East() | knightSouth.West() 44 | 45 | knightAttacks |= knightEast.North() | knightEast.South() 46 | knightAttacks |= knightWest.North() | knightWest.South() 47 | 48 | return knightAttacks 49 | } 50 | 51 | // kingAttacksFrom generates an attack bitboard containing all the 52 | // possible squares a king can move to from the given square. 53 | func kingAttacksFrom(from square.Square) bitboard.Board { 54 | king := bitboard.Square(from) 55 | 56 | kingNorth := king.North() 57 | kingSouth := king.South() 58 | kingEast := king.East() 59 | kingWest := king.West() 60 | 61 | kingAttacks := kingNorth | kingSouth | kingEast | kingWest 62 | 63 | kingAttacks |= kingNorth.East() | kingNorth.West() 64 | kingAttacks |= kingSouth.East() | kingSouth.West() 65 | 66 | return kingAttacks 67 | } 68 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 2 | # Copyright © 2023 Rak Laptudirm # 3 | # # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); # 5 | # you may not use this file except in compliance with the License. # 6 | # You may obtain a copy of the License at # 7 | # http://www.apache.org/licenses/LICENSE-2.0 # 8 | # # 9 | # Unless required by applicable law or agreed to in writing, software # 10 | # distributed under the License is distributed on an "AS IS" BASIS, # 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 12 | # See the License for the specific language governing permissions and # 13 | # limitations under the License. # 14 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 15 | 16 | # ==================== # 17 | # Base Executable Name # 18 | # ==================== # 19 | EXE = ./bin/mess 20 | 21 | # ==================== # 22 | # Make a Native Binary # 23 | # ==================== # 24 | 25 | # Development Binary: Detailed Versioning 26 | dev-binary: 27 | go run ./scripts/build -- EXE=${EXE} -- dev-build 28 | 29 | # Release Binary: Tagged Versioning 30 | release-binary: 31 | go run ./scripts/build -- EXE=${EXE} -- release-build 32 | 33 | # ============================ # 34 | # Make Cross-Platform Binaries # 35 | # ============================ # 36 | release-binaries: 37 | go run ./scripts/build -- GOOS=linux GOARCH=arm EXE=${EXE}-linux-arm -- release-build 38 | go run ./scripts/build -- GOOS=linux GOARCH=arm64 EXE=${EXE}-linux-arm64 -- release-build 39 | go run ./scripts/build -- GOOS=linux GOARCH=amd64 EXE=${EXE}-linux-amd64 -- release-build 40 | go run ./scripts/build -- GOOS=darwin GOARCH=amd64 EXE=${EXE}-darwin-amd64 -- release-build 41 | go run ./scripts/build -- GOOS=darwin GOARCH=arm64 EXE=${EXE}-darwin-arm64 -- release-build 42 | go run ./scripts/build -- GOOS=windows GOARCH=amd64 EXE=${EXE}-windows-amd64 -- release-build 43 | go run ./scripts/build -- GOOS=windows GOARCH=386 EXE=${EXE}-windows-386 -- release-build 44 | 45 | # ========================= # 46 | # Make Generated Code Files # 47 | # ========================= # 48 | code-files: 49 | go generate ./... 50 | -------------------------------------------------------------------------------- /pkg/board/mailbox/mailbox.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package mailbox implements a 8x8 mailbox chessboard representation. 15 | // https://www.chessprogramming.org/8x8_Board 16 | package mailbox 17 | 18 | import ( 19 | "fmt" 20 | 21 | "laptudirm.com/x/mess/pkg/board/piece" 22 | "laptudirm.com/x/mess/pkg/board/square" 23 | ) 24 | 25 | // Board represents a 8x8 chessboard consisting of pieces. 26 | type Board [square.N]piece.Piece 27 | 28 | // String converts a Board into it's human readable string representation. 29 | func (b Board) String() string { 30 | // leading divider 31 | s := "+---+---+---+---+---+---+---+---+\n" 32 | 33 | for rank := square.Rank8; rank <= square.Rank1; rank++ { 34 | s += "| " 35 | 36 | for file := square.FileA; file <= square.FileH; file++ { 37 | square := square.New(file, rank) 38 | s += b[square].String() + " | " 39 | } 40 | 41 | s += rank.String() 42 | s += "\n+---+---+---+---+---+---+---+---+\n" 43 | } 44 | 45 | s += " a b c d e f g h\n" 46 | return s 47 | } 48 | 49 | // FEN generates the position part of a fen string representing the current 50 | // Board position. It can be used together with other information about the 51 | // position to generate a complete fen string. 52 | func (b *Board) FEN() string { 53 | var fen string 54 | 55 | empty := 0 56 | for i, p := range b { 57 | currSquare := square.Square(i) 58 | 59 | if p == piece.NoPiece { 60 | // increase empty square count 61 | empty++ 62 | } else { 63 | 64 | if empty > 0 { 65 | fen += fmt.Sprint(empty) 66 | empty = 0 67 | } 68 | 69 | fen += p.String() 70 | } 71 | 72 | // rank separators after last file 73 | if currSquare.File() == square.FileH { 74 | if empty > 0 { 75 | fen += fmt.Sprint(empty) 76 | empty = 0 77 | } 78 | 79 | // no trailing separator after last rank 80 | if currSquare.Rank() != square.Rank1 { 81 | fen += "/" 82 | } 83 | } 84 | } 85 | 86 | return fen 87 | } 88 | -------------------------------------------------------------------------------- /internal/generator/attack/.gotemplate: -------------------------------------------------------------------------------- 1 | {{- /* 2 | This is a template file used for code generation with go generate. 3 | The notices given in the comment below only applies to the files 4 | generated with this template. This file can be freely edited when 5 | updating the code generator. 6 | */ -}} 7 | 8 | // Copyright © 2023 Rak Laptudirm 9 | // 10 | // Licensed under the Apache License, Version 2.0 (the "License"); 11 | // you may not use this file except in compliance with the License. 12 | // You may obtain a copy of the License at 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | 21 | // Code generated by go generate; DO NOT EDIT THE CONTENTS OF THIS FILE 22 | // The source code for the generator can be found at generator/attack 23 | 24 | package attacks 25 | 26 | import ( 27 | "laptudirm.com/x/mess/pkg/board/bitboard" 28 | "laptudirm.com/x/mess/pkg/board/move/attacks/magic" 29 | ) 30 | 31 | // non-sliding pieces; standard tables 32 | 33 | // King contains all the attack sets of a king indexed by it's square. 34 | var King = [64]bitboard.Board{ {{ range .King }}{{ printf "%#v" .}}, {{ end }}} 35 | 36 | // Knight contains all the attack sets of a knight indexed by it's square. 37 | var Knight = [64]bitboard.Board{ {{ range .Knight }}{{ printf "%#v" .}}, {{ end }}} 38 | 39 | // Pawn contains all the attack sets of a pawn indexed by it's color and 40 | // square. Note these are the diagonal attack sets, not move sets. 41 | var Pawn = [2][64]bitboard.Board{ {{- range .Pawn }} 42 | { {{ range . }}{{ printf "%0#16v" . }}, {{ end }} },{{ end }} 43 | } 44 | 45 | // sliding pieces; magic tables 46 | // these tables are un-exported since utility functions will be declared 47 | // to make probing these tables simpler in the attacks package 48 | 49 | var rookTable = magic.Table{ 50 | Magics: [64]magic.Magic{ {{ range .Rook.Magics }}{ {{ printf "%#v, %#v, %#v" .Number .BlockerMask .Shift }} }, {{ end }}}, 51 | Table: [64][]bitboard.Board{ {{- range .Rook.Table }} 52 | { {{ range . }}{{ printf "%0#16v" . }}, {{ end }} },{{ end }} 53 | }, 54 | } 55 | 56 | var bishopTable = magic.Table{ 57 | Magics: [64]magic.Magic{ {{ range .Bishop.Magics }}{ {{ printf "%#v, %#v, %#v" .Number .BlockerMask .Shift }} }, {{ end }}}, 58 | Table: [64][]bitboard.Board{ {{- range .Bishop.Table }} 59 | { {{ range . }}{{ printf "%0#16v" . }}, {{ end }} },{{ end }} 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | mess logo 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
19 | 20 | # Overview 21 | 22 | Mess is an open source, UCI-compliant chess engine. Mess is not a complete chess program 23 | and requires an UCI-compliant chess graphical user interface(e.g. XBoard with PolyGlot, 24 | Scid, Cute Chess, eboard, Arena, Sigma Chess, Shredder, Chess Partner or Fritz) to be 25 | used comfortably. 26 | 27 | Mess's code structure is extremely modular and thus it may be used as a library for 28 | developing chess engines in go. The [`./pkg`](./pkg) directory will contain all the 29 | packages that are available for use publicly. 30 | 31 | # Mess on Lichess 32 | 33 | > The MessChessEngine Bot is currently hosted on my laptop, and therefore might not 34 | always be online. The most reliable way to play against mess is to download the engine 35 | and use it with an UCI-compliant graphical user interface. 36 | 37 | - 🖥️ [Watch Mess Play on Lichess](https://lichess.org/@/MessChessEngine/tv) 38 | - 🎮 [Challange Mess on Lichess](https://lichess.org/@/MessChessEngine) 39 | 40 | # Installation 41 | 42 | ### Prebuilt Binaries 43 | 44 | Prebuilt binaries for mess can be found in the releases section of this repository. 45 | Binaries are only provided for release versions of Mess. Binaries have been provided for 46 | all the major operating systems, including Windows, Linux, and Darwin. 47 | 48 | A list of all of Mess's releases can be found [here](https://github.com/raklaptudirm/mess/releases). 49 | 50 | ### Prerequisites 51 | 52 | The following need to be installed before you can install Mess: 53 | - [The Go Programming Language](https://go.dev/dl/) 54 | - [Git](https://git-scm.com/downloads) (for building from source) 55 | 56 | ### Install Globally 57 | 58 | ```bash 59 | go install laptudirm.com/x/mess@latest 60 | ``` 61 | 62 | ### Build from source 63 | ```bash 64 | # downloard mess (other methods also work) 65 | git clone https://github.com/rakarchive/mess.git 66 | cd mess 67 | 68 | # building mess binary 69 | make EXE=path # creates binary in path 70 | ``` 71 | 72 | # License 73 | 74 | Mess is licensed under the [Apache 2.0 License](./LICENSE). 75 | -------------------------------------------------------------------------------- /pkg/search/deepning.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package search 15 | 16 | import ( 17 | "laptudirm.com/x/mess/pkg/board/move" 18 | "laptudirm.com/x/mess/pkg/search/eval" 19 | ) 20 | 21 | // iterativeDeepening is the main search function. It implements an iterative 22 | // deepening loop which call's the negamax search function for each iteration. 23 | // It returns the principal variation and it's evaluation. 24 | // https://www.chessprogramming.org/Iterative_Deepening 25 | func (search *Context) iterativeDeepening() (move.Variation, eval.Eval) { 26 | 27 | // iterative deepening loop, starting from 1, call negamax for each depth 28 | // until the depth limit is reached or time runs out. This allows us to 29 | // search to any depth depending on the allocated time. Previous iterations 30 | // also populate the transposition table with scores and pv moves which makes 31 | // iterative deepening to a depth faster that directly searching that depth. 32 | for search.stats.Depth = 1; search.stats.Depth <= search.limits.Depth; search.stats.Depth++ { 33 | 34 | // the new pv isn't directly stored into the pv variable since it will 35 | // pollute the correct pv if the next search is incomplete. Instead the 36 | // old pv is overwritten only if the search is found to be complete. 37 | score, pv := search.aspirationWindow(search.stats.Depth, search.pvScore) 38 | 39 | if search.stopped { 40 | // don't use the new pv if search was stopped since the 41 | // search is probably unfinished 42 | 43 | // search.shouldStop is not used since the new pv is 44 | // only bad if the search was stopped in the middle 45 | // of the iteration, and not in here 46 | break 47 | } 48 | 49 | // search successfully completed, so update pv 50 | search.pv = pv 51 | search.pvScore = score 52 | 53 | // print some info for the GUI 54 | search.reporter(search.GenerateReport()) 55 | 56 | if search.time != nil && search.time.OptimisticExpired() { 57 | break 58 | } 59 | } 60 | 61 | if search.stats.Depth < search.limits.Depth && search.limits.Infinite { 62 | for search.limits.Infinite && !search.shouldStop() { 63 | // if in infinite mode, wait for stop 64 | } 65 | } 66 | 67 | return search.pv, search.pvScore 68 | } 69 | -------------------------------------------------------------------------------- /pkg/search/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package search 15 | 16 | import ( 17 | "fmt" 18 | "time" 19 | 20 | "laptudirm.com/x/mess/internal/util" 21 | "laptudirm.com/x/mess/pkg/board/move" 22 | "laptudirm.com/x/mess/pkg/search/eval" 23 | ) 24 | 25 | // Stats stores the search's various statistics. 26 | type Stats struct { 27 | // time when search started 28 | SearchStart time.Time 29 | 30 | TTHits int // transposition table hits 31 | Nodes int // positions (nodes) searched 32 | 33 | Depth int // current iterative depth 34 | SelDepth int // maximum depth reached 35 | } 36 | 37 | // GenerateReport generates a statistics report from the current search 38 | // context. It contains all the relevant stats that anyone can need to 39 | // know about a search. 40 | func (search *Context) GenerateReport() Report { 41 | searchTime := time.Since(search.stats.SearchStart) 42 | 43 | return Report{ 44 | Depth: search.stats.Depth, 45 | SelDepth: search.stats.SelDepth, 46 | 47 | Nodes: search.stats.Nodes, 48 | Nps: float64(search.stats.Nodes) / util.Max(0.001, searchTime.Seconds()), 49 | 50 | Hashfull: 0, // TODO: implement hashfull calculation 51 | 52 | Time: searchTime, 53 | 54 | Score: search.pvScore, 55 | PV: search.pv, 56 | } 57 | } 58 | 59 | // Reporter takes a report as input and processes it in some way. 60 | type Reporter func(Report) 61 | 62 | // Report represents a report of various statistics about a search. 63 | type Report struct { 64 | // depth stats 65 | Depth int // current id depth 66 | SelDepth int // max depth reached 67 | 68 | // node stats 69 | Nodes int 70 | Nps float64 71 | 72 | // tt stats 73 | Hashfull float64 74 | 75 | // search time stats 76 | Time time.Duration 77 | 78 | // principal variation stats 79 | Score eval.Eval 80 | PV move.Variation 81 | } 82 | 83 | // String converts a Report into an UCI compatible info string. 84 | func (report Report) String() string { 85 | return fmt.Sprintf( 86 | "info depth %d seldepth %d score %s nodes %d nps %.f hashfull %.f tbhits 0 time %d pv %s", 87 | report.Depth, report.SelDepth, report.Score, report.Nodes, report.Nps, 88 | report.Hashfull*1000, // convert fraction to per-mille 89 | report.Time.Milliseconds(), report.PV, 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/search/limits.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package search 15 | 16 | import ( 17 | "laptudirm.com/x/mess/pkg/board/piece" 18 | ) 19 | 20 | // Limits contains the various limits which decide how long a search can 21 | // run for. It should be passed to the main search function when starting 22 | // a new search. 23 | type Limits struct { 24 | // search tree limits 25 | Nodes int 26 | Depth int 27 | 28 | // TODO: implement searching selected moves 29 | // Moves []move.Move 30 | 31 | // search time limits 32 | Infinite bool 33 | MoveTime int 34 | Time, Increment [piece.ColorN]int 35 | MovesToGo int 36 | } 37 | 38 | // UpdateLimits updates the search limits while a search is in progress. 39 | // The caller should make sure that a search is indeed in progress before 40 | // calling UpdateLimits. 41 | func (search *Context) UpdateLimits(limits Limits) { 42 | search.limits = limits // update limits 43 | 44 | switch { 45 | case limits.Infinite: 46 | return 47 | 48 | case limits.MoveTime != 0: 49 | search.time = &TimeManagerMovetime{Duration: limits.MoveTime} 50 | 51 | default: 52 | search.time = &TimeManagerNormal{ 53 | Time: limits.Time, 54 | Increment: limits.Increment, 55 | MovesToGo: limits.MovesToGo, 56 | Us: search.sideToMove, 57 | context: search, 58 | } 59 | } 60 | 61 | search.time.GetDeadline() // get search deadline 62 | } 63 | 64 | // shouldStop checks the various limits provided for the search and 65 | // reports if the search should be stopped at that moment. 66 | func (search *Context) shouldStop() bool { 67 | 68 | // the depth limit is kept up in the iterative deepening 69 | // loop so it's breaching isn't tested in this function 70 | 71 | switch { 72 | case search.stopped: 73 | // search already stopped 74 | // no checking necessary 75 | return true 76 | 77 | case search.stats.Nodes&2047 != 0, search.limits.Infinite: 78 | // only check once every 2048 nodes to prevent 79 | // spending too much time here 80 | 81 | // if search is infinite never stop 82 | 83 | return false 84 | 85 | case search.stats.Nodes > search.limits.Nodes, search.time.PessimisticExpired(): 86 | // node limit or time limit crossed 87 | search.Stop() 88 | return true 89 | 90 | default: 91 | // no search stopping clause reached 92 | return false 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pkg/search/aspiration.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package search 15 | 16 | import ( 17 | "laptudirm.com/x/mess/internal/util" 18 | "laptudirm.com/x/mess/pkg/board/move" 19 | "laptudirm.com/x/mess/pkg/search/eval" 20 | ) 21 | 22 | // aspirationWindow implements aspiration windows, which are a way to 23 | // reduce the search space in an alpha-beta search. The technique is to 24 | // use a guess of the expected value (usually from the last iteration in 25 | // iterative deepening), and use a window around this as the alpha-beta 26 | // bounds. Because the window is narrower, more beta cutoffs are achieved, 27 | // and the search takes a shorter time. The drawback is that if the true 28 | // score is outside this window, then a costly re-search must be made. 29 | func (search *Context) aspirationWindow(depth int, prevEval eval.Eval) (eval.Eval, move.Variation) { 30 | // default values for alpha and beta 31 | alpha := eval.Eval(-eval.Inf) 32 | beta := eval.Eval(eval.Inf) 33 | 34 | initialDepth := depth 35 | 36 | // aspiration window size 37 | var windowSize eval.Eval = 10 38 | 39 | // only do aspiration search at greater than depth 5 40 | if depth >= 5 { 41 | // reduce search window 42 | alpha = prevEval - windowSize 43 | beta = prevEval + windowSize 44 | } 45 | 46 | for { 47 | if search.shouldStop() { 48 | // some search limit has been breached 49 | // the return value doesn't matter since this search's result 50 | // will be trashed and the previous iteration's pv will be used 51 | return 0, move.Variation{} 52 | } 53 | 54 | var pv move.Variation 55 | result := search.negamax(0, depth, alpha, beta, &pv) 56 | 57 | switch { 58 | // result <= alpha: search failed low 59 | case result <= alpha: 60 | beta = (alpha + beta) / 2 61 | alpha = util.Max(alpha-windowSize, -eval.Inf) 62 | 63 | // reset reduced depth 64 | depth = initialDepth 65 | 66 | // result >= beta: search failed high 67 | case result >= beta: 68 | beta = util.Min(beta+windowSize, eval.Inf) 69 | 70 | // unless we are mating, reduce depth 71 | if util.Abs(result) <= eval.Inf/2 && 72 | depth > 1 { // don't reduce too much 73 | depth-- 74 | } 75 | 76 | // exact score is inside bounds 77 | default: 78 | // return exact score 79 | return result, pv 80 | } 81 | 82 | // score out of bounds, research needed 83 | 84 | // increase window size 85 | windowSize += windowSize / 2 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/board/move/ordered.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package move 15 | 16 | // the following core types may represent move evaluations 17 | // uint64 is excluded to prevent overflows during storage 18 | type eval interface { 19 | ~int | ~int8 | ~int16 | ~int32 | 20 | ~uint | ~uint8 | ~uint16 | ~uint32 21 | } 22 | 23 | // ScoreMoves scores each move in the provided move-list according to the 24 | // provided scorer function and returns an OrderedMoveList containing them. 25 | func ScoreMoves[T eval](moveList []Move, scorer func(Move) T) OrderedList[T] { 26 | ordered := make([]Ordered[T], len(moveList)) 27 | 28 | for i, move := range moveList { 29 | ordered[i] = NewOrdered(move, scorer(move)) 30 | } 31 | 32 | return OrderedList[T]{ 33 | moves: ordered, 34 | Length: len(moveList), 35 | } 36 | } 37 | 38 | // OrderedList represents an ordered/ranked move list. 39 | type OrderedList[T eval] struct { 40 | moves []Ordered[T] // moves will be sorted later 41 | Length int // number of moves in move-list 42 | } 43 | 44 | // PickMove finds the best move (move with the highest eval) from the 45 | // unsorted moves and puts it at the index position. 46 | func (list *OrderedList[T]) PickMove(index int) Move { 47 | // perform a single selection sort iteration 48 | // the full array is not sorted as most of the moves 49 | // will not be searched due to alpha-beta pruning 50 | 51 | bestIndex := index 52 | bestScore := list.moves[index].Eval() 53 | 54 | for i := index + 1; i < list.Length; i++ { 55 | if eval := list.moves[i].Eval(); eval > bestScore { 56 | bestIndex = i 57 | bestScore = eval 58 | } 59 | } 60 | 61 | list.swap(index, bestIndex) 62 | 63 | return list.moves[index].Move() 64 | } 65 | 66 | func (list *OrderedList[T]) swap(i, j int) { 67 | list.moves[i], list.moves[j] = list.moves[j], list.moves[i] 68 | } 69 | 70 | // NewOrdered creates a new ordered move with the provided move and 71 | // evaluation. The evaluation's type should belong to eval. 72 | func NewOrdered[T eval](m Move, eval T) Ordered[T] { 73 | // [ evaluation 32 bits ] [ move 32 bits ] 74 | return Ordered[T](uint64(eval)<<32 | uint64(m)) 75 | } 76 | 77 | // An Ordered represents a move that can be ranked in a move-list. 78 | type Ordered[T eval] uint64 79 | 80 | // Eval returns the OrderedMove's eval. 81 | func (m Ordered[T]) Eval() T { 82 | return T(m >> 32) 83 | } 84 | 85 | // Move returns the OrderedMove's move. 86 | func (m Ordered[T]) Move() Move { 87 | return Move(m & 0xFFFFFFFF) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/search/eval/evaluation.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package eval contains various types and functions related to evaluating 15 | // a chess position. It is used by search to determine how good a position 16 | // is and whether the moves leading to it should be played or not. 17 | package eval 18 | 19 | import ( 20 | "fmt" 21 | "math" 22 | 23 | "laptudirm.com/x/mess/pkg/board" 24 | "laptudirm.com/x/mess/pkg/board/piece" 25 | ) 26 | 27 | // EfficientlyUpdatable is an extension of board.EfficientlyUpdatable 28 | // which represents an efficiently updatable evaluation function. 29 | type EfficientlyUpdatable interface { 30 | // eval.EfficientlyUpdatable implements board.EfficientlyUpdatable 31 | // so that it can be efficiently updatable by a board.Board 32 | board.EfficientlyUpdatable 33 | 34 | // Accumulate the efficiently updatable variables and return the 35 | // evaluation of the position from the perspective of the given 36 | // color. 37 | Accumulate(piece.Color) Eval 38 | } 39 | 40 | // MatedIn returns the evaluation for being mated in the given plys. 41 | func MatedIn(plys int) Eval { 42 | // prefer the longer lines when getting mated 43 | // so longer lines have higher scores (+plys) 44 | return -Mate + Eval(plys) 45 | } 46 | 47 | // RandDraw returns a random draw score based on the provided seed. 48 | func RandDraw(seed int) Eval { 49 | return Eval(8 - (seed & 7)) 50 | } 51 | 52 | // Eval represents a relative centipawn evaluation where > 0 is better for 53 | // the side to move, while < 0 is better for the other side. 54 | type Eval int32 55 | 56 | // constants representing useful relative evaluations 57 | const ( 58 | // basic evaluations 59 | Inf Eval = math.MaxInt32 / 2 // prevent any overflows 60 | Mate Eval = Inf - 1 // Inf is king capture 61 | Draw Eval = 0 62 | 63 | // limits to differentiate between regular and mate in n evaluations 64 | WinInMaxPly Eval = Mate - 2*10000 65 | LoseInMaxPly Eval = -WinInMaxPly 66 | ) 67 | 68 | // String returns an UCI compliant string representation of the Eval. 69 | func (r Eval) String() string { 70 | switch { 71 | // mate x 72 | case r > WinInMaxPly: 73 | plys := Mate - r 74 | mateInN := (plys / 2) + (plys % 2) 75 | return fmt.Sprintf("mate %d", mateInN) 76 | 77 | // mate -x 78 | case r < LoseInMaxPly: 79 | plys := -Mate - r 80 | mateInN := (plys / 2) + (plys % 2) 81 | return fmt.Sprintf("mate %d", mateInN) 82 | 83 | // cp x 84 | default: 85 | return fmt.Sprintf("cp %d", r) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /pkg/search/eval/move.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package eval 15 | 16 | import ( 17 | "laptudirm.com/x/mess/pkg/board/mailbox" 18 | "laptudirm.com/x/mess/pkg/board/move" 19 | "laptudirm.com/x/mess/pkg/board/piece" 20 | "laptudirm.com/x/mess/pkg/board/square" 21 | ) 22 | 23 | // MoveFunc represents a move evaluation function. 24 | type MoveFunc func(move.Move) Move 25 | 26 | // Move represents the evaluation of a move. 27 | type Move int32 28 | 29 | // constants representing move evaluations 30 | const ( 31 | PVMove Move = 2000000000 32 | 33 | MvvLvaOffset Move = 1900000000 34 | 35 | KillerMove1 Move = 1820000000 36 | KillerMove2 Move = 1810000000 37 | ) 38 | 39 | // MvvLva table taken from Blunder 40 | // TODO: get better scores; may be redundant after see 41 | // score = MvvLvaOffset + MvvLva[victim][attacker] 42 | var MvvLva = [piece.TypeN][piece.TypeN]Move{ 43 | // No piece (-) column is used as promotion scores 44 | // Attackers: - P N B R Q K 45 | piece.Pawn: {16, 15, 14, 13, 12, 11, 10}, 46 | piece.Knight: {26, 25, 24, 23, 22, 21, 20}, 47 | piece.Bishop: {36, 35, 34, 33, 32, 31, 30}, 48 | piece.Rook: {46, 45, 44, 43, 42, 41, 40}, 49 | piece.Queen: {56, 55, 54, 53, 52, 51, 50}, 50 | } 51 | 52 | // OfMove is a move evaluation function which returns a move func which can 53 | // be used for ordering moves. It takes the position and pv move as input. 54 | func OfMove(info ModeEvalInfo) MoveFunc { 55 | return func(m move.Move) Move { 56 | switch { 57 | case m == info.PVMove: 58 | // pv move from previous iteration is most likely 59 | // to be the best move in the position 60 | return PVMove 61 | 62 | // captures and promotions 63 | case m.IsCapture(), m.IsPromotion(): 64 | victim := info.Board[m.Target()].Type() 65 | attacker := m.FromPiece().Type() // piece.NoType for promotions 66 | 67 | // a less valuable piece capturing a more valuable 68 | // piece is very likely to be a good move 69 | return MvvLvaOffset + MvvLva[victim][attacker] 70 | 71 | // killer moves 72 | case m == info.Killers[0]: 73 | return KillerMove1 74 | case m == info.Killers[1]: 75 | return KillerMove2 76 | 77 | default: 78 | return info.History[m.Source()][m.Target()] 79 | } 80 | } 81 | } 82 | 83 | // MoveEvalInfo stores the various search specific information which are 84 | // required by the move ordering function. 85 | type ModeEvalInfo struct { 86 | Board *mailbox.Board 87 | PVMove move.Move 88 | 89 | Killers [2]move.Move 90 | History *[square.N][square.N]Move 91 | } 92 | -------------------------------------------------------------------------------- /pkg/board/fen.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package board 15 | 16 | import ( 17 | "strconv" 18 | "strings" 19 | 20 | "laptudirm.com/x/mess/pkg/board/move/castling" 21 | "laptudirm.com/x/mess/pkg/board/piece" 22 | "laptudirm.com/x/mess/pkg/board/square" 23 | "laptudirm.com/x/mess/pkg/board/zobrist" 24 | "laptudirm.com/x/mess/pkg/formats/fen" 25 | ) 26 | 27 | var StartFEN = fen.FromString("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") 28 | 29 | // NewBoard creates an instance of a *Board from a given fen string. 30 | // https://www.chessprogramming.org/Forsyth-Edwards_Notation 31 | func (board *Board) UpdateWithFEN(fen fen.String) { 32 | // empty board 33 | for s := square.A8; s <= square.H1; s++ { 34 | board.ClearSquare(s) 35 | } 36 | 37 | // reset some stuff 38 | board.Plys = 0 39 | board.Hash = 0 40 | 41 | // side to move 42 | board.SideToMove = piece.NewColor(fen[1]) 43 | if board.SideToMove == piece.Black { 44 | board.Hash ^= zobrist.SideToMove 45 | } 46 | 47 | // generate position 48 | ranks := strings.Split(fen[0], "/") 49 | for rankId, rankData := range ranks { 50 | fileId := square.FileA 51 | for _, id := range rankData { 52 | s := square.New(fileId, square.Rank(rankId)) 53 | 54 | if id >= '1' && id <= '8' { 55 | skip := square.File(id - 48) // ascii value to number 56 | fileId += skip // skip over squares 57 | continue 58 | } 59 | 60 | // piece string to piece 61 | p := piece.NewFromString(string(id)) 62 | 63 | if p.Type() != piece.NoType { 64 | board.FillSquare(s, p) 65 | } 66 | 67 | fileId++ 68 | } 69 | } 70 | 71 | // castling rights 72 | board.CastlingRights = castling.NewRights(fen[2]) 73 | board.Hash ^= zobrist.Castling[board.CastlingRights] 74 | 75 | // en-passant target square 76 | board.EnPassantTarget = square.NewFromString(fen[3]) 77 | if board.EnPassantTarget != square.None { 78 | board.Hash ^= zobrist.EnPassant[board.EnPassantTarget.File()] 79 | } 80 | 81 | // move counters 82 | board.DrawClock, _ = strconv.Atoi(fen[4]) 83 | board.FullMoves, _ = strconv.Atoi(fen[5]) 84 | } 85 | 86 | // FEN returns the fen string of the current Board position. 87 | func (b *Board) FEN() fen.String { 88 | var fenString fen.String 89 | fenString[0] = b.Position.FEN() 90 | fenString[1] = b.SideToMove.String() 91 | fenString[2] = b.CastlingRights.String() 92 | fenString[3] = b.EnPassantTarget.String() 93 | fenString[4] = strconv.Itoa(b.DrawClock) 94 | fenString[5] = strconv.Itoa(b.FullMoves) 95 | return fenString 96 | } 97 | -------------------------------------------------------------------------------- /internal/engine/cmd/position.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | "errors" 18 | 19 | "laptudirm.com/x/mess/internal/engine/context" 20 | "laptudirm.com/x/mess/pkg/board" 21 | "laptudirm.com/x/mess/pkg/formats/fen" 22 | "laptudirm.com/x/mess/pkg/uci/cmd" 23 | "laptudirm.com/x/mess/pkg/uci/flag" 24 | ) 25 | 26 | // UCI command position [ fen | startpos ] moves ... 27 | // 28 | // Set up the position described in fenstring on the internal board and 29 | // play the moves on the internal chess board. 30 | // 31 | // If the game was played from the start position the string startpos will 32 | // be sent 33 | // 34 | // Note: no "new" command is needed. However, if this position is from a 35 | // different game than the last position sent to the engine, the GUI should 36 | // have sent a ucinewgame in-between. 37 | func NewPosition(engine *context.Engine) cmd.Command { 38 | schema := flag.NewSchema() 39 | 40 | // base position 41 | schema.Array("fen", len(board.StartFEN)) 42 | schema.Button("startpos") 43 | 44 | // moves played on base position 45 | schema.Variadic("moves") 46 | 47 | return cmd.Command{ 48 | Name: "position", 49 | Run: func(interaction cmd.Interaction) error { 50 | // parse flags into a board.Board 51 | fen, moves, err := parsePositionFlags(interaction.Values) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | // update search board 57 | engine.Search.UpdatePosition(fen) 58 | engine.Search.MakeMoves(moves...) 59 | 60 | return nil 61 | }, 62 | Flags: schema, 63 | } 64 | } 65 | 66 | // parsePositionFlags parses the position data from the given flags. 67 | func parsePositionFlags(values flag.Values) (fen.String, []string, error) { 68 | var fenString fen.String 69 | 70 | // parse base position 71 | switch { 72 | // only one of the base position descriptors should be set 73 | case values["startpos"].Set && values["fen"].Set: 74 | return board.StartFEN, nil, errors.New("position: both startpos and fen flags found") 75 | 76 | case values["startpos"].Set: 77 | fenString = board.StartFEN 78 | 79 | case values["fen"].Set: 80 | // parse fen string for base position 81 | fenString = fen.FromSlice(values["fen"].Value.([]string)) 82 | 83 | default: 84 | // one of fen or startpos have to be there 85 | return board.StartFEN, nil, errors.New("position: no startpos or fen option") 86 | } 87 | 88 | if values["moves"].Set { 89 | return fenString, values["moves"].Value.([]string), nil 90 | } 91 | 92 | return fenString, nil, nil 93 | } 94 | -------------------------------------------------------------------------------- /pkg/board/move/attacks/attack.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package attacks 15 | 16 | //go:generate go run laptudirm.com/x/mess/internal/generator/attack 17 | 18 | import ( 19 | "laptudirm.com/x/mess/pkg/board/bitboard" 20 | "laptudirm.com/x/mess/pkg/board/piece" 21 | "laptudirm.com/x/mess/pkg/board/square" 22 | ) 23 | 24 | // Of returns the attack set of the given piece on the given square 25 | // and with the given blocker set. The blocker set is unused while 26 | // calculating the attacks sets of non-sliding pieces. 27 | func Of(p piece.Piece, s square.Square, blockers bitboard.Board) bitboard.Board { 28 | switch p.Type() { 29 | case piece.Pawn: 30 | return Pawn[p.Color()][s] 31 | case piece.Knight: 32 | return Knight[s] 33 | case piece.Bishop: 34 | return Bishop(s, blockers) 35 | case piece.Rook: 36 | return Rook(s, blockers) 37 | case piece.Queen: 38 | return Queen(s, blockers) 39 | case piece.King: 40 | return King[s] 41 | default: 42 | panic("attacks.Of: unknown piece type") 43 | } 44 | } 45 | 46 | // PawnPush gives the result after pushing every pawn in the given BB. 47 | func PawnPush(pawns bitboard.Board, color piece.Color) bitboard.Board { 48 | return pawns.Up(color) 49 | } 50 | 51 | func Pawns(pawns bitboard.Board, color piece.Color) bitboard.Board { 52 | return PawnsLeft(pawns, color) | PawnsRight(pawns, color) 53 | } 54 | 55 | // PawnsLeft gives the result after every pawn captures left in the given BB. 56 | func PawnsLeft(pawns bitboard.Board, color piece.Color) bitboard.Board { 57 | return pawns.Up(color).West() 58 | } 59 | 60 | // PawnsRight gives the result after every pawn captures right in the given BB. 61 | func PawnsRight(pawns bitboard.Board, color piece.Color) bitboard.Board { 62 | return pawns.Up(color).East() 63 | } 64 | 65 | // Bishop returns the attack set for a bishop on the given square and with 66 | // the given blocker set(occupied squares). 67 | func Bishop(s square.Square, blockers bitboard.Board) bitboard.Board { 68 | return bishopTable.Probe(s, blockers) 69 | } 70 | 71 | // Rook returns the attack set for a rook on the given square and with 72 | // the given blocker set(occupied squares). 73 | func Rook(s square.Square, blockers bitboard.Board) bitboard.Board { 74 | return rookTable.Probe(s, blockers) 75 | } 76 | 77 | // Queen returns the attack set for a queen on the given square and with 78 | // the given blocker set(occupied squares). It is calculated as the union 79 | // of the attack sets of a bishop and a rook on the given square. 80 | func Queen(s square.Square, occ bitboard.Board) bitboard.Board { 81 | return Rook(s, occ) | Bishop(s, occ) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/board/square/square.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package square declares constants representing every square on a 15 | // chessboard, and related utility functions. 16 | // 17 | // Squares are represented using the algebraic notation. 18 | // https://www.chessprogramming.org/Algebraic_Chess_Notation 19 | // The null square is represented using the "-" symbol. 20 | package square 21 | 22 | // NewFromString creates a new instance of a Square from the given identifier. 23 | func NewFromString(id string) Square { 24 | switch { 25 | case id == "-": 26 | return None 27 | case len(id) != 2: 28 | panic("new square: invalid square id") 29 | } 30 | 31 | // ascii code to square index 32 | return New(FileFrom(string(id[0])), RankFrom(string(id[1]))) 33 | } 34 | 35 | // New creates a new instance of a Square from the given file and rank. 36 | func New(file File, rank Rank) Square { 37 | return Square(int(rank)<<3 | int(file)) 38 | } 39 | 40 | // Square represents a square on a chessboard. 41 | type Square int8 42 | 43 | // constants representing various squares. 44 | const ( 45 | None Square = -1 46 | 47 | A8, B8, C8, D8, E8, F8, G8, H8 Square = +0, +1, +2, +3, +4, +5, +6, +7 48 | A7, B7, C7, D7, E7, F7, G7, H7 Square = +8, +9, 10, 11, 12, 13, 14, 15 49 | A6, B6, C6, D6, E6, F6, G6, H6 Square = 16, 17, 18, 19, 20, 21, 22, 23 50 | A5, B5, C5, D5, E5, F5, G5, H5 Square = 24, 25, 26, 27, 28, 29, 30, 31 51 | A4, B4, C4, D4, E4, F4, G4, H4 Square = 32, 33, 34, 35, 36, 37, 38, 39 52 | A3, B3, C3, D3, E3, F3, G3, H3 Square = 40, 41, 42, 43, 44, 45, 46, 47 53 | A2, B2, C2, D2, E2, F2, G2, H2 Square = 48, 49, 50, 51, 52, 53, 54, 55 54 | A1, B1, C1, D1, E1, F1, G1, H1 Square = 56, 57, 58, 59, 60, 61, 62, 63 55 | ) 56 | 57 | // Number of squares 58 | const N = 64 59 | 60 | // String converts a square into it's algebraic string representation. 61 | func (s Square) String() string { 62 | if s == None { 63 | return "-" 64 | } 65 | 66 | // 67 | return s.File().String() + s.Rank().String() 68 | } 69 | 70 | // File returns the file of the given square. 71 | func (s Square) File() File { 72 | return File(s) & 7 73 | } 74 | 75 | // Rank returns the rank of the given square. 76 | func (s Square) Rank() Rank { 77 | return Rank(s) >> 3 78 | } 79 | 80 | // Diagonal returns the NE-SW diagonal of the given square. 81 | func (s Square) Diagonal() Diagonal { 82 | return 14 - Diagonal(s.Rank()) - Diagonal(s.File()) 83 | } 84 | 85 | // AntiDiagonal returns the NW-SE anti-diagonal of the given square. 86 | func (s Square) AntiDiagonal() AntiDiagonal { 87 | return 7 - AntiDiagonal(s.Rank()) + AntiDiagonal(s.File()) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/search/manager.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package search 15 | 16 | import ( 17 | "time" 18 | 19 | "laptudirm.com/x/mess/internal/util" 20 | "laptudirm.com/x/mess/pkg/board/piece" 21 | ) 22 | 23 | // Manager represents a time manager. 24 | type TimeManager interface { 25 | // GetDeadline calculates the optimal amount of time to be used 26 | // and sets a deadline internally for the search's end. 27 | GetDeadline() 28 | 29 | // ExtendDeadline is called when the engine want's to extend the 30 | // search's length. A deadline extension may fail. 31 | ExtendDeadline() 32 | 33 | // PessimisticExpired reports if the search deadline has been crossed. 34 | PessimisticExpired() bool 35 | OptimisticExpired() bool 36 | } 37 | 38 | // NormalManager is the standard time manager which uses the wtime, btime, 39 | // winc, binc, and movestogo provided by the GUI to calculate the optimal 40 | // search time. 41 | type TimeManagerNormal struct { 42 | Us piece.Color // side to move 43 | 44 | Time, Increment [piece.ColorN]int 45 | MovesToGo int // moves to next time control 46 | 47 | deadline time.Time // search end deadline 48 | maxUsage time.Time 49 | 50 | context *Context 51 | } 52 | 53 | // compile time check that NormalManager implements Manager 54 | var _ TimeManager = (*TimeManagerNormal)(nil) 55 | 56 | func (c *TimeManagerNormal) GetDeadline() { 57 | if c.MovesToGo == 0 { 58 | c.MovesToGo = util.Max(20, 50-(c.context.board.Plys/2)) 59 | } 60 | 61 | maximum := time.Duration(c.Time[c.Us]) * time.Millisecond 62 | 63 | c.deadline = time.Now().Add(maximum / time.Duration(c.MovesToGo)) 64 | c.maxUsage = time.Now().Add(maximum / 2) 65 | } 66 | 67 | func (c *TimeManagerNormal) ExtendDeadline() { 68 | c.deadline = c.deadline.Add((time.Duration(c.Time[c.Us]) * time.Millisecond) / 30) 69 | } 70 | 71 | func (c *TimeManagerNormal) PessimisticExpired() bool { 72 | return time.Now().After(c.maxUsage) 73 | } 74 | 75 | func (c *TimeManagerNormal) OptimisticExpired() bool { 76 | return time.Now().After(c.deadline) 77 | } 78 | 79 | // MoveManger is the time manager used when the gui wants to time a search 80 | // by move-time. Extending it's deadline is not possible. 81 | type TimeManagerMovetime struct { 82 | deadline time.Time 83 | Duration int 84 | } 85 | 86 | // compile time check that MoveManager implements Manager 87 | var _ TimeManager = (*TimeManagerMovetime)(nil) 88 | 89 | func (c *TimeManagerMovetime) GetDeadline() { 90 | c.deadline = time.Now().Add(time.Duration(c.Duration) * time.Millisecond) 91 | } 92 | 93 | func (c *TimeManagerMovetime) ExtendDeadline() { 94 | // can't extend deadline: search time is fixed 95 | } 96 | 97 | func (c *TimeManagerMovetime) PessimisticExpired() bool { 98 | return time.Now().After(c.deadline) 99 | } 100 | 101 | func (c *TimeManagerMovetime) OptimisticExpired() bool { 102 | return time.Now().After(c.deadline) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/uci/cmd/command.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package command implements type representing an UCI command, whose 15 | // details and working can be specified by an engine. 16 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | "io" 21 | 22 | "laptudirm.com/x/mess/pkg/uci/flag" 23 | ) 24 | 25 | // NewSchema initializes a new command schema. 26 | func NewSchema(replyWriter io.Writer) Schema { 27 | return Schema{ 28 | replyWriter: replyWriter, 29 | commands: make(map[string]Command), 30 | } 31 | } 32 | 33 | // Schema contains a command schema for a client. 34 | type Schema struct { 35 | replyWriter io.Writer 36 | commands map[string]Command 37 | } 38 | 39 | // Add adds the given command to the Schema. 40 | func (l *Schema) Add(c Command) { 41 | l.commands[c.Name] = c 42 | } 43 | 44 | // Get tries to find a command with the given name from the Schema. If it 45 | // succeeds, it returns and command and true, otherwise, it returns 46 | // something undefined along with false. 47 | func (l *Schema) Get(name string) (Command, bool) { 48 | cmd, found := l.commands[name] 49 | return cmd, found 50 | } 51 | 52 | // Command represents the schema of a GUI to Engine command. 53 | type Command struct { 54 | // name of the command 55 | // this is used as a token to identify if this command has been run 56 | Name string 57 | 58 | // Run is the actual work function for the command. It is provided 59 | // with an interaction which contains the relevant information 60 | // about the command interaction by the GUI. 61 | Run func(Interaction) error 62 | 63 | // Flags contains the flag schema of this command. The flags the 64 | // parsed from the provided args before the Run function is called. 65 | Flags flag.Schema 66 | } 67 | 68 | // RunWith runs the given Command with the given flags and Schema. 69 | func (c Command) RunWith(args []string, parallelize bool, schema Schema) error { 70 | values, err := c.Flags.Parse(args) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | return c.Run(Interaction{ 76 | stdout: schema.replyWriter, 77 | Command: c, 78 | 79 | Parallelize: parallelize, 80 | Values: values, 81 | }) 82 | } 83 | 84 | // Interaction encapsulates relevant information about a Command sent to 85 | // the Engine by the GUI. 86 | type Interaction struct { 87 | stdout io.Writer 88 | 89 | Command // parent Command 90 | 91 | Parallelize bool // whether to run command in parallel 92 | 93 | // values provided for the command's flags 94 | Values flag.Values 95 | } 96 | 97 | // Reply writes to the GUI's input. It is similar to fmt.Println. 98 | func (i *Interaction) Reply(a ...any) { 99 | fmt.Fprintln(i.stdout, a...) 100 | } 101 | 102 | // Replyf writes to the GUI's input. It is similar to fmt.Printf with 103 | // a newline terminator. 104 | func (i *Interaction) Replyf(format string, a ...any) { 105 | fmt.Fprintf(i.stdout, format+"\n", a...) 106 | } 107 | -------------------------------------------------------------------------------- /pkg/board/bitboard/bitboard.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package bitboard implements a 64-bit bitboard and other related 15 | // functions for manipulating them. 16 | package bitboard 17 | 18 | import ( 19 | "math/bits" 20 | 21 | "laptudirm.com/x/mess/pkg/board/piece" 22 | "laptudirm.com/x/mess/pkg/board/square" 23 | ) 24 | 25 | func Square(s square.Square) Board { 26 | return Board(1 << s) 27 | } 28 | 29 | // Board is a 64-bit bitboard 30 | type Board uint64 31 | 32 | // String returns a string representation of the given BB. 33 | func (b Board) String() string { 34 | var str string 35 | for s := square.A8; s <= square.H1; s++ { 36 | if b.IsSet(s) { 37 | str += "1" 38 | } else { 39 | str += "0" 40 | } 41 | 42 | if s.File() == square.FileH { 43 | str += "\n" 44 | } else { 45 | str += " " 46 | } 47 | } 48 | 49 | return str 50 | } 51 | 52 | // Up shifts the given BB up relative to the given color. 53 | func (b Board) Up(c piece.Color) Board { 54 | switch c { 55 | case piece.White: 56 | return b.North() 57 | case piece.Black: 58 | return b.South() 59 | default: 60 | panic("bad color") 61 | } 62 | } 63 | 64 | // Down shifts the given BB down relative to the given color. 65 | func (b Board) Down(color piece.Color) Board { 66 | switch color { 67 | case piece.White: 68 | return b.South() 69 | case piece.Black: 70 | return b.North() 71 | default: 72 | panic("bad color") 73 | } 74 | } 75 | 76 | // North shifts the given BB to the north. 77 | func (b Board) North() Board { 78 | return b >> 8 79 | } 80 | 81 | // South shifts the given BB to the south. 82 | func (b Board) South() Board { 83 | return b << 8 84 | } 85 | 86 | // East shifts the given BB to the east. 87 | func (b Board) East() Board { 88 | return (b &^ FileH) << 1 89 | } 90 | 91 | // West shifts the given BB to the west. 92 | func (b Board) West() Board { 93 | return (b &^ FileA) >> 1 94 | } 95 | 96 | // Pop returns the LSB of the given BB and removes it. 97 | func (b *Board) Pop() square.Square { 98 | sq := b.FirstOne() 99 | *b &= *b - 1 100 | return sq 101 | } 102 | 103 | // Count returns the number of set bits in the given BB. 104 | func (b Board) Count() int { 105 | return bits.OnesCount64(uint64(b)) 106 | } 107 | 108 | // FirstOne returns the LSB of the given BB. 109 | func (b Board) FirstOne() square.Square { 110 | return square.Square(bits.TrailingZeros64(uint64(b))) 111 | } 112 | 113 | // IsSet checks whether the given Square is set in the bitboard. 114 | func (b Board) IsSet(index square.Square) bool { 115 | return b&Square(index) != 0 116 | } 117 | 118 | // Set sets the given Square in the bitboard. 119 | func (b *Board) Set(index square.Square) { 120 | if index == square.None { 121 | return 122 | } 123 | 124 | *b |= Square(index) 125 | } 126 | 127 | // Unset clears the given Square in the bitboard. 128 | func (b *Board) Unset(index square.Square) { 129 | if index == square.None { 130 | return 131 | } 132 | 133 | *b &^= Square(index) 134 | } 135 | -------------------------------------------------------------------------------- /pkg/search/quiescence.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package search 15 | 16 | import ( 17 | "laptudirm.com/x/mess/internal/util" 18 | "laptudirm.com/x/mess/pkg/board/move" 19 | "laptudirm.com/x/mess/pkg/search/eval" 20 | "laptudirm.com/x/mess/pkg/search/tt" 21 | ) 22 | 23 | // quiescence search is a type of limited search which only evaluates 'quiet' 24 | // positions, i.e. positions with no tactical moves like captures or promotions. 25 | // This search is needed to avoid the horizon effect. 26 | // https://www.chessprogramming.org/Quiescence_Search 27 | func (search *Context) quiescence(plys int, alpha, beta eval.Eval) eval.Eval { 28 | // quick exit clauses 29 | switch { 30 | case search.shouldStop(): 31 | return 0 // return value doesn't matter 32 | 33 | case search.board.DrawClock >= 100, 34 | search.board.IsInsufficientMaterial(), 35 | search.board.IsRepetition(): 36 | return search.draw() 37 | 38 | case plys >= MaxDepth: 39 | return search.score() 40 | } 41 | 42 | // check for transposition table hits 43 | if entry, hit := search.tt.Probe(search.board.Hash); hit { 44 | search.stats.TTHits++ 45 | 46 | // check if the tt entry can be used to exit the search early 47 | // on this node. If we have an exact value, we can safely 48 | // return it. If we have a new upper bound or lower bound, 49 | // check if it causes a beta cutoff. 50 | switch value := entry.Value.Eval(plys); { 51 | case entry.Type == tt.ExactEntry, // exact score 52 | entry.Type == tt.LowerBound && value >= beta, // fail high 53 | entry.Type == tt.UpperBound && alpha >= value: // fail high 54 | // exit search early cause we have an exact 55 | // score or a beta cutoff from the tt entry 56 | return value 57 | } 58 | } 59 | 60 | bestScore := search.score() // standing pat 61 | if bestScore >= beta { 62 | return bestScore // fail high 63 | } 64 | 65 | alpha = util.Max(alpha, bestScore) 66 | 67 | // generate tactical (captures and promotions) moves only 68 | moves := search.board.GenerateMoves(true) 69 | 70 | // move ordering 71 | list := move.ScoreMoves(moves, eval.OfMove(eval.ModeEvalInfo{ 72 | Board: &search.board.Position, 73 | Killers: search.killers[plys], 74 | })) 75 | 76 | for i := 0; i < list.Length; i++ { 77 | m := list.PickMove(i) 78 | 79 | // node amount updates are done here to prevent duplicates 80 | // when quiescence search is called from the negamax function. 81 | // In other words, node amount updates for a quiescence search 82 | // is done by the caller function, which in this case is the 83 | // quiescence search itself. 84 | search.stats.Nodes++ 85 | 86 | search.board.MakeMove(m) 87 | score := -search.quiescence(plys+1, -beta, -alpha) 88 | search.board.UnmakeMove() 89 | 90 | // update score and bounds 91 | if score > bestScore { 92 | // better move found 93 | bestScore = score 94 | 95 | // check if move is new pv move 96 | if score > alpha { 97 | // new pv so alpha increases 98 | alpha = score 99 | 100 | if alpha >= beta { 101 | break // fail high 102 | } 103 | } 104 | } 105 | } 106 | 107 | if !search.stopped { 108 | // update transposition table 109 | search.tt.Store(tt.Entry{ 110 | Hash: search.board.Hash, 111 | Value: tt.EvalFrom(bestScore, plys), 112 | Depth: 0, 113 | Type: tt.ExactEntry, 114 | }) 115 | } 116 | 117 | return bestScore 118 | } 119 | -------------------------------------------------------------------------------- /pkg/uci/uci.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package uci 15 | 16 | import ( 17 | "bufio" 18 | "fmt" 19 | "io" 20 | "os" 21 | "strings" 22 | 23 | "laptudirm.com/x/mess/pkg/uci/cmd" 24 | ) 25 | 26 | // NewClient creates a new uci.Client which is listening to the stdin for 27 | // commands and with the default isready and quit commands added. 28 | func NewClient() Client { 29 | client := Client{ 30 | // communication streams 31 | stdin: os.Stdin, 32 | stdout: os.Stdout, 33 | } 34 | 35 | client.commands = cmd.NewSchema(client.stdout) 36 | 37 | // add default commands 38 | client.AddCommand(cmdQuit) 39 | client.AddCommand(cmdIsReady) 40 | 41 | return client 42 | } 43 | 44 | // Client represents an UCI client. 45 | type Client struct { 46 | stdin io.Reader // GUI to Engine commands 47 | stdout io.Writer // Engine to GUI commands 48 | 49 | commands cmd.Schema // commands schema 50 | } 51 | 52 | // AddCommand adds the given command to the client's schema. 53 | func (c *Client) AddCommand(cmd cmd.Command) { 54 | c.commands.Add(cmd) 55 | } 56 | 57 | // Start starts a repl listening for UCI commands which match the client's 58 | // Schema. It listen's on the Client's stdin. 59 | func (c *Client) Start() error { 60 | reader := bufio.NewReader(c.stdin) 61 | 62 | // read-eval-print loop 63 | for { 64 | // read prompt form client's stdin 65 | prompt, err := reader.ReadString('\n') 66 | if err != nil { 67 | // read errors are probably fatal 68 | return err 69 | } 70 | 71 | // parse arguments from prompt 72 | args := strings.Fields(prompt) 73 | 74 | // since we are in a repl run commands in parallel if needed 75 | switch err := c.RunWith(args, true); err { 76 | case nil: 77 | // no error: continue repl 78 | 79 | case errQuit: 80 | // errQuit is returned by quit command to stop the repl 81 | // so honour the request and return, stopping the repl 82 | return nil 83 | 84 | default: 85 | // non-nil error: print and continue 86 | c.Println(err) 87 | } 88 | } 89 | } 90 | 91 | // Run is a simple utility function which runs the provided arguments as a 92 | // command without parallelization. 93 | func (c *Client) Run(args ...string) error { 94 | return c.RunWith(args, false) 95 | } 96 | 97 | // RunWith finds a command whose name matches the first element of the args 98 | // array, and runs it with the remaining args. It returns any error sent 99 | // by the command. It honours the cmd.Parallel property if parallelize is 100 | // set to true. 101 | func (c *Client) RunWith(args []string, parallelize bool) error { 102 | // separate command name and arguments 103 | name, args := args[0], args[1:] 104 | 105 | // get uci command 106 | cmd, found := c.commands.Get(name) 107 | if !found { 108 | // command with given name not found 109 | return fmt.Errorf("%s: command not found", name) 110 | } 111 | 112 | // run command with given arguments 113 | return cmd.RunWith(args, parallelize, c.commands) 114 | } 115 | 116 | // Print acts as fmt.Print on the client's stdout. 117 | func (c *Client) Print(a ...any) (int, error) { 118 | return fmt.Fprint(c.stdout, a...) 119 | } 120 | 121 | // Printf acts as fmt.Printf on the client's stdout. 122 | func (c *Client) Printf(format string, a ...any) (int, error) { 123 | return fmt.Fprintf(c.stdout, format, a...) 124 | } 125 | 126 | // Println acts as fmt.Println on the client's stdout. 127 | func (c *Client) Println(a ...any) (int, error) { 128 | return fmt.Fprintln(c.stdout, a...) 129 | } 130 | -------------------------------------------------------------------------------- /internal/generator/bitboard/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | _ "embed" 18 | 19 | "laptudirm.com/x/mess/internal/generator" 20 | "laptudirm.com/x/mess/pkg/board/bitboard" 21 | "laptudirm.com/x/mess/pkg/board/move/attacks" 22 | "laptudirm.com/x/mess/pkg/board/piece" 23 | "laptudirm.com/x/mess/pkg/board/square" 24 | ) 25 | 26 | type bitboardStruct struct { 27 | Between [square.N][square.N]bitboard.Board 28 | 29 | KingAreas [piece.ColorN][square.N]bitboard.Board 30 | 31 | AdjacentFiles [square.FileN]bitboard.Board 32 | 33 | PassedPawnMask [piece.ColorN][square.N]bitboard.Board 34 | 35 | ForwardFileMask [piece.ColorN][square.N]bitboard.Board 36 | ForwardRanksMask [piece.ColorN][square.RankN]bitboard.Board 37 | } 38 | 39 | //go:embed .gotemplate 40 | var template string 41 | 42 | func main() { 43 | var b bitboardStruct 44 | 45 | // initialize Between 46 | for s1 := square.A8; s1 <= square.H1; s1++ { 47 | for s2 := square.A8; s2 <= square.H1; s2++ { 48 | sqs := bitboard.Square(s1) | bitboard.Square(s2) 49 | var mask bitboard.Board 50 | 51 | switch { 52 | case s1.File() == s2.File(): 53 | mask = bitboard.Files[s1.File()] 54 | case s1.Rank() == s2.Rank(): 55 | mask = bitboard.Ranks[s1.Rank()] 56 | case s1.Diagonal() == s2.Diagonal(): 57 | mask = bitboard.Diagonals[s1.Diagonal()] 58 | case s1.AntiDiagonal() == s2.AntiDiagonal(): 59 | mask = bitboard.AntiDiagonals[s1.AntiDiagonal()] 60 | default: 61 | // the squares don't have their file, rank, diagonal, or 62 | // anti-diagonal in common, so path is Empty (default). 63 | continue 64 | } 65 | 66 | b.Between[s1][s2] = bitboard.Hyperbola(s1, sqs, mask) & bitboard.Hyperbola(s2, sqs, mask) 67 | } 68 | } 69 | 70 | for s := square.A8; s <= square.H1; s++ { 71 | attacks := attacks.King[s] | bitboard.Square(s) 72 | 73 | b.KingAreas[piece.White][s] = attacks | attacks.North() 74 | b.KingAreas[piece.Black][s] = attacks | attacks.South() 75 | 76 | switch s.File() { 77 | case square.FileA: 78 | b.KingAreas[piece.White][s] |= b.KingAreas[piece.White][s].East() 79 | b.KingAreas[piece.Black][s] |= b.KingAreas[piece.Black][s].East() 80 | case square.FileH: 81 | b.KingAreas[piece.White][s] |= b.KingAreas[piece.White][s].West() 82 | b.KingAreas[piece.Black][s] |= b.KingAreas[piece.Black][s].West() 83 | } 84 | } 85 | 86 | for file := square.File(0); file < square.FileN; file++ { 87 | bb := bitboard.Files[file] 88 | b.AdjacentFiles[file] = bb.East() | bb.West() 89 | } 90 | 91 | for rank := square.Rank(0); rank < square.RankN; rank++ { 92 | for rank2 := rank; rank2 >= 0; rank2-- { 93 | b.ForwardRanksMask[piece.White][rank] |= bitboard.Ranks[rank2] 94 | } 95 | 96 | for rank2 := rank; rank2 < square.RankN; rank2++ { 97 | b.ForwardRanksMask[piece.Black][rank] |= bitboard.Ranks[rank2] 98 | } 99 | } 100 | 101 | for sq := square.Square(0); sq < square.N; sq++ { 102 | b.PassedPawnMask[piece.White][sq] = b.ForwardRanksMask[piece.White][sq.Rank()] & 103 | (b.AdjacentFiles[sq.File()] | bitboard.Files[sq.File()]) 104 | b.PassedPawnMask[piece.Black][sq] = b.ForwardRanksMask[piece.Black][sq.Rank()] & 105 | (b.AdjacentFiles[sq.File()] | bitboard.Files[sq.File()]) 106 | } 107 | 108 | for sq := square.Square(0); sq < square.N; sq++ { 109 | b.ForwardFileMask[piece.White][sq] = bitboard.Files[sq.File()] & 110 | b.ForwardRanksMask[piece.White][sq.Rank()] 111 | b.ForwardFileMask[piece.Black][sq] = bitboard.Files[sq.File()] & 112 | b.ForwardRanksMask[piece.Black][sq.Rank()] 113 | } 114 | 115 | generator.Generate("bitboards", template, b) 116 | } 117 | -------------------------------------------------------------------------------- /pkg/board/move/castling/castling.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package castling provides various types and definitions which are useful 15 | // when dealing with castling moves in a board representation. 16 | package castling 17 | 18 | import "laptudirm.com/x/mess/pkg/board/square" 19 | 20 | // Rights represents the current castling rights of the position. 21 | // [Black Queen-side][Black King-side][White Queen-side][White King-side] 22 | type Rights byte 23 | 24 | // NewRights creates a new castling.Rights from the given string. It 25 | // checks if the identifier for each possible castling is in the string 26 | // in the proper order. 27 | // 28 | // White King-side: K 29 | // White Queen-side: Q 30 | // Black King-side: k 31 | // Black Queen-side: q 32 | // 33 | // The string "-" represents castling.NoCasl. 34 | func NewRights(r string) Rights { 35 | var rights Rights 36 | 37 | if r == "-" { 38 | return NoCasl 39 | } 40 | 41 | if r != "" && r[0] == 'K' { 42 | r = r[1:] 43 | rights |= WhiteK 44 | } 45 | 46 | if r != "" && r[0] == 'Q' { 47 | r = r[1:] 48 | rights |= WhiteQ 49 | } 50 | 51 | if r != "" && r[0] == 'k' { 52 | r = r[1:] 53 | rights |= BlackK 54 | } 55 | 56 | if r != "" && r[0] == 'q' { 57 | rights |= BlackQ 58 | } 59 | 60 | return rights 61 | } 62 | 63 | // Constants representing various castling rights. 64 | const ( 65 | WhiteK Rights = 1 << 0 // white king-side 66 | WhiteQ Rights = 1 << 1 // white queen-side 67 | BlackK Rights = 1 << 2 // black king-side 68 | BlackQ Rights = 1 << 3 // black queen-side 69 | 70 | NoCasl Rights = 0 // no castling possible 71 | 72 | WhiteA Rights = WhiteK | WhiteQ // only white can castle 73 | BlackA Rights = BlackK | BlackQ // only black can castle 74 | 75 | Kingside Rights = WhiteK | BlackK // only king-side castling 76 | Queenside Rights = WhiteQ | BlackQ // only queen-side castling 77 | 78 | All Rights = WhiteA | BlackA // all castling possible 79 | ) 80 | 81 | // N is the number of possible unique castling rights. 82 | const N = 1 << 4 // 4 possible castling sides 83 | 84 | // RightUpdates is a map of each chessboard square to the rights that 85 | // need to be removed if a piece moves from or to that square. For example, 86 | // if a piece moves from or two from the square A1, either the white rook 87 | // has moved or it has been captured, so white can no longer castle 88 | // queen-side. Squares which are not occupied by a king or a rook do not 89 | // effect the castling rights. Squares occupied by a rook remove the 90 | // castling rights of the rook's side and color. Squares occupied by a king 91 | // remove it's color's castling rights. 92 | var RightUpdates = [square.N]Rights{ 93 | BlackQ, NoCasl, NoCasl, NoCasl, BlackA, NoCasl, NoCasl, BlackK, 94 | NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, 95 | NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, 96 | NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, 97 | NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, 98 | NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, 99 | NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, NoCasl, 100 | WhiteQ, NoCasl, NoCasl, NoCasl, WhiteA, NoCasl, NoCasl, WhiteK, 101 | } 102 | 103 | // String converts the given castling.Rights to a readable string. 104 | func (c Rights) String() string { 105 | var str string 106 | 107 | if c&WhiteK != 0 { 108 | str += "K" 109 | } 110 | 111 | if c&WhiteQ != 0 { 112 | str += "Q" 113 | } 114 | 115 | if c&BlackK != 0 { 116 | str += "k" 117 | } 118 | 119 | if c&BlackQ != 0 { 120 | str += "q" 121 | } 122 | 123 | if str == "" { 124 | str = "-" 125 | } 126 | 127 | return str 128 | } 129 | -------------------------------------------------------------------------------- /pkg/search/search.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package search implements various functions used to search a 15 | // position for the best move. 16 | package search 17 | 18 | //go:generate go run laptudirm.com/x/mess/internal/generator/reductions 19 | 20 | import ( 21 | "errors" 22 | realtime "time" 23 | 24 | "laptudirm.com/x/mess/pkg/board" 25 | "laptudirm.com/x/mess/pkg/board/move" 26 | "laptudirm.com/x/mess/pkg/board/piece" 27 | "laptudirm.com/x/mess/pkg/board/square" 28 | "laptudirm.com/x/mess/pkg/search/eval" 29 | "laptudirm.com/x/mess/pkg/search/eval/classical" 30 | "laptudirm.com/x/mess/pkg/search/tt" 31 | ) 32 | 33 | // maximum depth to search to 34 | const MaxDepth = 256 35 | 36 | // NewContext creates a new search Context. 37 | func NewContext(reporter Reporter, ttSize int) *Context { 38 | evaluator := &classical.EfficientlyUpdatable{} 39 | chessboard := board.New(board.EU(evaluator)) 40 | evaluator.Board = chessboard 41 | chessboard.UpdateWithFEN(board.StartFEN) 42 | 43 | return &Context{ 44 | // default position 45 | board: chessboard, 46 | 47 | evaluator: evaluator, 48 | 49 | tt: tt.NewTable(ttSize), 50 | stopped: true, 51 | 52 | reporter: reporter, 53 | } 54 | } 55 | 56 | // Context stores various options, state, and debug variables regarding a 57 | // particular search. During multiple searches on the same position, the 58 | // internal board (*Context).Board should be switched out, while a brand 59 | // new Context should be used for different games. 60 | type Context struct { 61 | // search state 62 | board *board.Board 63 | sideToMove piece.Color 64 | tt *tt.Table 65 | stopped bool 66 | 67 | evaluator eval.EfficientlyUpdatable 68 | 69 | // principal variation 70 | pv move.Variation 71 | pvScore eval.Eval 72 | 73 | // stats 74 | stats Stats 75 | reporter Reporter 76 | 77 | // search limits 78 | time TimeManager 79 | limits Limits 80 | 81 | // move ordering stuff 82 | history [piece.ColorN][square.N][square.N]eval.Move 83 | killers [MaxDepth][2]move.Move 84 | } 85 | 86 | // Search initializes the context for a new search and calls the main 87 | // iterative deepening function. It checks if the position is illegal 88 | // and cleans up the context after the search finishes. 89 | func (search *Context) Search(limits Limits) (move.Variation, eval.Eval, error) { 90 | 91 | search.start(limits) 92 | defer search.Stop() 93 | 94 | // illegal position check; king can be captured 95 | if search.board.IsInCheck(search.board.SideToMove.Other()) { 96 | return move.Variation{}, eval.Inf, errors.New("search move: position is illegal") 97 | } 98 | 99 | pv, eval := search.iterativeDeepening() 100 | return pv, eval, nil 101 | } 102 | 103 | // InProgress reports whether a search is in progress on the given context. 104 | func (search *Context) InProgress() bool { 105 | return !search.stopped 106 | } 107 | 108 | // ResizeTT resizes the search's transposition table. 109 | func (search *Context) ResizeTT(mbs int) { 110 | search.tt.Resize(mbs) 111 | } 112 | 113 | // Stop stops any ongoing search on the given context. The main search 114 | // function will immediately return after this function is called. 115 | func (search *Context) Stop() { 116 | search.stopped = true 117 | } 118 | 119 | // start initializes search variables during the start of a search. 120 | func (search *Context) start(limits Limits) { 121 | // reset principal variation 122 | search.pv.Clear() 123 | 124 | // reset stats 125 | search.stats = Stats{} 126 | search.sideToMove = search.board.SideToMove 127 | 128 | // age the transposition table 129 | search.tt.NextEpoch() 130 | 131 | // start search 132 | search.UpdateLimits(limits) 133 | search.stopped = false // search not stopped 134 | 135 | // start search timer 136 | search.stats.SearchStart = realtime.Now() 137 | } 138 | -------------------------------------------------------------------------------- /pkg/board/piece/piece.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package piece implements representations of all the chess pieces and 15 | // colors, and related utility functions. 16 | // 17 | // The King, Queen, Rook, Knight, Bishop, and Pawn are represented by the 18 | // K, Q, R, N, B, and P strings respectively, with uppercase for white and 19 | // lower case for black. 20 | // 21 | // The strings w, and b are used for representing the White and Black 22 | // colors respectively. 23 | package piece 24 | 25 | // New creates a new Piece with the given type and color. 26 | func New(t Type, c Color) Piece { 27 | return Piece(c<> colorOffset) 114 | } 115 | 116 | // Is checks if the type of the given Piece matches the given type. 117 | func (p Piece) Is(target Type) bool { 118 | return p.Type() == target 119 | } 120 | 121 | // IsColor checks if the color of the given Piece matches the given Color. 122 | func (p Piece) IsColor(target Color) bool { 123 | return p.Color() == target 124 | } 125 | 126 | // Type represents the type/kind of chess piece. 127 | type Type uint8 128 | 129 | // constants representing chess piece types 130 | const ( 131 | NoType Type = iota 132 | Pawn 133 | Knight 134 | Bishop 135 | Rook 136 | Queen 137 | King 138 | ) 139 | 140 | // TypeN is the number of chess piece types, including NoType. 141 | const TypeN = 7 142 | 143 | func (t Type) String() string { 144 | const typeToStr = " pnbrqk" 145 | return string(typeToStr[t]) 146 | } 147 | -------------------------------------------------------------------------------- /pkg/uci/flag/flag.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package flag implements types representing flags provided to UCI 15 | // commands and their values. 16 | package flag 17 | 18 | import ( 19 | "fmt" 20 | ) 21 | 22 | // NewSchema initializes a new flag Schema. 23 | func NewSchema() Schema { 24 | return Schema{ 25 | flags: make(map[string]Flag), 26 | } 27 | } 28 | 29 | // Schema contains the flag schema for a command. 30 | type Schema struct { 31 | // flags maps each flag's name to it's collection function 32 | flags map[string]Flag 33 | } 34 | 35 | // Parse parses the given argument list according to the given flag 36 | // schema. It returns the values for each command and an error. 37 | func (s Schema) Parse(args []string) (Values, error) { 38 | values := make(Values) 39 | 40 | // nil schema 41 | if s.flags == nil { 42 | // no flags should be present if schema is nil 43 | if len(args) > 0 { 44 | return values, fmt.Errorf("parse flags: unknown flag %q", args[0]) 45 | } 46 | 47 | return values, nil 48 | } 49 | 50 | for len(args) > 0 { 51 | name := args[0] 52 | 53 | // check if it is a valid flag 54 | collect, isFlag := s.flags[name] 55 | if !isFlag { 56 | return values, fmt.Errorf("parse flags: unknown flag %q", name) 57 | } 58 | 59 | // check if flag is already set 60 | if values[name].Set { 61 | return values, fmt.Errorf("parse flags: flag %q already set", name) 62 | } 63 | 64 | // collect value from arguments 65 | value, newArgs, err := collect(args[1:]) 66 | if err != nil { 67 | return values, err 68 | } 69 | 70 | // update args after collection 71 | args = newArgs 72 | 73 | // add new value 74 | values[name] = Value{ 75 | Set: true, 76 | Value: value, 77 | } 78 | } 79 | 80 | return values, nil 81 | } 82 | 83 | // Button adds a button flag with the given name to the schema. A button 84 | // flag is a flag without any arguments, it is either set or not set. All 85 | // the values of Button flags are equal to nil. 86 | func (s Schema) Button(name string) { 87 | s.flags[name] = func(args []string) (any, []string, error) { 88 | return nil, args, nil 89 | } 90 | } 91 | 92 | // Single adds a single flag with the given name to the schema. A single 93 | // flag is a flag with a single argument. Values of single flags are of 94 | // type string. 95 | func (s Schema) Single(name string) { 96 | s.flags[name] = func(args []string) (any, []string, error) { 97 | if len(args) == 0 { 98 | return nil, nil, argNumErr(name, 1, 0) 99 | } 100 | 101 | return args[0], args[1:], nil 102 | } 103 | } 104 | 105 | // Array adds an array flag with the given name and argument number to the 106 | // schema. An array flag is a flag with a fixed number of arguments. Values 107 | // of array flags are of type []string. 108 | func (s Schema) Array(name string, argN int) { 109 | s.flags[name] = func(args []string) (any, []string, error) { 110 | value := make([]string, argN) 111 | if collected := copy(value, args); collected != argN { 112 | return nil, nil, argNumErr(name, argN, collected) 113 | } 114 | 115 | return value, args[argN:], nil 116 | } 117 | } 118 | 119 | // Variadic adds a variadic flag with the given name to the schema. A 120 | // variadic flag is a flag which collects all the remaining arguments. 121 | // Values of variadic flags are of type []string. 122 | func (s Schema) Variadic(name string) { 123 | s.flags[name] = func(s []string) (any, []string, error) { 124 | return s, []string{}, nil 125 | } 126 | } 127 | 128 | // Flag represents a flag of an uci command. Flag is a collector function 129 | // which collects it's arguments from the provided list, and return's it's 130 | // value, the remaining arguments, and an error, if any. 131 | type Flag func([]string) (any, []string, error) 132 | 133 | // Values map's each flag's name to it's value in the current interaction. 134 | type Values map[string]Value 135 | 136 | // Value represents the value of a flag. 137 | type Value struct { 138 | // Set stores whether or not this flag was set. 139 | Set bool 140 | 141 | // Value contains the value of the flag. It should be type casted to 142 | // it's proper type before use. See the documentation of the various 143 | // flag's for their value's data types. 144 | Value any 145 | } 146 | 147 | func argNumErr(flag string, expected, collected int) error { 148 | return fmt.Errorf("flag %s: expected %d args, collected %d args", flag, expected, collected) 149 | } 150 | -------------------------------------------------------------------------------- /pkg/board/bitboard/useful.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package bitboard 15 | 16 | import "laptudirm.com/x/mess/pkg/board/square" 17 | 18 | //go:generate go run laptudirm.com/x/mess/internal/generator/bitboard 19 | 20 | // useful bitboard definitions 21 | const ( 22 | Empty Board = 0 23 | Universe Board = 0xffffffffffffffff 24 | ) 25 | 26 | // file bitboards 27 | const ( 28 | FileA Board = 0x0101010101010101 29 | FileB Board = 0x0202020202020202 30 | FileC Board = 0x0404040404040404 31 | FileD Board = 0x0808080808080808 32 | FileE Board = 0x1010101010101010 33 | FileF Board = 0x2020202020202020 34 | FileG Board = 0x4040404040404040 35 | FileH Board = 0x8080808080808080 36 | ) 37 | 38 | var Files = [...]Board{ 39 | square.FileA: FileA, 40 | square.FileB: FileB, 41 | square.FileC: FileC, 42 | square.FileD: FileD, 43 | square.FileE: FileE, 44 | square.FileF: FileF, 45 | square.FileG: FileG, 46 | square.FileH: FileH, 47 | } 48 | 49 | // rank bitboards 50 | const ( 51 | Rank1 Board = 0xff00000000000000 52 | Rank2 Board = 0x00ff000000000000 53 | Rank3 Board = 0x0000ff0000000000 54 | Rank4 Board = 0x000000ff00000000 55 | Rank5 Board = 0x00000000ff000000 56 | Rank6 Board = 0x0000000000ff0000 57 | Rank7 Board = 0x000000000000ff00 58 | Rank8 Board = 0x00000000000000ff 59 | ) 60 | 61 | var Ranks = [...]Board{ 62 | square.Rank1: Rank1, 63 | square.Rank2: Rank2, 64 | square.Rank3: Rank3, 65 | square.Rank4: Rank4, 66 | square.Rank5: Rank5, 67 | square.Rank6: Rank6, 68 | square.Rank7: Rank7, 69 | square.Rank8: Rank8, 70 | } 71 | 72 | // diagonal bitboards 73 | const ( 74 | DiagonalH1H1 Board = 0x8000000000000000 75 | DiagonalH2G1 Board = 0x4080000000000000 76 | DiagonalH3F1 Board = 0x2040800000000000 77 | DiagonalH4E1 Board = 0x1020408000000000 78 | DiagonalH5D1 Board = 0x0810204080000000 79 | DiagonalH6C1 Board = 0x0408102040800000 80 | DiagonalH7B1 Board = 0x0204081020408000 81 | 82 | DiagonalH8A1 Board = 0x0102040810204080 83 | 84 | DiagonalG8A2 Board = 0x0001020408102040 85 | DiagonalF8A3 Board = 0x0000010204081020 86 | DiagonalE8A4 Board = 0x0000000102040810 87 | DiagonalD8A5 Board = 0x0000000001020408 88 | DiagonalC8A6 Board = 0x0000000000010204 89 | DiagonalB8A7 Board = 0x0000000000000102 90 | DiagonalA8A8 Board = 0x0000000000000001 91 | ) 92 | 93 | var Diagonals = [...]Board{ 94 | square.DiagonalH1H1: DiagonalH1H1, 95 | square.DiagonalH2G1: DiagonalH2G1, 96 | square.DiagonalH3F1: DiagonalH3F1, 97 | square.DiagonalH4E1: DiagonalH4E1, 98 | square.DiagonalH5D1: DiagonalH5D1, 99 | square.DiagonalH6C1: DiagonalH6C1, 100 | square.DiagonalH7B1: DiagonalH7B1, 101 | 102 | square.DiagonalH8A1: DiagonalH8A1, 103 | 104 | square.DiagonalG8A2: DiagonalG8A2, 105 | square.DiagonalF8A3: DiagonalF8A3, 106 | square.DiagonalE8A4: DiagonalE8A4, 107 | square.DiagonalD8A5: DiagonalD8A5, 108 | square.DiagonalC8A6: DiagonalC8A6, 109 | square.DiagonalB8A7: DiagonalB8A7, 110 | square.DiagonalA8A8: DiagonalA8A8, 111 | } 112 | 113 | // anti-diagonal bitboards 114 | const ( 115 | DiagonalA1A1 Board = 0x0100000000000000 116 | DiagonalA2B1 Board = 0x0201000000000000 117 | DiagonalA3C1 Board = 0x0402010000000000 118 | DiagonalA4D1 Board = 0x0804020100000000 119 | DiagonalA5E1 Board = 0x1008040201000000 120 | DiagonalA6F1 Board = 0x2010080402010000 121 | DiagonalA7G1 Board = 0x4020100804020100 122 | 123 | DiagonalA8H1 Board = 0x8040201008040201 124 | 125 | DiagonalB8H2 Board = 0x0080402010080402 126 | DiagonalC8H3 Board = 0x0000804020100804 127 | DiagonalD8H4 Board = 0x0000008040201008 128 | DiagonalE8H5 Board = 0x0000000080402010 129 | DiagonalF8H6 Board = 0x0000000000804020 130 | DiagonalG8H7 Board = 0x0000000000008040 131 | DiagonalH8H8 Board = 0x0000000000000080 132 | ) 133 | 134 | const ( 135 | F1G1 Board = 0x6000000000000000 136 | F8G8 Board = 0x0000000000000060 137 | C1D1 Board = 0x0c00000000000000 138 | C8D8 Board = 0x000000000000000c 139 | B1C1D1 Board = 0x0e00000000000000 140 | B8C8D8 Board = 0x000000000000000e 141 | ) 142 | 143 | var AntiDiagonals = [...]Board{ 144 | square.DiagonalA1A1: DiagonalA1A1, 145 | square.DiagonalA2B1: DiagonalA2B1, 146 | square.DiagonalA3C1: DiagonalA3C1, 147 | square.DiagonalA4D1: DiagonalA4D1, 148 | square.DiagonalA5E1: DiagonalA5E1, 149 | square.DiagonalA6F1: DiagonalA6F1, 150 | square.DiagonalA7G1: DiagonalA7G1, 151 | 152 | square.DiagonalA8H1: DiagonalA8H1, 153 | 154 | square.DiagonalB8H2: DiagonalB8H2, 155 | square.DiagonalC8H3: DiagonalC8H3, 156 | square.DiagonalD8H4: DiagonalD8H4, 157 | square.DiagonalE8H5: DiagonalE8H5, 158 | square.DiagonalF8H6: DiagonalF8H6, 159 | square.DiagonalG8H7: DiagonalG8H7, 160 | square.DiagonalH8H8: DiagonalH8H8, 161 | } 162 | -------------------------------------------------------------------------------- /pkg/board/move/move.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package move declares types and constants pertaining to chess moves. 15 | package move 16 | 17 | import ( 18 | "laptudirm.com/x/mess/pkg/board/piece" 19 | "laptudirm.com/x/mess/pkg/board/square" 20 | ) 21 | 22 | // Move represents a chess move. It contains various metadata regarding 23 | // the move including the source and target squares, the moving piece, 24 | // the promoted piece and wether the move is a capture. 25 | // 26 | // Format: MSB -> LSB 27 | // [20 isCapture bool 20] \ 28 | // [19 toPiece piece.Piece 16][15 fromPiece piece.Piece 12] \ 29 | // [11 target square.Square 6][05 source square.Square 00] 30 | type Move uint32 31 | 32 | // MaxN is the maximum number of plys in a chess game. 33 | const MaxN = 1024 34 | 35 | // Null Move represents a "do nothing" move on the chessboard. It is 36 | // represented by "0000", and is useful for returning errors and pruning. 37 | const Null Move = 0 38 | 39 | const ( 40 | // bit width of each field 41 | sourceWidth = 6 42 | targetWidth = 6 43 | fPieceWidth = 4 44 | tPieceWidth = 4 45 | tacticWidth = 1 46 | 47 | // bit offsets of each field 48 | sourceOffset = 0 49 | targetOffset = sourceOffset + sourceWidth 50 | fPieceOffset = targetOffset + targetWidth 51 | tPieceOffset = fPieceOffset + fPieceWidth 52 | tacticOffset = tPieceOffset + tPieceWidth 53 | 54 | // bit masks of each field 55 | sourceMask = (1 << sourceWidth) - 1 56 | targetMask = (1 << targetWidth) - 1 57 | fPieceMask = (1 << fPieceWidth) - 1 58 | tPieceMask = (1 << tPieceWidth) - 1 59 | tacticMask = (1 << tacticWidth) - 1 60 | ) 61 | 62 | // New creates a new Move value which is populated with the provided data. 63 | func New(source, target square.Square, fPiece piece.Piece, isCapture bool) Move { 64 | m := Move(source) << sourceOffset 65 | m |= Move(target) << targetOffset 66 | m |= Move(fPiece) << fPieceOffset 67 | m |= Move(fPiece) << tPieceOffset 68 | if isCapture { 69 | m |= tacticMask << tacticOffset 70 | } 71 | return m 72 | } 73 | 74 | // String converts a move to it's long algebraic notation form. 75 | // For example "e2e4", "e1g1"(castling), "d7d8q"(promotion), "0000"(null). 76 | func (m Move) String() string { 77 | // null move is a special case 78 | if m == Null { 79 | return "0000" 80 | } 81 | 82 | s := m.Source().String() + m.Target().String() 83 | 84 | // add promotion indicator 85 | if m.IsPromotion() { 86 | s += m.ToPiece().Type().String() 87 | } 88 | 89 | return s 90 | } 91 | 92 | // SetPromotion sets the promotion field of the move to the given piece. 93 | func (m Move) SetPromotion(p piece.Piece) Move { 94 | m &^= tPieceMask << tPieceOffset 95 | m |= Move(p) << tPieceOffset 96 | return m 97 | } 98 | 99 | // Source returns the source square of the move. 100 | func (m Move) Source() square.Square { 101 | return square.Square((m >> sourceOffset) & sourceMask) 102 | } 103 | 104 | // Target returns the target square of the move. 105 | func (m Move) Target() square.Square { 106 | return square.Square((m >> targetOffset) & targetMask) 107 | } 108 | 109 | // FromPiece returns the piece that is being moved. 110 | func (m Move) FromPiece() piece.Piece { 111 | return piece.Piece((m >> fPieceOffset) & fPieceMask) 112 | } 113 | 114 | // ToPiece returns the piece after moving. This is the same as FromPiece 115 | // for normal moves, and is only useful in promotions, where it returns 116 | // the promoted piece. 117 | func (m Move) ToPiece() piece.Piece { 118 | return piece.Piece((m >> tPieceOffset) & tPieceMask) 119 | } 120 | 121 | // IsCapture checks whether the move is a capture. 122 | func (m Move) IsCapture() bool { 123 | return (m>>tacticOffset)&tacticMask != 0 124 | } 125 | 126 | // IsPromotion checks if the move is a promotion. 127 | func (m Move) IsPromotion() bool { 128 | return m.FromPiece() != m.ToPiece() 129 | } 130 | 131 | // IsEnPassant checks if the move is en passant given the target square. 132 | func (m Move) IsEnPassant(ep square.Square) bool { 133 | return m.Target() == ep && m.FromPiece().Type() == piece.Pawn 134 | } 135 | 136 | // IsQuiet checks if the move is a quiet move. A quiet move is a move 137 | // which does not create huge material differences when played, unlike 138 | // captures and promotions. 139 | func (m Move) IsQuiet() bool { 140 | return !m.IsCapture() && !m.IsPromotion() 141 | } 142 | 143 | // IsReversible checks if the move is reversible. A move is termed as 144 | // reversible if it is possible to "undo" the move, like moving a knight 145 | // back. Captures and pawn moves are not reversible. 146 | func (m Move) IsReversible() bool { 147 | return !m.IsCapture() && m.FromPiece().Type() != piece.Pawn 148 | } 149 | -------------------------------------------------------------------------------- /pkg/search/eval/see.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package eval 15 | 16 | import ( 17 | "laptudirm.com/x/mess/internal/util" 18 | "laptudirm.com/x/mess/pkg/board" 19 | "laptudirm.com/x/mess/pkg/board/bitboard" 20 | "laptudirm.com/x/mess/pkg/board/move" 21 | "laptudirm.com/x/mess/pkg/board/move/attacks" 22 | "laptudirm.com/x/mess/pkg/board/piece" 23 | "laptudirm.com/x/mess/pkg/board/square" 24 | ) 25 | 26 | var seeValue = [piece.TypeN]Eval{ 27 | piece.Pawn: 100, 28 | piece.Knight: 400, 29 | piece.Bishop: 400, 30 | piece.Rook: 600, 31 | piece.Queen: 1000, 32 | piece.King: 30000, 33 | } 34 | 35 | // SEE performs a static exchange evaluation on the given board starting 36 | // with the given move. It returns true if the capture sequence beats the 37 | // provided threshold, and false otherwise. 38 | func SEE(b *board.Board, m move.Move, threshold Eval) bool { 39 | // relevant squares 40 | source, target := m.Source(), m.Target() 41 | 42 | // relevant piece types 43 | attacker := m.ToPiece().Type() 44 | victim := util.Ternary(m.IsEnPassant(b.EnPassantTarget), piece.Pawn, b.Position[target].Type()) 45 | 46 | balance := seeValue[victim] // win the victim 47 | if balance < threshold { 48 | // even if we win the captured piece for free, balance is still 49 | // less than the threshold, so we can't beat threshold 50 | return false 51 | } 52 | 53 | balance -= seeValue[attacker] // lose the attacker 54 | if balance >= threshold { 55 | // even if we lose the capturing piece for nothing, balance is 56 | // still greater than or equal to threshold, so this capture 57 | // will definitely beat threshold 58 | return true 59 | } 60 | 61 | // calculate occupied squares 62 | occupied := b.ColorBBs[piece.White] | b.ColorBBs[piece.Black] 63 | 64 | // make the capture 65 | occupied.Unset(source) // remove the capturing piece 66 | sideToMove := b.SideToMove.Other() // switch sides after capture 67 | 68 | // calculate attackers to target square 69 | attackers := attackersTo(b, target, occupied) & occupied 70 | 71 | // calculate ray attackers to reveal x-rays 72 | diagonal := b.PieceBBs[piece.Bishop] | b.PieceBBs[piece.Queen] // diagonal attackers 73 | straight := b.PieceBBs[piece.Rook] | b.PieceBBs[piece.Queen] // straight attackers 74 | 75 | for { 76 | // calculate friendly attackers 77 | friends := attackers & b.ColorBBs[sideToMove] 78 | if friends == bitboard.Empty { 79 | // no more friendly attackers: end see 80 | break 81 | } 82 | 83 | // find least valuable piece to attack with 84 | for attacker = piece.Pawn; attacker < piece.King; attacker++ { 85 | if friends&b.PieceBBs[attacker] != bitboard.Empty { 86 | // piece of this type has been found 87 | break 88 | } 89 | } 90 | 91 | if attacker == piece.King && (attackers&^friends) != bitboard.Empty { 92 | // king can't capture if other side still has attackers 93 | break 94 | } 95 | 96 | // get source square of new attacker 97 | source = (friends & b.PieceBBs[attacker]).FirstOne() 98 | 99 | // make the capture 100 | occupied.Unset(source) // remove the capturing piece 101 | sideToMove = sideToMove.Other() // switch sides after capture 102 | 103 | // lose the current capturer 104 | balance = -balance - seeValue[attacker] 105 | 106 | if balance >= threshold { 107 | // capture is winning even if the current capturer is lost 108 | // so we can end the exchange evaluation safely 109 | break 110 | } 111 | 112 | // add attackers which were hidden by the capturing piece (x rays) 113 | switch attacker { 114 | case piece.Pawn, piece.Bishop: 115 | attackers |= attacks.Bishop(target, occupied) & diagonal 116 | case piece.Rook: 117 | attackers |= attacks.Rook(target, occupied) & straight 118 | case piece.Queen: 119 | switch { 120 | case source.File() == target.File(), source.Rank() == target.Rank(): 121 | attackers |= attacks.Rook(target, occupied) & straight 122 | default: 123 | attackers |= attacks.Bishop(target, occupied) & diagonal 124 | } 125 | } 126 | 127 | // remove attackers which have already captured 128 | attackers &= occupied 129 | } 130 | 131 | // at the end of see sideToMove is the side which failed to capture 132 | // back. The capture sequence is only winning/equal if we are able 133 | // to capture back. 134 | return sideToMove != b.SideToMove 135 | } 136 | 137 | func attackersTo(b *board.Board, s square.Square, blockers bitboard.Board) bitboard.Board { 138 | diagonal := b.PieceBBs[piece.Bishop] | b.PieceBBs[piece.Queen] 139 | straight := b.PieceBBs[piece.Rook] | b.PieceBBs[piece.Queen] 140 | 141 | return attacks.King[s]&b.PieceBBs[piece.King] | // kings 142 | attacks.Knight[s]&b.PieceBBs[piece.Knight] | // knights 143 | attacks.Pawn[piece.White][s]&b.PawnsBB(piece.Black) | // black pawns 144 | attacks.Pawn[piece.Black][s]&b.PawnsBB(piece.White) | // white pawns 145 | attacks.Bishop(s, blockers)&(diagonal) | // bishops and queens 146 | attacks.Rook(s, blockers)&(straight) // rooks and queens 147 | } 148 | -------------------------------------------------------------------------------- /pkg/board/move/attacks/magic/magic.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package magic provides reusable utility types and functions that are 15 | // used to generate magic hash tables for any sliding piece. 16 | // 17 | // Blocker masks are uint64 bitboards and therefore there are too many 18 | // permutations to exhaustively calculate. However, the relevant blockers 19 | // for a given square are much fewer in number and can be calculated 20 | // exhaustively. Therefore, we can design a perfect hash function which 21 | // can index every blocker mask relevant to a given square by calculating 22 | // a magic number such that mask * magic >> bits is a perfect contagious 23 | // hash function. It is simplest to calculate this by generating random 24 | // magic numbers and checking if they work. 25 | package magic 26 | 27 | import ( 28 | "laptudirm.com/x/mess/internal/util" 29 | "laptudirm.com/x/mess/pkg/board/bitboard" 30 | "laptudirm.com/x/mess/pkg/board/square" 31 | ) 32 | 33 | // magicSeeds are optimized prng seeds which generate valid magics fastest 34 | // these values have been taken from the Stockfish chess engine 35 | var magicSeeds = [8]uint64{255, 16645, 15100, 12281, 32803, 55013, 10316, 728} 36 | 37 | // NewTable generates a new Magic Hash Table from the given moveFunc. It 38 | // automatically generates the magics and thus is a slow function. 39 | func NewTable(maskN int, moveFunc MoveFunc) *Table { 40 | var t Table 41 | 42 | // populate table 43 | 44 | var rand util.PRNG 45 | 46 | for s := square.A8; s <= square.H1; s++ { 47 | magic := &t.Magics[s] // get magic entry for the current square 48 | 49 | // calculate known info 50 | magic.BlockerMask = moveFunc(s, bitboard.Empty, true) // relevant blocker mask 51 | bitCount := magic.BlockerMask.Count() 52 | magic.Shift = uint8(64 - bitCount) // index function shift amount 53 | 54 | // calculate number of permutations of the blocker mask 55 | permutationsN := 1 << bitCount // 2^bitCount 56 | permutations := make([]bitboard.Board, permutationsN) 57 | 58 | // initialize blocker mask 59 | blockers := bitboard.Empty 60 | 61 | // generate all blocker mask permutations and store them, i.e. generate 62 | // all subsets of the set of the current blocker mask. This is achieved 63 | // using the Carry-Rippler Trick (https://bit.ly/3XlXipd) 64 | for index := 0; blockers != bitboard.Empty || index == 0; index++ { 65 | permutations[index] = blockers 66 | blockers = (blockers - magic.BlockerMask) & magic.BlockerMask 67 | } 68 | 69 | // seed random number generator 70 | rand.Seed(magicSeeds[s.Rank()]) 71 | 72 | searchingMagic: 73 | for { // loop until a valid magic is found 74 | 75 | // initialize table entry 76 | t.Table[s] = make([]bitboard.Board, maskN) 77 | 78 | // generate a magic candidate 79 | magic.Number = rand.SparseUint64() 80 | 81 | // try to index all permutations of the blocker 82 | // mask using the new magic candidate 83 | for i := 0; i < permutationsN; i++ { 84 | blockers := permutations[i] 85 | 86 | index := magic.Index(blockers) // permutation index 87 | attacks := moveFunc(s, blockers, false) // permutation attack set 88 | 89 | if t.Table[s][index] != bitboard.Empty && t.Table[s][index] != attacks { 90 | // the calculated index is not empty and the attack sets are not 91 | // equal: we have a hash collision. Continue searching the magic 92 | continue searchingMagic 93 | } 94 | 95 | // no hash collision: store the entry 96 | t.Table[s][index] = attacks 97 | } 98 | 99 | // all permutations were successfully stored without hash collisions, 100 | // so we have found a valid magic and can stop searching for others 101 | break 102 | } 103 | } 104 | 105 | return &t 106 | } 107 | 108 | // Table represents a magic hash table. 109 | type Table struct { 110 | Magics [square.N]Magic // list of magics for each square 111 | Table [square.N][]bitboard.Board // the underlying move-list table 112 | } 113 | 114 | // Probe probes the magic hash table for the move bitboard given the 115 | // piece square and blocker mask. It returns the move bitboard. 116 | func (t *Table) Probe(s square.Square, blockerMask bitboard.Board) bitboard.Board { 117 | return t.Table[s][t.Magics[s].Index(blockerMask)] 118 | } 119 | 120 | // Magic represents a single magic entry. Each magic entry is used to 121 | // index all the attack sets for a given square. 122 | type Magic struct { 123 | Number uint64 // magic multiplication number 124 | BlockerMask bitboard.Board // mask of relevant blockers 125 | Shift byte // 64 - no of blocker permutations 126 | } 127 | 128 | // Index calculates the index of the given blocker mask given it's magic. 129 | func (magic Magic) Index(blockerMask bitboard.Board) uint64 { 130 | blockerMask &= magic.BlockerMask // remove irrelevant blockers 131 | return (uint64(blockerMask) * magic.Number) >> magic.Shift 132 | } 133 | 134 | // MoveFunc is a sliding piece's move generation function. It takes the 135 | // piece square, blocker mask, and bool which reports if the function is 136 | // being used to generate blocker masks, so that it can mask out the edge 137 | // bits. It returns a bitboard with all the movable squares set. 138 | type MoveFunc func(square.Square, bitboard.Board, bool) bitboard.Board 139 | -------------------------------------------------------------------------------- /internal/generator/classical/tables.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "laptudirm.com/x/mess/pkg/board/piece" 18 | "laptudirm.com/x/mess/pkg/board/square" 19 | "laptudirm.com/x/mess/pkg/search/eval" 20 | ) 21 | 22 | // piece values 23 | var mgPieceValues = [piece.TypeN]eval.Eval{0, 82, 337, 365, 477, 1025, 0} 24 | var egPieceValues = [piece.TypeN]eval.Eval{0, 94, 281, 297, 512, 936, 0} 25 | 26 | // piece-square tables 27 | 28 | var mgPawn = [square.N]eval.Eval{ 29 | 0, 0, 0, 0, 0, 0, 0, 0, 30 | 98, 134, 61, 95, 68, 126, 34, -11, 31 | -6, 7, 26, 31, 65, 56, 25, -20, 32 | -14, 13, 6, 21, 23, 12, 17, -23, 33 | -27, -2, -5, 12, 17, 6, 10, -25, 34 | -26, -4, -4, -10, 3, 3, 33, -12, 35 | -35, -1, -20, -23, -15, 24, 38, -22, 36 | 0, 0, 0, 0, 0, 0, 0, 0, 37 | } 38 | 39 | var egPawn = [square.N]eval.Eval{ 40 | 0, 0, 0, 0, 0, 0, 0, 0, 41 | 178, 173, 158, 134, 147, 132, 165, 187, 42 | 94, 100, 85, 67, 56, 53, 82, 84, 43 | 32, 24, 13, 5, -2, 4, 17, 17, 44 | 13, 9, -3, -7, -7, -8, 3, -1, 45 | 4, 7, -6, 1, 0, -5, -1, -8, 46 | 13, 8, 8, 10, 13, 0, 2, -7, 47 | 0, 0, 0, 0, 0, 0, 0, 0, 48 | } 49 | 50 | var mgKnight = [square.N]eval.Eval{ 51 | -167, -89, -34, -49, 61, -97, -15, -107, 52 | -73, -41, 72, 36, 23, 62, 7, -17, 53 | -47, 60, 37, 65, 84, 129, 73, 44, 54 | -9, 17, 19, 53, 37, 69, 18, 22, 55 | -13, 4, 16, 13, 28, 19, 21, -8, 56 | -23, -9, 12, 10, 19, 17, 25, -16, 57 | -29, -53, -12, -3, -1, 18, -14, -19, 58 | -105, -21, -58, -33, -17, -28, -19, -23, 59 | } 60 | 61 | var egKnight = [square.N]eval.Eval{ 62 | -58, -38, -13, -28, -31, -27, -63, -99, 63 | -25, -8, -25, -2, -9, -25, -24, -52, 64 | -24, -20, 10, 9, -1, -9, -19, -41, 65 | -17, 3, 22, 22, 22, 11, 8, -18, 66 | -18, -6, 16, 25, 16, 17, 4, -18, 67 | -23, -3, -1, 15, 10, -3, -20, -22, 68 | -42, -20, -10, -5, -2, -20, -23, -44, 69 | -29, -51, -23, -15, -22, -18, -50, -64, 70 | } 71 | 72 | var mgBishop = [square.N]eval.Eval{ 73 | -29, 4, -82, -37, -25, -42, 7, -8, 74 | -26, 16, -18, -13, 30, 59, 18, -47, 75 | -16, 37, 43, 40, 35, 50, 37, -2, 76 | -4, 5, 19, 50, 37, 37, 7, -2, 77 | -6, 13, 13, 26, 34, 12, 10, 4, 78 | 0, 15, 15, 15, 14, 27, 18, 10, 79 | 4, 15, 16, 0, 7, 21, 33, 1, 80 | -33, -3, -14, -21, -13, -12, -39, -21, 81 | } 82 | 83 | var egBishop = [square.N]eval.Eval{ 84 | -14, -21, -11, -8, -7, -9, -17, -24, 85 | -8, -4, 7, -12, -3, -13, -4, -14, 86 | 2, -8, 0, -1, -2, 6, 0, 4, 87 | -3, 9, 12, 9, 14, 10, 3, 2, 88 | -6, 3, 13, 19, 7, 10, -3, -9, 89 | -12, -3, 8, 10, 13, 3, -7, -15, 90 | -14, -18, -7, -1, 4, -9, -15, -27, 91 | -23, -9, -23, -5, -9, -16, -5, -17, 92 | } 93 | 94 | var mgRook = [square.N]eval.Eval{ 95 | 32, 42, 32, 51, 63, 9, 31, 43, 96 | 27, 32, 58, 62, 80, 67, 26, 44, 97 | -5, 19, 26, 36, 17, 45, 61, 16, 98 | -24, -11, 7, 26, 24, 35, -8, -20, 99 | -36, -26, -12, -1, 9, -7, 6, -23, 100 | -45, -25, -16, -17, 3, 0, -5, -33, 101 | -44, -16, -20, -9, -1, 11, -6, -71, 102 | -19, -13, 1, 17, 16, 7, -37, -26, 103 | } 104 | 105 | var egRook = [square.N]eval.Eval{ 106 | 13, 10, 18, 15, 12, 12, 8, 5, 107 | 11, 13, 13, 11, -3, 3, 8, 3, 108 | 7, 7, 7, 5, 4, -3, -5, -3, 109 | 4, 3, 13, 1, 2, 1, -1, 2, 110 | 3, 5, 8, 4, -5, -6, -8, -11, 111 | -4, 0, -5, -1, -7, -12, -8, -16, 112 | -6, -6, 0, 2, -9, -9, -11, -3, 113 | -9, 2, 3, -1, -5, -13, 4, -20, 114 | } 115 | 116 | var mgQueen = [square.N]eval.Eval{ 117 | -28, 0, 29, 12, 59, 44, 43, 45, 118 | -24, -39, -5, 1, -16, 57, 28, 54, 119 | -13, -17, 7, 8, 29, 56, 47, 57, 120 | -27, -27, -16, -16, -1, 17, -2, 1, 121 | -9, -26, -9, -10, -2, -4, 3, -3, 122 | -14, 2, -11, -2, -5, 2, 14, 5, 123 | -35, -8, 11, 2, 8, 15, -3, 1, 124 | -1, -18, -9, 10, -15, -25, -31, -50, 125 | } 126 | 127 | var egQueen = [square.N]eval.Eval{ 128 | -9, 22, 22, 27, 27, 19, 10, 20, 129 | -17, 20, 32, 41, 58, 25, 30, 0, 130 | -20, 6, 9, 49, 47, 35, 19, 9, 131 | 3, 22, 24, 45, 57, 40, 57, 36, 132 | -18, 28, 19, 47, 31, 34, 39, 23, 133 | -16, -27, 15, 6, 9, 17, 10, 5, 134 | -22, -23, -30, -16, -16, -23, -36, -32, 135 | -33, -28, -22, -43, -5, -32, -20, -41, 136 | } 137 | 138 | var mgKing = [square.N]eval.Eval{ 139 | -65, 23, 16, -15, -56, -34, 2, 13, 140 | 29, -1, -20, -7, -8, -4, -38, -29, 141 | -9, 24, 2, -16, -20, 6, 22, -22, 142 | -17, -20, -12, -27, -30, -25, -14, -36, 143 | -49, -1, -27, -39, -46, -44, -33, -51, 144 | -14, -14, -22, -46, -44, -30, -15, -27, 145 | 1, 7, -8, -64, -43, -16, 9, 8, 146 | -15, 36, 12, -54, 8, -28, 24, 14, 147 | } 148 | 149 | var egKing = [square.N]eval.Eval{ 150 | -74, -35, -18, -18, -11, 15, 4, -17, 151 | -12, 17, 14, 17, 17, 38, 23, 11, 152 | 10, 17, 23, 15, 20, 45, 44, 13, 153 | -8, 22, 24, 27, 26, 33, 26, 3, 154 | -18, -4, 21, 24, 27, 23, 9, -11, 155 | -19, -3, 11, 21, 23, 16, 7, -9, 156 | -27, -11, 4, 13, 14, 4, -5, -17, 157 | -53, -34, -21, -11, -28, -14, -24, -43, 158 | } 159 | 160 | // collected piece-square tables 161 | 162 | var mgPieceTable = [piece.TypeN][square.N]eval.Eval{ 163 | piece.Pawn: mgPawn, 164 | piece.Knight: mgKnight, 165 | piece.Bishop: mgBishop, 166 | piece.Rook: mgRook, 167 | piece.Queen: mgQueen, 168 | piece.King: mgKing, 169 | } 170 | 171 | var egPieceTable = [piece.TypeN][square.N]eval.Eval{ 172 | piece.Pawn: egPawn, 173 | piece.Knight: egKnight, 174 | piece.Bishop: egBishop, 175 | piece.Rook: egRook, 176 | piece.Queen: egQueen, 177 | piece.King: egKing, 178 | } 179 | -------------------------------------------------------------------------------- /internal/engine/cmd/bench.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cmd 15 | 16 | import ( 17 | realtime "time" 18 | 19 | "laptudirm.com/x/mess/internal/engine/context" 20 | "laptudirm.com/x/mess/pkg/formats/fen" 21 | "laptudirm.com/x/mess/pkg/search" 22 | "laptudirm.com/x/mess/pkg/uci/cmd" 23 | ) 24 | 25 | // Custom command bench 26 | // 27 | // The bench command is used to benchmark the engine when testing it using 28 | // a testing framework like OpenBench. 29 | func NewBench(engine *context.Engine) cmd.Command { 30 | 31 | // benchmarks from BitGenie 32 | // https://github.com/Aryan1508/Bit-Genie/blob/master/src/bench.txt 33 | benchFens := []string{ 34 | "r3k2r/2pb1ppp/2pp1q2/p7/1nP1B3/1P2P3/P2N1PPP/R2QK2R w KQkq a6 0 14", 35 | "4rrk1/2p1b1p1/p1p3q1/4p3/2P2n1p/1P1NR2P/PB3PP1/3R1QK1 b - - 2 24", 36 | "r3qbrk/6p1/2b2pPp/p3pP1Q/PpPpP2P/3P1B2/2PB3K/R5R1 w - - 16 42", 37 | "6k1/1R3p2/6p1/2Bp3p/3P2q1/P7/1P2rQ1K/5R2 b - - 4 44", 38 | "8/8/1p2k1p1/3p3p/1p1P1P1P/1P2PK2/8/8 w - - 3 54", 39 | "7r/2p3k1/1p1p1qp1/1P1Bp3/p1P2r1P/P7/4R3/Q4RK1 w - - 0 36", 40 | "r1bq1rk1/pp2b1pp/n1pp1n2/3P1p2/2P1p3/2N1P2N/PP2BPPP/R1BQ1RK1 b - - 2 10", 41 | "3r3k/2r4p/1p1b3q/p4P2/P2Pp3/1B2P3/3BQ1RP/6K1 w - - 3 87", 42 | "2r4r/1p4k1/1Pnp4/3Qb1pq/8/4BpPp/5P2/2RR1BK1 w - - 0 42", 43 | "4q1bk/6b1/7p/p1p4p/PNPpP2P/KN4P1/3Q4/4R3 b - - 0 37", 44 | "2q3r1/1r2pk2/pp3pp1/2pP3p/P1Pb1BbP/1P4Q1/R3NPP1/4R1K1 w - - 2 34", 45 | "1r2r2k/1b4q1/pp5p/2pPp1p1/P3Pn2/1P1B1Q1P/2R3P1/4BR1K b - - 1 37", 46 | "r3kbbr/pp1n1p1P/3ppnp1/q5N1/1P1pP3/P1N1B3/2P1QP2/R3KB1R b KQkq b3 0 17", 47 | "8/6pk/2b1Rp2/3r4/1R1B2PP/P5K1/8/2r5 b - - 16 42", 48 | "1r4k1/4ppb1/2n1b1qp/pB4p1/1n1BP1P1/7P/2PNQPK1/3RN3 w - - 8 29", 49 | "8/p2B4/PkP5/4p1pK/4Pb1p/5P2/8/8 w - - 29 68", 50 | "3r4/ppq1ppkp/4bnp1/2pN4/2P1P3/1P4P1/PQ3PBP/R4K2 b - - 2 20", 51 | "5rr1/4n2k/4q2P/P1P2n2/3B1p2/4pP2/2N1P3/1RR1K2Q w - - 1 49", 52 | "1r5k/2pq2p1/3p3p/p1pP4/4QP2/PP1R3P/6PK/8 w - - 1 51", 53 | "q5k1/5ppp/1r3bn1/1B6/P1N2P2/BQ2P1P1/5K1P/8 b - - 2 34", 54 | "r1b2k1r/5n2/p4q2/1ppn1Pp1/3pp1p1/NP2P3/P1PPBK2/1RQN2R1 w - - 0 22", 55 | "r1bqk2r/pppp1ppp/5n2/4b3/4P3/P1N5/1PP2PPP/R1BQKB1R w KQkq - 0 5", 56 | "r1bqr1k1/pp1p1ppp/2p5/8/3N1Q2/P2BB3/1PP2PPP/R3K2n b Q - 1 12", 57 | "r1bq2k1/p4r1p/1pp2pp1/3p4/1P1B3Q/P2B1N2/2P3PP/4R1K1 b - - 2 19", 58 | "r4qk1/6r1/1p4p1/2ppBbN1/1p5Q/P7/2P3PP/5RK1 w - - 2 25", 59 | "r7/6k1/1p6/2pp1p2/7Q/8/p1P2K1P/8 w - - 0 32", 60 | "r3k2r/ppp1pp1p/2nqb1pn/3p4/4P3/2PP4/PP1NBPPP/R2QK1NR w KQkq - 1 5", 61 | "3r1rk1/1pp1pn1p/p1n1q1p1/3p4/Q3P3/2P5/PP1NBPPP/4RRK1 w - - 0 12", 62 | "5rk1/1pp1pn1p/p3Brp1/8/1n6/5N2/PP3PPP/2R2RK1 w - - 2 20", 63 | "8/1p2pk1p/p1p1r1p1/3n4/8/5R2/PP3PPP/4R1K1 b - - 3 27", 64 | "8/4pk2/1p1r2p1/p1p4p/Pn5P/3R4/1P3PP1/4RK2 w - - 1 33", 65 | "8/5k2/1pnrp1p1/p1p4p/P6P/4R1PK/1P3P2/4R3 b - - 1 38", 66 | "8/8/1p1kp1p1/p1pr1n1p/P6P/1R4P1/1P3PK1/1R6 b - - 15 45", 67 | "8/8/1p1k2p1/p1prp2p/P2n3P/6P1/1P1R1PK1/4R3 b - - 5 49", 68 | "8/8/1p4p1/p1p2k1p/P2npP1P/4K1P1/1P6/3R4 w - - 6 54", 69 | "8/8/1p4p1/p1p2k1p/P2n1P1P/4K1P1/1P6/6R1 b - - 6 59", 70 | "8/5k2/1p4p1/p1pK3p/P2n1P1P/6P1/1P6/4R3 b - - 14 63", 71 | "8/1R6/1p1K1kp1/p6p/P1p2P1P/6P1/1Pn5/8 w - - 0 67", 72 | "1rb1rn1k/p3q1bp/2p3p1/2p1p3/2P1P2N/PP1RQNP1/1B3P2/4R1K1 b - - 4 23", 73 | "4rrk1/pp1n1pp1/q5p1/P1pP4/2n3P1/7P/1P3PB1/R1BQ1RK1 w - - 3 22", 74 | "r2qr1k1/pb1nbppp/1pn1p3/2ppP3/3P4/2PB1NN1/PP3PPP/R1BQR1K1 w - - 4 12", 75 | "2r2k2/8/4P1R1/1p6/8/P4K1N/7b/2B5 b - - 0 55", 76 | "6k1/5pp1/8/2bKP2P/2P5/p4PNb/B7/8 b - - 1 44", 77 | "2rqr1k1/1p3p1p/p2p2p1/P1nPb3/2B1P3/5P2/1PQ2NPP/R1R4K w - - 3 25", 78 | "r1b2rk1/p1q1ppbp/6p1/2Q5/8/4BP2/PPP3PP/2KR1B1R b - - 2 14", 79 | "6r1/5k2/p1b1r2p/1pB1p1p1/1Pp3PP/2P1R1K1/2P2P2/3R4 w - - 1 36", 80 | "rnbqkb1r/pppppppp/5n2/8/2PP4/8/PP2PPPP/RNBQKBNR b KQkq c3 0 2", 81 | "2rr2k1/1p4bp/p1q1p1p1/4Pp1n/2PB4/1PN3P1/P3Q2P/2RR2K1 w - f6 0 20", 82 | "3br1k1/p1pn3p/1p3n2/5pNq/2P1p3/1PN3PP/P2Q1PB1/4R1K1 w - - 0 23", 83 | "2r2b2/5p2/5k2/p1r1pP2/P2pB3/1P3P2/K1P3R1/7R w - - 23 93", 84 | "1rqbkrbn/1ppppp1p/1n6/p1N3p1/8/2P4P/PP1PPPP1/1RQBKRBN w FBfb - 0 9", 85 | "rbbqn1kr/pp2p1pp/6n1/2pp1p2/2P4P/P7/BP1PPPP1/R1BQNNKR w HAha - 0 9", 86 | "rqbbknr1/1ppp2pp/p5n1/4pp2/P7/1PP5/1Q1PPPPP/R1BBKNRN w GAga - 0 9", 87 | "4rrb1/1kp3b1/1p1p4/pP1Pn2p/5p2/1PR2P2/2P1NB1P/2KR1B2 w D - 0 21", 88 | "1rkr3b/1ppn3p/3pB1n1/6q1/R2P4/4N1P1/1P5P/2KRQ1B1 b Dbd - 0 14", 89 | } 90 | 91 | // number of benchmarks 92 | benchN := len(benchFens) 93 | 94 | // search limits 95 | limits := search.Limits{ 96 | Depth: 12, // make this higher as engine gets faster 97 | Infinite: true, 98 | } 99 | 100 | return cmd.Command{ 101 | Name: "bench", 102 | Run: func(interaction cmd.Interaction) error { 103 | nodes := 0 // number of nodes searched 104 | startTime := realtime.Now() 105 | 106 | for i, fenString := range benchFens { 107 | // report position info 108 | interaction.Replyf("Position %d/%d: %s", i+1, benchN, fenString) 109 | 110 | var newNodes int 111 | 112 | // setup position to search on 113 | context := search.NewContext(func(report search.Report) { 114 | newNodes = report.Nodes // add to total node count 115 | interaction.Reply(report) 116 | }, 16) 117 | context.UpdatePosition(fen.FromString(fenString)) 118 | 119 | // search position 120 | _, _, err := context.Search(limits) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | nodes += newNodes 126 | 127 | // newline separator between each position 128 | interaction.Reply() 129 | } 130 | 131 | // report nodes and nps 132 | benchTime := realtime.Since(startTime) 133 | interaction.Replyf("%d nodes %.f nps", nodes, float64(nodes)/benchTime.Seconds()) 134 | 135 | return nil 136 | }, 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /pkg/search/tt/table.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package tt implements a transposition table which is used to cache 15 | // results from previous searches of a position to make search more 16 | // efficient. It stores things like the score and pv move. 17 | package tt 18 | 19 | import ( 20 | "math/bits" 21 | "unsafe" 22 | 23 | "laptudirm.com/x/mess/pkg/board/move" 24 | "laptudirm.com/x/mess/pkg/board/zobrist" 25 | "laptudirm.com/x/mess/pkg/search/eval" 26 | ) 27 | 28 | // EntrySize stores the size in bytes of a tt entry. 29 | var EntrySize = int(unsafe.Sizeof(Entry{})) 30 | 31 | // NewTable creates a new transposition table with a size equal to or 32 | // less than the given number of megabytes. 33 | func NewTable(mbs int) *Table { 34 | // compute table size (number of entries) 35 | size := (mbs * 1024 * 1024) / EntrySize 36 | 37 | return &Table{ 38 | table: make([]Entry, size), 39 | size: size, 40 | } 41 | } 42 | 43 | // Table represents a transposition table. 44 | type Table struct { 45 | table []Entry // hash table 46 | size int // table size 47 | epoch uint8 // table epoch 48 | } 49 | 50 | func (tt *Table) Clear() { 51 | clear(tt.table) 52 | } 53 | 54 | // NextEpoch increases the epoch number of the given tt. 55 | func (tt *Table) NextEpoch() { 56 | tt.epoch++ 57 | } 58 | 59 | // Resize resizes the given transposition table to the new size. The 60 | // entries are copied from the old table to the new one. If the new table 61 | // is smaller, some entries are discarded. 62 | func (tt *Table) Resize(mbs int) { 63 | // compute new table size (number of entries) 64 | size := (mbs * 1024 * 1024) / EntrySize 65 | 66 | // create table with new size 67 | newTable := make([]Entry, size) 68 | 69 | // copy old elements 70 | copy(newTable, tt.table) 71 | 72 | // replace old table 73 | *tt = Table{ 74 | table: newTable, 75 | size: size, 76 | } 77 | } 78 | 79 | // Store puts the given data into the transposition table. 80 | func (tt *Table) Store(entry Entry) { 81 | target := tt.fetch(entry.Hash) 82 | entry.epoch = tt.epoch 83 | 84 | // replace only if the new data has an equal or higher quality. 85 | if entry.quality() >= target.quality() { 86 | *target = entry 87 | } 88 | } 89 | 90 | // Probe fetches the data associated with the given zobrist key from the 91 | // transposition table. It returns the fetched data and whether it is 92 | // usable or not. It guards against hash collisions and empty entries. 93 | // If the bool value is false, the entry should not be use for anything. 94 | func (tt *Table) Probe(hash zobrist.Key) (Entry, bool) { 95 | entry := *tt.fetch(hash) 96 | return entry, entry.Type != NoEntry && entry.Hash == hash 97 | } 98 | 99 | // fetch returns a pointer pointing to the tt entry of the given hash. 100 | func (tt *Table) fetch(hash zobrist.Key) *Entry { 101 | return &tt.table[tt.indexOf(hash)] 102 | } 103 | 104 | // indexOf is the hash function used by the transposition table. 105 | func (tt *Table) indexOf(hash zobrist.Key) uint { 106 | // fast indexing function from Daniel Lemire's blog post 107 | // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ 108 | index, _ := bits.Mul(uint(hash), uint(tt.size)) 109 | return index 110 | } 111 | 112 | // Entry represents a transposition table entry. 113 | type Entry struct { 114 | // complete hash of the position; to guard against 115 | // transposition table key collisions 116 | Hash zobrist.Key 117 | 118 | // best move in the position 119 | // used during iterative deepening as pv move 120 | Move move.Move 121 | 122 | // evaluation info 123 | Value Eval // value of this position 124 | Type EntryType // bound type of the value 125 | 126 | // entry metadata 127 | Depth uint8 // depth the position was searched to 128 | epoch uint8 // epoch/age of the entry from creation 129 | } 130 | 131 | // quality returns a quality measure of the given tt entry which will be 132 | // used to determine whether a tt entry should be overwritten or not. 133 | func (entry *Entry) quality() uint8 { 134 | return entry.epoch + entry.Depth/3 135 | } 136 | 137 | // EntryType represents the type of a transposition table entry's 138 | // value, whether it exists, it is upper bound, lower bound, or exact. 139 | type EntryType uint8 140 | 141 | // constants representing various transposition table entry types 142 | const ( 143 | NoEntry EntryType = iota // no entry exists 144 | 145 | ExactEntry // the value is an exact score 146 | LowerBound // the value is a lower bound on the exact score 147 | UpperBound // the value is an upper bound on the exact score 148 | ) 149 | 150 | // EvalFrom converts a given mate score from "n plys till mate from root" 151 | // to "n plys till mate from current position" so that it is reusable in 152 | // other positions with greater or lesser depth. 153 | func EvalFrom(score eval.Eval, plys int) Eval { 154 | switch { 155 | case score > eval.WinInMaxPly: 156 | score += eval.Eval(plys) 157 | case score < eval.LoseInMaxPly: 158 | score -= eval.Eval(plys) 159 | } 160 | 161 | return Eval(score) 162 | } 163 | 164 | // Eval represents the evaluation of a transposition table entry. For mate 165 | // scores, it stores "n plys till mate from current position" instead of the 166 | // standard "n plys till mate from root" used in search. 167 | type Eval eval.Eval 168 | 169 | // Eval converts transposition table entry scores from "n plys to mate 170 | // from current position" to "n plys till mate from root" which is the 171 | // format used during search. 172 | func (e Eval) Eval(plys int) eval.Eval { 173 | score := eval.Eval(e) 174 | 175 | // checkmate scores need to be changed from 176 | switch { 177 | case score > eval.WinInMaxPly: 178 | score -= eval.Eval(plys) 179 | case score < eval.LoseInMaxPly: 180 | score += eval.Eval(plys) 181 | } 182 | 183 | return score 184 | } 185 | -------------------------------------------------------------------------------- /scripts/datagen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "laptudirm.com/x/mess/internal/util" 12 | "laptudirm.com/x/mess/pkg/board/move" 13 | "laptudirm.com/x/mess/pkg/board/piece" 14 | "laptudirm.com/x/mess/pkg/formats/fen" 15 | "laptudirm.com/x/mess/pkg/search" 16 | "laptudirm.com/x/mess/pkg/search/eval" 17 | ) 18 | 19 | func main() { 20 | if err := Main(); err != nil { 21 | _, _ = fmt.Fprintln(os.Stderr, err) 22 | os.Exit(1) 23 | } 24 | } 25 | 26 | func Main() error { 27 | // Command-Line Flags: 28 | openings := flag.String("openings", "", "shuffled opening book containing a list of fens") 29 | offset := flag.Int("opening-offset", 0, "offset from which the books should be read") 30 | output := flag.String("output", "data.legacy", "output file for the generated fens and other data") 31 | games := flag.Int("games", 100_000, "number of games to generate data for (actual might be less)") 32 | threads := flag.Int("threads", 1, "number of threads to use for data generation work") 33 | winAdjudicateEval := flag.Uint("win-adjudicate-eval", uint(eval.Mate), "search score for which game will be adjudicated as a win") 34 | nodes := flag.Int("nodes", 10_000, "node limit for searches on a single fen") 35 | depth := flag.Int("depth", 0x0009, "depth limit for searches on a single fen") 36 | 37 | // Parse the CLI Flags. 38 | flag.Parse() 39 | 40 | // Create a new data generator. 41 | g, err := NewGenerator(*openings, *output, *offset, *games, *threads, *nodes, *depth, eval.Eval(*winAdjudicateEval)) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | // Generate Data. 47 | g.GenerateData() 48 | return nil 49 | } 50 | 51 | func NewGenerator(from, to string, offset, games, threads, nodes, depth int, winThreshold eval.Eval) (*Generator, error) { 52 | // Open the opening source epd file. 53 | i, err := os.Open(from) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | // Open the data target file. 59 | o, err := os.OpenFile(to, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | // Get most effective hash size for the node count. 65 | hashSize := util.Clamp((nodes*15)/(1024*1024), 1, 256) 66 | log.Printf("hash size used is %d mb\n", hashSize) 67 | 68 | // Create a new instance of a Generator. 69 | return &Generator{ 70 | Input: bufio.NewScanner(i), 71 | Output: bufio.NewWriterSize(o, 2000*100), 72 | 73 | Offset: offset, 74 | 75 | Openings: make(chan string), 76 | Data: make(chan DataPoint), 77 | Deaths: make(chan int), 78 | 79 | Games: games, 80 | Threads: threads, 81 | 82 | Nodes: nodes, 83 | Depth: depth, 84 | 85 | HashSize: hashSize, 86 | 87 | WinThreshold: winThreshold, 88 | }, nil 89 | } 90 | 91 | type Generator struct { 92 | // Input and Output files. 93 | Input *bufio.Scanner 94 | Output *bufio.Writer 95 | 96 | // Opening Offset in Input. 97 | Offset int 98 | 99 | // Sync channels. 100 | Openings chan string 101 | Data chan DataPoint 102 | Deaths chan int 103 | 104 | // Number of games done. 105 | Done int 106 | 107 | // Generator Configuration. 108 | 109 | Games int // Total number of games to play. 110 | Threads int // Total number of threads to use. 111 | 112 | // Threshold for win adjudication. 113 | WinThreshold eval.Eval 114 | 115 | Nodes int // Node limit for searches. 116 | Depth int // Depth limit for searches. 117 | 118 | HashSize int // Most efficient hash size. 119 | } 120 | 121 | func (generator *Generator) GenerateData() { 122 | log.Printf("starting %d workers\n", generator.Threads) 123 | for i := 1; i <= generator.Threads; i++ { 124 | go generator.StartWorker(i) 125 | } 126 | 127 | log.Printf("playing %d games\n", generator.Games) 128 | go generator.ScheduleWork() 129 | 130 | start := time.Now() 131 | datapoints := 0 132 | deaths := 0 133 | 134 | for { 135 | select { 136 | case data := <-generator.Data: 137 | _, _ = generator.Output.WriteString(data.String()) 138 | datapoints++ 139 | 140 | if datapoints&4095 == 0 { 141 | delta := int(time.Since(start).Seconds()) + 1 142 | log.Printf( 143 | "%10d fens [%4d fens/second] %8d games [%2d games/second] [%3d fens/game]\n", 144 | datapoints, datapoints/delta, generator.Done, generator.Done/delta, datapoints/generator.Done, 145 | ) 146 | } 147 | 148 | case <-generator.Deaths: 149 | if deaths++; deaths == generator.Threads { 150 | close(generator.Deaths) 151 | close(generator.Data) 152 | 153 | _ = generator.Output.Flush() 154 | 155 | log.Println("all workers are done") 156 | return 157 | } 158 | } 159 | } 160 | } 161 | 162 | func (generator *Generator) ScheduleWork() { 163 | for i, openings := 0, 0; openings < generator.Games && generator.Input.Scan(); i++ { 164 | if i >= generator.Offset { 165 | openings++ 166 | generator.Openings <- generator.Input.Text() 167 | } 168 | } 169 | 170 | close(generator.Openings) 171 | } 172 | 173 | func (generator *Generator) StartWorker(id int) { 174 | data := make([]DataPoint, 0) 175 | 176 | limits := search.Limits{ 177 | Depth: generator.Depth, 178 | Nodes: generator.Nodes, 179 | Infinite: true, 180 | } 181 | 182 | worker := search.NewContext(func(report search.Report) {}, generator.HashSize) 183 | 184 | for opening := range generator.Openings { 185 | worker.UpdatePosition(fen.FromString(opening)) 186 | 187 | board := worker.Board() 188 | 189 | data = data[:0] 190 | var result = float32(0.5) 191 | 192 | for { 193 | if board.DrawClock >= 100 || 194 | board.IsRepetition() { 195 | break 196 | } 197 | 198 | pv, score, _ := worker.Search(limits) 199 | 200 | score = util.Ternary(board.SideToMove == piece.White, score, -score) 201 | bestMove := pv.Move(0) 202 | 203 | if bestMove == move.Null || util.Abs(score) >= generator.WinThreshold { 204 | result = util.Ternary[float32](score > eval.Draw, 1.0, 0.0) 205 | break 206 | } 207 | 208 | // Position Filters: Tactical Positions 209 | if !bestMove.IsQuiet() && !board.IsInCheck(board.SideToMove) { 210 | goto nextMove 211 | } 212 | 213 | data = append(data, DataPoint{ 214 | FEN: board.FEN().String(), 215 | Score: score, 216 | }) 217 | 218 | nextMove: 219 | worker.MakeMove(bestMove) 220 | } 221 | 222 | for i := 0; i < len(data); i++ { 223 | data[i].Result = result 224 | generator.Data <- data[i] 225 | } 226 | 227 | generator.Done++ 228 | } 229 | 230 | generator.Deaths <- id 231 | } 232 | 233 | type DataPoint struct { 234 | FEN string 235 | Score eval.Eval 236 | Result float32 237 | } 238 | 239 | func (data *DataPoint) String() string { 240 | return fmt.Sprintf("%s | %d | %.1f\n", data.FEN, data.Score, data.Result) 241 | } 242 | -------------------------------------------------------------------------------- /pkg/board/move.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Rak Laptudirm 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package board 15 | 16 | import ( 17 | "strings" 18 | 19 | "laptudirm.com/x/mess/internal/util" 20 | "laptudirm.com/x/mess/pkg/board/move" 21 | "laptudirm.com/x/mess/pkg/board/move/attacks" 22 | "laptudirm.com/x/mess/pkg/board/move/castling" 23 | "laptudirm.com/x/mess/pkg/board/piece" 24 | "laptudirm.com/x/mess/pkg/board/square" 25 | "laptudirm.com/x/mess/pkg/board/zobrist" 26 | ) 27 | 28 | // MakeMove plays the given legal move on the Board. 29 | func (b *Board) MakeMove(m move.Move) { 30 | // add current state to history 31 | b.History[b.Plys].Move = m 32 | b.History[b.Plys].CastlingRights = b.CastlingRights 33 | b.History[b.Plys].CapturedPiece = piece.NoPiece 34 | b.History[b.Plys].EnPassantTarget = b.EnPassantTarget 35 | b.History[b.Plys].DrawClock = b.DrawClock 36 | b.History[b.Plys].Hash = b.Hash 37 | 38 | // update the half-move clock 39 | // it records the number of plys since the last pawn push or capture 40 | // for positions which are drawn by the 50-move rule 41 | b.DrawClock++ 42 | 43 | // parse move 44 | 45 | if m == move.Null { 46 | b.makeNullMove() 47 | return 48 | } 49 | 50 | sourceSq := m.Source() 51 | targetSq := m.Target() 52 | captureSq := targetSq 53 | fromPiece := m.FromPiece() 54 | pieceType := fromPiece.Type() 55 | toPiece := m.ToPiece() 56 | 57 | isDoublePush := pieceType == piece.Pawn && util.Abs(targetSq-sourceSq) == 16 58 | isCastling := pieceType == piece.King && util.Abs(targetSq-sourceSq) == 2 59 | isEnPassant := pieceType == piece.Pawn && targetSq == b.EnPassantTarget 60 | isCapture := m.IsCapture() 61 | 62 | if pieceType == piece.Pawn { 63 | b.DrawClock = 0 64 | } 65 | 66 | // update en passant target square 67 | if b.EnPassantTarget != square.None { 68 | b.Hash ^= zobrist.EnPassant[b.EnPassantTarget.File()] // reset hash 69 | } 70 | b.EnPassantTarget = square.None // reset square 71 | 72 | switch { 73 | case isDoublePush: 74 | // double pawn push; set new en passant target 75 | target := sourceSq 76 | if b.SideToMove == piece.White { 77 | target -= 8 78 | } else { 79 | target += 8 80 | } 81 | 82 | // only set en passant square if an enemy pawn can capture it 83 | if b.PawnsBB(b.SideToMove.Other())&attacks.Pawn[b.SideToMove][target] != 0 { 84 | b.EnPassantTarget = target 85 | // and new square to zobrist hash 86 | b.Hash ^= zobrist.EnPassant[b.EnPassantTarget.File()] 87 | } 88 | 89 | case isCastling: 90 | // castle the rook 91 | rookInfo := castling.Rooks[targetSq] 92 | b.ClearSquare(rookInfo.From) 93 | b.FillSquare(rookInfo.To, rookInfo.RookType) 94 | 95 | case isEnPassant: 96 | // capture square is different from target square during en passant 97 | if b.SideToMove == piece.White { 98 | captureSq += 8 99 | } else { 100 | captureSq -= 8 101 | } 102 | fallthrough 103 | 104 | case isCapture: 105 | b.History[b.Plys].CapturedPiece = b.Position[captureSq] // put captured piece in history 106 | b.DrawClock = 0 // reset draw clock since capture 107 | b.ClearSquare(captureSq) // clear the captured square 108 | } 109 | 110 | // move the piece 111 | b.ClearSquare(sourceSq) 112 | b.FillSquare(targetSq, toPiece) 113 | 114 | b.Hash ^= zobrist.Castling[b.CastlingRights] // remove old rights 115 | b.CastlingRights &^= castling.RightUpdates[sourceSq] 116 | b.CastlingRights &^= castling.RightUpdates[targetSq] 117 | b.Hash ^= zobrist.Castling[b.CastlingRights] // put new rights 118 | 119 | // switch turn 120 | b.Plys++ 121 | 122 | // update side to move 123 | if b.SideToMove = b.SideToMove.Other(); b.SideToMove == piece.White { 124 | b.FullMoves++ 125 | } 126 | b.Hash ^= zobrist.SideToMove // switch in zobrist hash 127 | } 128 | 129 | func (b *Board) makeNullMove() { 130 | // update en passant target square 131 | if b.EnPassantTarget != square.None { 132 | b.Hash ^= zobrist.EnPassant[b.EnPassantTarget.File()] // reset hash 133 | } 134 | b.EnPassantTarget = square.None // reset square 135 | 136 | // switch turn 137 | b.Plys++ 138 | 139 | // update side to move 140 | if b.SideToMove = b.SideToMove.Other(); b.SideToMove == piece.White { 141 | b.FullMoves++ 142 | } 143 | b.Hash ^= zobrist.SideToMove // switch in zobrist hash 144 | } 145 | 146 | // UnmakeMove unmakes the last move played on the Board. 147 | func (b *Board) UnmakeMove() { 148 | if b.SideToMove = b.SideToMove.Other(); b.SideToMove == piece.Black { 149 | b.FullMoves-- 150 | } 151 | 152 | b.Plys-- 153 | 154 | b.EnPassantTarget = b.History[b.Plys].EnPassantTarget 155 | b.DrawClock = b.History[b.Plys].DrawClock 156 | b.CastlingRights = b.History[b.Plys].CastlingRights 157 | 158 | m := b.History[b.Plys].Move 159 | 160 | // parse move 161 | 162 | if m == move.Null { 163 | // use the hash stored in history 164 | b.Hash = b.History[b.Plys].Hash 165 | return 166 | } 167 | 168 | sourceSq := m.Source() 169 | targetSq := m.Target() 170 | captureSq := targetSq 171 | fromPiece := m.FromPiece() 172 | pieceType := fromPiece.Type() 173 | capturedPiece := b.History[b.Plys].CapturedPiece 174 | 175 | isCastling := pieceType == piece.King && util.Abs(targetSq-sourceSq) == 2 176 | isEnPassant := pieceType == piece.Pawn && targetSq == b.EnPassantTarget 177 | isCapture := m.IsCapture() 178 | 179 | // un-move the piece 180 | b.ClearSquare(targetSq) 181 | b.FillSquare(sourceSq, fromPiece) 182 | 183 | switch { 184 | case isCastling: 185 | // un-castle the rook 186 | rookInfo := castling.Rooks[targetSq] 187 | b.ClearSquare(rookInfo.To) 188 | b.FillSquare(rookInfo.From, rookInfo.RookType) 189 | 190 | case isEnPassant: 191 | // capture square is different from target square during en passant 192 | if b.SideToMove == piece.White { 193 | captureSq += 8 194 | } else { 195 | captureSq -= 8 196 | } 197 | fallthrough 198 | 199 | case isCapture: 200 | // put the captured piece back 201 | b.FillSquare(captureSq, capturedPiece) 202 | } 203 | 204 | // use the hash stored in history 205 | b.Hash = b.History[b.Plys].Hash 206 | } 207 | 208 | // NewMove returns a new move.Move representing moving a piece from `from` 209 | // to `to` by adding the necessary contextual information from the Board. 210 | // If the move is a promotion, the promotion square can be set using the 211 | // (move).SetPromotion(piece.Piece) method. 212 | func (b *Board) NewMove(from, to square.Square) move.Move { 213 | p := b.Position[from] 214 | return move.New(from, to, p, b.Position[to] != piece.NoPiece) 215 | } 216 | 217 | func (b *Board) NewMoveFromString(m string) move.Move { 218 | from := square.NewFromString(m[:2]) 219 | to := square.NewFromString(m[2:4]) 220 | 221 | move := b.NewMove(from, to) 222 | if len(m) == 5 { 223 | pieceID := m[4:] 224 | if b.SideToMove == piece.White { 225 | pieceID = strings.ToUpper(pieceID) 226 | } 227 | 228 | move = move.SetPromotion(piece.NewFromString(pieceID)) 229 | } 230 | 231 | return move 232 | } 233 | --------------------------------------------------------------------------------