Skip to content

Commit 60b3ccb

Browse files
authored
Merge pull request #4 from lovc21/Feat/add_uci_and_gui
Feat/add uci and gui
2 parents e98e9ca + ef4dcce commit 60b3ccb

3 files changed

Lines changed: 326 additions & 10 deletions

File tree

src/main.zig

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ const move_gen = @import("move_generation.zig");
99
const uci = @import("uci.zig");
1010

1111
pub fn main() !void {
12-
attacks.init_attacks();
13-
14-
var b = types.Board.new();
15-
try bitboard.fan_pars(types.start_position, &b);
16-
17-
bitboard.print_unicode_board(b);
12+
// attacks.init_attacks();
13+
//
14+
// var b = types.Board.new();
15+
// try bitboard.fan_pars(types.start_position, &b);
16+
//
17+
// bitboard.print_unicode_board(b);
1818

1919
// var movesWhite: lists.MoveList = .{};
2020
// move_gen.generate_moves(&b, &movesWhite, types.Color.Black);
@@ -34,9 +34,10 @@ pub fn main() !void {
3434
// print("Illegal move made", .{});
3535
// }
3636
// }
37-
//
37+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
38+
defer _ = gpa.deinit();
39+
const allocator = gpa.allocator();
3840

39-
//util.perft_test(&b, 5);
40-
//
41-
util.perft_test_detailed(&b, 5);
41+
var game = uci.UCI.new(allocator);
42+
try game.uci_loop();
4243
}

src/search.zig

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const std = @import("std");
2+
const types = @import("types.zig");
3+
const print = std.debug.print;
4+
5+
pub fn search_position(uci_ref: anytype, depth: ?u8, time_ms: u64) void {
6+
_ = depth;
7+
_ = time_ms;
8+
9+
uci_ref.is_searching = true;
10+
print("bestmove {s}\n", .{"e2e4"});
11+
12+
uci_ref.is_searching = false;
13+
}

src/uci.zig

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
const std = @import("std");
2+
const attacks = @import("attacks.zig");
3+
const types = @import("types.zig");
4+
const util = @import("util.zig");
5+
const bitboard = @import("bitboard.zig");
6+
const move_gen = @import("move_generation.zig");
7+
const print = std.debug.print;
8+
const search = @import("search.zig");
9+
const lists = @import("lists.zig");
10+
11+
const UCI_COMMANDS_MAX: usize = 10000;
12+
const VERSION: []const u8 = "0.1";
13+
const ENGINE_NAME: []const u8 = "NeuroSpeed";
14+
const AUTHOR: []const u8 = "Jakob Dekleva";
15+
16+
pub const UCI = struct {
17+
board: types.Board,
18+
allocator: std.mem.Allocator,
19+
is_searching: bool,
20+
stop_search: bool,
21+
search_thread: ?std.Thread,
22+
23+
// new uci
24+
pub fn new(allocator: std.mem.Allocator) UCI {
25+
attacks.init_attacks();
26+
var board = types.Board.new();
27+
bitboard.fan_pars(types.start_position, &board) catch {
28+
print("Error parsing fen in the new uci function\n", .{});
29+
};
30+
return UCI{
31+
.board = board,
32+
.allocator = allocator,
33+
.is_searching = false,
34+
.stop_search = false,
35+
.search_thread = null,
36+
};
37+
}
38+
39+
// parse moves
40+
fn parse_move(self: *UCI, move_string: []const u8) ?move_gen.Move {
41+
if (move_string.len < 4) return null;
42+
43+
// Parse source and target squares
44+
const from_file = move_string[0] - 'a';
45+
const from_rank = move_string[1] - '1';
46+
const to_file = move_string[2] - 'a';
47+
const to_rank = move_string[3] - '1';
48+
49+
if (from_file > 7 or from_rank > 7 or to_file > 7 or to_rank > 7) return null;
50+
51+
const from_square: u6 = @intCast(from_rank * 8 + from_file);
52+
const to_square: u6 = @intCast(to_rank * 8 + to_file);
53+
54+
// Generate legal moves to find the matching move
55+
var move_list: lists.MoveList = .{};
56+
if (self.board.side == types.Color.White) {
57+
move_gen.generate_moves(&self.board, &move_list, types.Color.White);
58+
} else {
59+
move_gen.generate_moves(&self.board, &move_list, types.Color.Black);
60+
}
61+
62+
// Find matching move
63+
for (0..move_list.count) |i| {
64+
const move = move_list.moves[i];
65+
if (move.from == from_square and move.to == to_square) {
66+
if (move_string.len >= 5) {
67+
const promotion_piece = move_string[4];
68+
const expected_flag = switch (promotion_piece) {
69+
'q' => if (move_gen.Print_move_list.is_capture(move)) types.MoveFlags.PC_QUEEN else types.MoveFlags.PR_QUEEN,
70+
'r' => if (move_gen.Print_move_list.is_capture(move)) types.MoveFlags.PC_ROOK else types.MoveFlags.PR_ROOK,
71+
'b' => if (move_gen.Print_move_list.is_capture(move)) types.MoveFlags.PC_BISHOP else types.MoveFlags.PR_BISHOP,
72+
'n' => if (move_gen.Print_move_list.is_capture(move)) types.MoveFlags.PC_KNIGHT else types.MoveFlags.PR_KNIGHT,
73+
else => continue,
74+
};
75+
if (move.flags == expected_flag) return move;
76+
} else {
77+
if (!move_gen.Print_move_list.is_promotion(move)) return move;
78+
}
79+
}
80+
}
81+
82+
return null;
83+
}
84+
85+
// Parse a position command
86+
fn parse_position(self: *UCI, command: []const u8) !void {
87+
var tokens = std.mem.tokenizeScalar(u8, command, ' ');
88+
_ = tokens.next(); // Skip "position"
89+
90+
const position_type = tokens.next() orelse return;
91+
92+
if (std.mem.eql(u8, position_type, "startpos")) {
93+
try bitboard.fan_pars(types.start_position, &self.board);
94+
} else if (std.mem.eql(u8, position_type, "fen")) {
95+
var fen_buffer: [256]u8 = undefined;
96+
var fen_len: usize = 0;
97+
98+
var parts_count: u8 = 0;
99+
while (parts_count < 6) : (parts_count += 1) {
100+
const part = tokens.next() orelse break;
101+
if (std.mem.eql(u8, part, "moves")) break;
102+
103+
if (fen_len > 0) {
104+
fen_buffer[fen_len] = ' ';
105+
fen_len += 1;
106+
}
107+
@memcpy(fen_buffer[fen_len .. fen_len + part.len], part);
108+
fen_len += part.len;
109+
}
110+
111+
if (fen_len > 0) {
112+
try bitboard.fan_pars(fen_buffer[0..fen_len], &self.board);
113+
}
114+
}
115+
116+
const rest = tokens.rest();
117+
if (rest.len > 0) {
118+
var moves_tokens = std.mem.tokenizeScalar(u8, rest, ' ');
119+
if (moves_tokens.next()) |first_token| {
120+
if (std.mem.eql(u8, first_token, "moves")) {
121+
// Parse and play moves
122+
while (moves_tokens.next()) |move_str| {
123+
if (self.parse_move(move_str)) |move| {
124+
_ = move_gen.make_move(&self.board, move);
125+
}
126+
}
127+
}
128+
}
129+
}
130+
}
131+
132+
// parse go
133+
fn parse_go(self: *UCI, command: []const u8) void {
134+
var tokens = std.mem.tokenizeScalar(u8, command, ' ');
135+
_ = tokens.next(); // Skip "go"
136+
137+
var depth: ?u8 = null;
138+
var movetime: ?u64 = null;
139+
var wtime: ?u64 = null;
140+
var btime: ?u64 = null;
141+
var winc: ?u64 = null;
142+
var binc: ?u64 = null;
143+
var movestogo: ?u64 = null;
144+
var infinite = false;
145+
146+
while (tokens.next()) |token| {
147+
if (std.mem.eql(u8, token, "depth")) {
148+
if (tokens.next()) |depth_str| {
149+
depth = std.fmt.parseUnsigned(u8, depth_str, 10) catch null;
150+
}
151+
} else if (std.mem.eql(u8, token, "movetime")) {
152+
if (tokens.next()) |time_str| {
153+
movetime = std.fmt.parseUnsigned(u64, time_str, 10) catch null;
154+
}
155+
} else if (std.mem.eql(u8, token, "wtime")) {
156+
if (tokens.next()) |time_str| {
157+
wtime = std.fmt.parseUnsigned(u64, time_str, 10) catch null;
158+
}
159+
} else if (std.mem.eql(u8, token, "btime")) {
160+
if (tokens.next()) |time_str| {
161+
btime = std.fmt.parseUnsigned(u64, time_str, 10) catch null;
162+
}
163+
} else if (std.mem.eql(u8, token, "winc")) {
164+
if (tokens.next()) |inc_str| {
165+
winc = std.fmt.parseUnsigned(u64, inc_str, 10) catch null;
166+
}
167+
} else if (std.mem.eql(u8, token, "binc")) {
168+
if (tokens.next()) |inc_str| {
169+
binc = std.fmt.parseUnsigned(u64, inc_str, 10) catch null;
170+
}
171+
} else if (std.mem.eql(u8, token, "movestogo")) {
172+
if (tokens.next()) |moves_str| {
173+
movestogo = std.fmt.parseUnsigned(u64, moves_str, 10) catch null;
174+
}
175+
} else if (std.mem.eql(u8, token, "infinite")) {
176+
infinite = true;
177+
}
178+
}
179+
180+
// Calculate time for move
181+
var calculated_time: u64 = 1000;
182+
183+
if (movetime) |mt| {
184+
calculated_time = mt;
185+
} else if (infinite) {
186+
calculated_time = 1000000; // Very long time for infinite
187+
} else {
188+
// Calculate time based on remaining time and increment
189+
var my_time: ?u64 = null;
190+
var my_inc: ?u64 = null;
191+
192+
if (self.board.side == types.Color.White) {
193+
my_time = wtime;
194+
my_inc = winc;
195+
} else {
196+
my_time = btime;
197+
my_inc = binc;
198+
}
199+
200+
if (my_time) |time| {
201+
const inc = my_inc orelse 0;
202+
if (movestogo) |moves| {
203+
// Fixed number of moves
204+
calculated_time = (time / moves) + inc;
205+
} else {
206+
// Sudden death or increment
207+
calculated_time = (time / 30) + inc; // Assume 30 moves left
208+
}
209+
calculated_time = @min(calculated_time, time - 100); // Leave 100ms buffer
210+
calculated_time = @max(calculated_time, 100); // Minimum 100ms
211+
}
212+
}
213+
214+
// Start search in a separate thread
215+
self.search_thread = std.Thread.spawn(.{}, searchWrapper, .{ self, depth, calculated_time }) catch null;
216+
}
217+
218+
fn searchWrapper(self: *UCI, depth: ?u8, time_ms: u64) void {
219+
search.search_position(self, depth, time_ms);
220+
}
221+
// main loop
222+
pub fn uci_loop(self: *UCI) !void {
223+
var stdin = std.io.getStdIn().reader();
224+
var stdout = std.io.getStdOut().writer();
225+
226+
try stdout.print("NeuroSpeed version {s}\n", .{VERSION});
227+
228+
const buffer = try self.allocator.alloc(u8, UCI_COMMANDS_MAX);
229+
defer self.allocator.free(buffer);
230+
231+
while (true) {
232+
if (stdin.readUntilDelimiterOrEof(buffer, '\n')) |maybe_line| {
233+
const line = maybe_line orelse break;
234+
const trimmed = std.mem.trim(u8, line, " \r\n\t");
235+
236+
if (trimmed.len == 0) continue;
237+
238+
var tokens = std.mem.tokenizeScalar(u8, trimmed, ' ');
239+
const command = tokens.next() orelse continue;
240+
241+
if (std.mem.eql(u8, command, "quit")) {
242+
break;
243+
} else if (std.mem.eql(u8, command, "uci")) {
244+
try stdout.print("id name {s} {s}\n", .{ ENGINE_NAME, VERSION });
245+
try stdout.print("id author {s}\n", .{AUTHOR});
246+
try stdout.print("uciok\n", .{});
247+
} else if (std.mem.eql(u8, command, "isready")) {
248+
try stdout.print("readyok\n", .{});
249+
} else if (std.mem.eql(u8, command, "ucinewgame")) {
250+
// Reset board to starting position
251+
self.board = types.Board.new();
252+
try bitboard.fan_pars(types.start_position, &self.board);
253+
self.stop_search = true;
254+
if (self.search_thread) |thread| {
255+
thread.join();
256+
self.search_thread = null;
257+
}
258+
} else if (std.mem.eql(u8, command, "position")) {
259+
try self.parse_position(trimmed);
260+
} else if (std.mem.eql(u8, command, "go")) {
261+
if (!self.is_searching) {
262+
self.parse_go(trimmed);
263+
}
264+
} else if (std.mem.eql(u8, command, "stop")) {
265+
self.stop_search = true;
266+
} else if (std.mem.eql(u8, command, "d")) {
267+
// Debug command to display board
268+
bitboard.print_unicode_board(self.board);
269+
} else if (std.mem.eql(u8, command, "perft")) {
270+
// Perft command for testing
271+
// var depth: u8 = 1;
272+
// if (tokens.next()) |depth_str| {
273+
// depth = std.fmt.parseUnsigned(u8, depth_str, 10) catch 1;
274+
// }
275+
//
276+
// const nodes = if (self.board.side == types.Color.White)
277+
// util.perft(types.Color.White, &self.board, depth)
278+
// else
279+
// util.perft(types.Color.Black, &self.board, depth);
280+
//
281+
// try stdout.print("Nodes searched: {d}\n", .{nodes});
282+
} else if (std.mem.eql(u8, command, "setoption")) {
283+
// Parse setoption command (currently just ignore)
284+
// Format: setoption name <name> value <value>
285+
// You can implement option handling here
286+
} else {
287+
// Unknown command - ignore as per UCI spec
288+
}
289+
} else |err| {
290+
// Handle read error
291+
print("Error reading input: {}\n", .{err});
292+
break;
293+
}
294+
}
295+
296+
// Clean up
297+
if (self.search_thread) |thread| {
298+
self.stop_search = true;
299+
thread.join();
300+
}
301+
}
302+
};

0 commit comments

Comments
 (0)