Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

engine.gno

15.91 Kb · 671 lines
  1package minichess
  2
  3// engine is the mutable, allocation-light core used by move generation and
  4// the bot. GnoVM charges gas per operation (roughly 1k gas per loop
  5// iteration, ~4k per append), so the engine uses precomputed tables, piece
  6// lists (only occupied squares are visited), integer-encoded moves written
  7// into reused buffers, in-place make/unmake and an incrementally updated
  8// evaluation.
  9
 10// Encoded move: from | to<<6 | promo<<12.
 11func enc(from, to int, promo Piece) int { return from | to<<6 | int(promo)<<12 }
 12
 13func decode(m int) Move {
 14	return Move{From: m & 63, To: (m >> 6) & 63, Promo: Piece(m >> 12)}
 15}
 16
 17func (m Move) encode() int { return enc(m.From, m.To, m.Promo) }
 18
 19const bufSize = 256 // more than the maximum number of pseudo-legal moves
 20
 21// Lookup tables. A deployed package pays a storage deposit for everything
 22// it persists, so target-square lists are stored as strings (one byte per
 23// square): compact on-chain, and ranging over them costs about the same
 24// gas as ranging over a slice.
 25var (
 26	knightT [NumSquares]string
 27	kingT   [NumSquares]string
 28	rays    [NumSquares * 8]string // [sq*8+dir]; dirs 0-3 orthogonal, 4-7 diagonal
 29	pawnAtk [2 * NumSquares]string // [c*64+sq]: squares a pawn of colour c on sq attacks
 30	psqt    [13][NumSquares]int    // [piece+6][sq], signed (white +)
 31
 32	// geo[a*64+b] flags: geoLine (shared rank/file/diagonal), geoKnight,
 33	// geoKing (adjacent).
 34	geo string
 35)
 36
 37const (
 38	geoLine   byte = 1
 39	geoKnight byte = 2
 40	geoKing   byte = 4
 41)
 42
 43var pieceValue = [7]int{0, 100, 320, 330, 500, 900, 0}
 44
 45func inside(r, f int) bool { return r >= 0 && r < Size && f >= 0 && f < Size }
 46
 47// init fills the lookup tables. Pure packages are immutable after init, so
 48// the tables are built once here and only read afterwards.
 49func init() {
 50	dirs8 := [8][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}
 51	knightD := [8][2]int{{1, 2}, {2, 1}, {2, -1}, {1, -2}, {-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}}
 52
 53	// Piece-square tables ("simplified evaluation function"), written
 54	// from white's point of view with rank 8 first.
 55	pst := [7][64]int{
 56		{},
 57		{ // pawn
 58			0, 0, 0, 0, 0, 0, 0, 0,
 59			50, 50, 50, 50, 50, 50, 50, 50,
 60			10, 10, 20, 30, 30, 20, 10, 10,
 61			5, 5, 10, 25, 25, 10, 5, 5,
 62			0, 0, 0, 20, 20, 0, 0, 0,
 63			5, -5, -10, 0, 0, -10, -5, 5,
 64			5, 10, 10, -20, -20, 10, 10, 5,
 65			0, 0, 0, 0, 0, 0, 0, 0,
 66		},
 67		{ // knight
 68			-50, -40, -30, -30, -30, -30, -40, -50,
 69			-40, -20, 0, 0, 0, 0, -20, -40,
 70			-30, 0, 10, 15, 15, 10, 0, -30,
 71			-30, 5, 15, 20, 20, 15, 5, -30,
 72			-30, 0, 15, 20, 20, 15, 0, -30,
 73			-30, 5, 10, 15, 15, 10, 5, -30,
 74			-40, -20, 0, 5, 5, 0, -20, -40,
 75			-50, -40, -30, -30, -30, -30, -40, -50,
 76		},
 77		{ // bishop
 78			-20, -10, -10, -10, -10, -10, -10, -20,
 79			-10, 0, 0, 0, 0, 0, 0, -10,
 80			-10, 0, 5, 10, 10, 5, 0, -10,
 81			-10, 5, 5, 10, 10, 5, 5, -10,
 82			-10, 0, 10, 10, 10, 10, 0, -10,
 83			-10, 10, 10, 10, 10, 10, 10, -10,
 84			-10, 5, 0, 0, 0, 0, 5, -10,
 85			-20, -10, -10, -10, -10, -10, -10, -20,
 86		},
 87		{ // rook
 88			0, 0, 0, 0, 0, 0, 0, 0,
 89			5, 10, 10, 10, 10, 10, 10, 5,
 90			-5, 0, 0, 0, 0, 0, 0, -5,
 91			-5, 0, 0, 0, 0, 0, 0, -5,
 92			-5, 0, 0, 0, 0, 0, 0, -5,
 93			-5, 0, 0, 0, 0, 0, 0, -5,
 94			-5, 0, 0, 0, 0, 0, 0, -5,
 95			0, 0, 0, 5, 5, 0, 0, 0,
 96		},
 97		{ // queen
 98			-20, -10, -10, -5, -5, -10, -10, -20,
 99			-10, 0, 0, 0, 0, 0, 0, -10,
100			-10, 0, 5, 5, 5, 5, 0, -10,
101			-5, 0, 5, 5, 5, 5, 0, -5,
102			0, 0, 5, 5, 5, 5, 0, -5,
103			-10, 5, 5, 5, 5, 5, 0, -10,
104			-10, 0, 5, 0, 0, 0, 0, -10,
105			-20, -10, -10, -5, -5, -10, -10, -20,
106		},
107		{ // king (middlegame)
108			-30, -40, -40, -50, -50, -40, -40, -30,
109			-30, -40, -40, -50, -50, -40, -40, -30,
110			-30, -40, -40, -50, -50, -40, -40, -30,
111			-30, -40, -40, -50, -50, -40, -40, -30,
112			-20, -30, -30, -40, -40, -30, -30, -20,
113			-10, -20, -20, -20, -20, -20, -20, -10,
114			20, 20, 0, 0, 0, 0, 20, 20,
115			20, 30, 10, 0, 0, 10, 30, 20,
116		},
117	}
118
119	sqs := func(list []int) string {
120		b := make([]byte, len(list))
121		for i, v := range list {
122			b[i] = byte(v)
123		}
124		return string(b)
125	}
126	for sq := 0; sq < NumSquares; sq++ {
127		r, f := sq/Size, sq%Size
128		var kn, kg []int
129		for _, d := range knightD {
130			if inside(r+d[0], f+d[1]) {
131				kn = append(kn, (r+d[0])*Size+f+d[1])
132			}
133		}
134		for i, d := range dirs8 {
135			if inside(r+d[0], f+d[1]) {
136				kg = append(kg, (r+d[0])*Size+f+d[1])
137			}
138			var ray []int
139			nr, nf := r+d[0], f+d[1]
140			for inside(nr, nf) {
141				ray = append(ray, nr*Size+nf)
142				nr += d[0]
143				nf += d[1]
144			}
145			rays[sq*8+i] = sqs(ray)
146		}
147		knightT[sq] = sqs(kn)
148		kingT[sq] = sqs(kg)
149		for c := 0; c < 2; c++ {
150			nr := r + 1
151			if c == 1 {
152				nr = r - 1
153			}
154			var pa []int
155			for _, df := range [2]int{-1, 1} {
156				if inside(nr, f+df) {
157					pa = append(pa, nr*Size+f+df)
158				}
159			}
160			pawnAtk[c*NumSquares+sq] = sqs(pa)
161		}
162		for t := 1; t <= 6; t++ {
163			v := pieceValue[t] + pst[t][(Size-1-r)*Size+f]
164			psqt[6+t][sq] = v
165			psqt[6-t][(Size-1-r)*Size+f] = -v
166		}
167	}
168
169	g8 := make([]byte, NumSquares*NumSquares)
170	for a := 0; a < NumSquares; a++ {
171		for b := 0; b < NumSquares; b++ {
172			if a == b {
173				continue
174			}
175			dr, df := a/Size-b/Size, a%Size-b%Size
176			var g byte
177			if dr == 0 || df == 0 || dr == df || dr == -df {
178				g |= geoLine
179			}
180			if dr*df == 2 || dr*df == -2 {
181				g |= geoKnight
182			}
183			if dr >= -1 && dr <= 1 && df >= -1 && df <= 1 {
184				g |= geoKing
185			}
186			g8[a*NumSquares+b] = g
187		}
188	}
189	geo = string(g8)
190}
191
192// castleKeep returns the castling rights that survive a move touching sq.
193func castleKeep(sq int) int {
194	switch sq {
195	case 0:
196		return ^CastleWQ
197	case 4:
198		return ^(CastleWK | CastleWQ)
199	case 7:
200		return ^CastleWK
201	case 56:
202		return ^CastleBQ
203	case 60:
204		return ^(CastleBK | CastleBQ)
205	case 63:
206		return ^CastleBK
207	}
208	return ^0
209}
210
211type undo struct {
212	moved    Piece
213	captured Piece
214	capSq    int
215	capIdx   int
216	castle   int
217	ep       int
218	half     int
219	score    int
220	special  int // 0 normal, 1 en passant, 2 castling
221}
222
223// plyBuf holds the moves generated at one search ply.
224type plyBuf struct {
225	caps   []int
226	quiets []int
227	nc, nq int
228}
229
230func newPlyBuf() *plyBuf {
231	return &plyBuf{caps: make([]int, bufSize), quiets: make([]int, bufSize)}
232}
233
234type engine struct {
235	b      [NumSquares]Piece
236	turn   Color
237	castle int
238	ep     int
239	half   int
240	kings  [2]int
241	score  int // white-perspective evaluation, updated incrementally
242
243	// Piece lists: squares of each side's pieces, and each occupied
244	// square's index in its list.
245	plist  [2][16]int
246	pcount [2]int
247	pidx   [NumSquares]int
248
249	nodes   int
250	budget  int
251	qdepth  int
252	aborted bool
253	bufs    []*plyBuf
254	killers [maxPly + 2][2]int // quiet moves that caused a cutoff, per ply
255}
256
257func newEngine(p *Position) *engine {
258	e := &engine{b: p.Board, turn: p.Turn, castle: p.Castling, ep: p.EP, half: p.Halfmove}
259	for sq := 0; sq < NumSquares; sq++ {
260		pc := e.b[sq]
261		if pc == Empty {
262			continue
263		}
264		e.score += psqt[pc+6][sq]
265		c := 0
266		if pc < 0 {
267			c = 1
268		}
269		if pc.Type() == King {
270			e.kings[c] = sq
271		}
272		if e.pcount[c] < 16 {
273			e.pidx[sq] = e.pcount[c]
274			e.plist[c][e.pcount[c]] = sq
275			e.pcount[c]++
276		}
277	}
278	return e
279}
280
281func (e *engine) buf(ply int) *plyBuf {
282	for len(e.bufs) <= ply {
283		e.bufs = append(e.bufs, newPlyBuf())
284	}
285	return e.bufs[ply]
286}
287
288// attacked reports whether sq is attacked by colour by.
289func (e *engine) attacked(sq int, by Color) bool {
290	sign := Piece(1)
291	if by == Black {
292		sign = -1
293	}
294	b := &e.b
295	// A pawn of colour `by` attacks sq if it sits on a square that a pawn
296	// of the other colour on sq would attack.
297	pw := sign * Pawn
298	for _, ch := range pawnAtk[int(1-by)*NumSquares+sq] {
299		if b[int(ch)] == pw {
300			return true
301		}
302	}
303	kn := sign * Knight
304	for _, ch := range knightT[sq] {
305		if b[int(ch)] == kn {
306			return true
307		}
308	}
309	kg := sign * King
310	for _, ch := range kingT[sq] {
311		if b[int(ch)] == kg {
312			return true
313		}
314	}
315	q := sign * Queen
316	slider := sign * Rook
317	for d := 0; d < 8; d++ {
318		if d == 4 {
319			slider = sign * Bishop
320		}
321		for _, ch := range rays[sq*8+d] {
322			pc := b[int(ch)]
323			if pc != Empty {
324				if pc == slider || pc == q {
325					return true
326				}
327				break
328			}
329		}
330	}
331	return false
332}
333
334func (e *engine) inCheck() bool {
335	return e.attacked(e.kings[e.turn], 1-e.turn)
336}
337
338// gen fills pb with pseudo-legal moves: captures and queen promotions in
339// pb.caps, everything else in pb.quiets (skipped when capsOnly).
340func (e *engine) gen(pb *plyBuf, capsOnly bool) {
341	caps, quiets := pb.caps, pb.quiets
342	nc, nq := 0, 0
343	b := &e.b
344	us := e.turn
345	sign := Piece(1)
346	fwd, startRank, promoRank := Size, 1, Size-1
347	if us == Black {
348		sign, fwd, startRank, promoRank = -1, -Size, Size-2, 0
349	}
350	list := &e.plist[us]
351	for i := 0; i < e.pcount[us]; i++ {
352		sq := list[i]
353		pc := b[sq] * sign
354		switch pc {
355		case Pawn:
356			to := sq + fwd
357			promo := to/Size == promoRank
358			if b[to] == Empty {
359				if promo {
360					caps[nc] = enc(sq, to, Queen)
361					nc++
362					if !capsOnly {
363						quiets[nq] = enc(sq, to, Knight)
364						quiets[nq+1] = enc(sq, to, Rook)
365						quiets[nq+2] = enc(sq, to, Bishop)
366						nq += 3
367					}
368				} else if !capsOnly {
369					quiets[nq] = sq | to<<6
370					nq++
371					if sq/Size == startRank && b[to+fwd] == Empty {
372						quiets[nq] = sq | (to+fwd)<<6
373						nq++
374					}
375				}
376			}
377			for _, ch := range pawnAtk[int(us)*NumSquares+sq] {
378				t := int(ch)
379				if b[t]*sign < 0 {
380					if promo {
381						caps[nc] = enc(sq, t, Queen)
382						nc++
383						if !capsOnly {
384							quiets[nq] = enc(sq, t, Knight)
385							quiets[nq+1] = enc(sq, t, Rook)
386							quiets[nq+2] = enc(sq, t, Bishop)
387							nq += 3
388						}
389					} else {
390						caps[nc] = sq | t<<6
391						nc++
392					}
393				} else if t == e.ep {
394					caps[nc] = sq | t<<6
395					nc++
396				}
397			}
398		case Knight, King:
399			targets := knightT[sq]
400			if pc == King {
401				targets = kingT[sq]
402			}
403			for _, ch := range targets {
404				t := int(ch)
405				v := b[t]
406				if v == Empty {
407					if !capsOnly {
408						quiets[nq] = sq | t<<6
409						nq++
410					}
411				} else if v*sign < 0 {
412					caps[nc] = sq | t<<6
413					nc++
414				}
415			}
416		default: // sliders
417			lo, hi := 0, 8
418			if pc == Rook {
419				hi = 4
420			} else if pc == Bishop {
421				lo = 4
422			}
423			for d := lo; d < hi; d++ {
424				for _, ch := range rays[sq*8+d] {
425					t := int(ch)
426					v := b[t]
427					if v == Empty {
428						if !capsOnly {
429							quiets[nq] = sq | t<<6
430							nq++
431						}
432						continue
433					}
434					if v*sign < 0 {
435						caps[nc] = sq | t<<6
436						nc++
437					}
438					break
439				}
440			}
441		}
442	}
443	if !capsOnly && e.castle != 0 {
444		nq = e.genCastles(quiets, nq)
445	}
446	pb.nc, pb.nq = nc, nq
447}
448
449// genCastles adds castling moves. The king may not castle out of, through
450// or into check, and the squares between king and rook must be empty.
451func (e *engine) genCastles(quiets []int, nq int) int {
452	b := &e.b
453	them := 1 - e.turn
454	base, rook, kRight, qRight := 0, Rook, CastleWK, CastleWQ
455	if e.turn == Black {
456		base, rook, kRight, qRight = 56, -Rook, CastleBK, CastleBQ
457	}
458	k := base + 4
459	if e.castle&kRight != 0 && b[base+5] == Empty && b[base+6] == Empty && b[base+7] == rook &&
460		!e.attacked(k, them) && !e.attacked(k+1, them) && !e.attacked(k+2, them) {
461		quiets[nq] = k | (k+2)<<6
462		nq++
463	}
464	if e.castle&qRight != 0 && b[base+3] == Empty && b[base+2] == Empty && b[base+1] == Empty &&
465		b[base] == rook && !e.attacked(k, them) && !e.attacked(k-1, them) && !e.attacked(k-2, them) {
466		quiets[nq] = k | (k-2)<<6
467		nq++
468	}
469	return nq
470}
471
472// movePiece updates the board and piece list for a piece moving from->to.
473func (e *engine) movePiece(c Color, from, to int) {
474	i := e.pidx[from]
475	e.plist[c][i] = to
476	e.pidx[to] = i
477	e.b[to] = e.b[from]
478	e.b[from] = Empty
479}
480
481// removePiece takes the piece on sq out of colour c's list; returns its
482// former index so restorePiece can undo it exactly.
483func (e *engine) removePiece(c Color, sq int) int {
484	i := e.pidx[sq]
485	n := e.pcount[c] - 1
486	last := e.plist[c][n]
487	e.plist[c][i] = last
488	e.pidx[last] = i
489	e.pcount[c] = n
490	return i
491}
492
493func (e *engine) restorePiece(c Color, sq, i int) {
494	n := e.pcount[c]
495	moved := e.plist[c][i]
496	e.plist[c][n] = moved
497	e.pidx[moved] = n
498	e.plist[c][i] = sq
499	e.pidx[sq] = i
500	e.pcount[c] = n + 1
501}
502
503func (e *engine) make(m int) undo {
504	from, to := m&63, (m>>6)&63
505	us, them := e.turn, 1-e.turn
506	pc := e.b[from]
507	pt := pc.Type()
508	u := undo{moved: pc, capSq: to, castle: e.castle, ep: e.ep, half: e.half, score: e.score}
509
510	capt := e.b[to]
511	if pt == Pawn && to == e.ep && capt == Empty && (from&7) != (to&7) {
512		// En passant: the captured pawn is behind the target square.
513		u.special = 1
514		if us == White {
515			u.capSq = to - Size
516		} else {
517			u.capSq = to + Size
518		}
519		capt = e.b[u.capSq]
520	}
521	u.captured = capt
522	if capt != Empty {
523		u.capIdx = e.removePiece(them, u.capSq)
524		e.score -= psqt[capt+6][u.capSq]
525		e.b[u.capSq] = Empty
526	}
527
528	e.movePiece(us, from, to)
529	np := pc
530	if m >= 4096 {
531		np = Piece(m >> 12)
532		if pc < 0 {
533			np = -np
534		}
535		e.b[to] = np
536	}
537	e.score += psqt[np+6][to] - psqt[pc+6][from]
538
539	if pt == King {
540		e.kings[us] = to
541		if to-from == 2 || from-to == 2 {
542			u.special = 2
543			rf, rt := from+3, from+1
544			if to < from {
545				rf, rt = from-4, from-1
546			}
547			rk := e.b[rf]
548			e.movePiece(us, rf, rt)
549			e.score += psqt[rk+6][rt] - psqt[rk+6][rf]
550		}
551	}
552	if e.castle != 0 {
553		e.castle &= castleKeep(from) & castleKeep(to)
554	}
555	e.ep = -1
556	if pt == Pawn && (to-from == 16 || from-to == 16) {
557		e.ep = (from + to) / 2
558	}
559	if pt == Pawn || capt != Empty {
560		e.half = 0
561	} else {
562		e.half++
563	}
564	e.turn = them
565	return u
566}
567
568func (e *engine) unmake(m int, u undo) {
569	from, to := m&63, (m>>6)&63
570	them := e.turn
571	us := 1 - them
572	e.turn = us
573	if u.special == 2 {
574		rf, rt := from+3, from+1
575		if to < from {
576			rf, rt = from-4, from-1
577		}
578		e.movePiece(us, rt, rf)
579	}
580	e.movePiece(us, to, from)
581	e.b[from] = u.moved
582	if u.moved == King || u.moved == -King {
583		e.kings[us] = from
584	}
585	if u.captured != Empty {
586		e.b[u.capSq] = u.captured
587		e.restorePiece(them, u.capSq, u.capIdx)
588	}
589	e.castle, e.ep, e.half, e.score = u.castle, u.ep, u.half, u.score
590}
591
592// moveIsLegal is called right after make(m) (so e.turn is the opponent).
593// wasInCheck says whether the mover was in check before m. A normal
594// non-king move from a square not lined up with its own king cannot
595// expose that king, so the attack test is skipped.
596func (e *engine) moveIsLegal(m int, u undo, wasInCheck bool) bool {
597	k := e.kings[1-e.turn]
598	if !wasInCheck && u.special == 0 && u.moved != King && u.moved != -King &&
599		geo[k*NumSquares+m&63]&geoLine == 0 {
600		return true
601	}
602	return !e.attacked(k, e.turn)
603}
604
605// givesCheck is called right after make(m): does the side to move now
606// stand in check? Cheap geometric filters avoid most attack tests.
607func (e *engine) givesCheck(m int, u undo) bool {
608	ek := e.kings[e.turn]
609	from, to := m&63, (m>>6)&63
610	g := ek * NumSquares
611	maybe := u.special != 0 || geo[g+from]&geoLine != 0 // castling, e.p., discovery
612	if !maybe {
613		switch e.b[to].Type() {
614		case Knight:
615			maybe = geo[g+to]&geoKnight != 0
616		case Pawn:
617			maybe = geo[g+to]&geoKing != 0
618		case King:
619		default:
620			maybe = geo[g+to]&geoLine != 0
621		}
622	}
623	return maybe && e.attacked(ek, 1-e.turn)
624}
625
626// legal returns the legal moves (captures first) as encoded ints.
627func (e *engine) legal() []int {
628	pb := newPlyBuf()
629	e.gen(pb, false)
630	out := make([]int, 0, pb.nc+pb.nq)
631	for i := 0; i < pb.nc+pb.nq; i++ {
632		m := 0
633		if i < pb.nc {
634			m = pb.caps[i]
635		} else {
636			m = pb.quiets[i-pb.nc]
637		}
638		u := e.make(m)
639		if !e.attacked(e.kings[1-e.turn], e.turn) {
640			out = append(out, m)
641		}
642		e.unmake(m, u)
643	}
644	return out
645}
646
647// mix is a splitmix64-style mixer used to build position hashes.
648func mix(x uint64) uint64 {
649	x += 0x9e3779b97f4a7c15
650	x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9
651	x = (x ^ (x >> 27)) * 0x94d049bb133111eb
652	return x ^ (x >> 31)
653}
654
655// hash is a Zobrist-style hash of the position (pieces, side to move,
656// castling rights, en passant square), used to detect repetitions.
657func (e *engine) hash() uint64 {
658	var h uint64
659	for c := 0; c < 2; c++ {
660		for i := 0; i < e.pcount[c]; i++ {
661			sq := e.plist[c][i]
662			h ^= mix(uint64(int(e.b[sq]+6)*NumSquares + sq))
663		}
664	}
665	return h ^ mix(uint64(1000+int(e.turn)+2*e.castle+32*(e.ep+1)))
666}
667
668// position converts the engine state back to a Position.
669func (e *engine) position(fullmove int) Position {
670	return Position{Board: e.b, Turn: e.turn, Castling: e.castle, EP: e.ep, Halfmove: e.half, Fullmove: fullmove}
671}