online multiplayer chess game (note server currently down)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
extends Control
class_name Grid

const PieceScene := preload("res://piece/Piece.tscn")
const Square := preload("res://Square.tscn")

### for the sandisplay
signal add_to_pgn(move)
signal clear_pgn
signal load_pgn(moves)
signal remove_last

var move_indicators: PoolIntArray = []

var rot: float = 0
var piece_size: Vector2

export(Color) var overlay_color: Color
export(Color) var premove_color: Color
export(Color) var last_move_indicator_color: Color
export(Color) var last_move_take_indicator: Color
export(Color) var clockrunning_color: Color
export(Color) var clockrunninglow: Color
export(Color) var clocklow: Color

var board = []  # has `get_piece(algebraic position)` and `set_piece(algebraic position)` for ease of use


func get_piece(alg: String) -> Piece:
	return board[Chess.SQUARE_MAP[alg]]


func set_piece(alg: String, p: Piece) -> void:
	board[Chess.SQUARE_MAP[alg]] = p


var flipped := false
var labels := {numbers = [], letters = []}
var background_array := []
var last_clicked: Piece
var premove: Dictionary = {}
var check_circle: GradientTexture2D = load("res://piece/check-circle.tres")
var take_circle: GradientTexture2D = load("res://piece/takeable-circle.tres")
var move_circle: GradientTexture2D = load("res://piece/move-circle.tres")

onready var game: GameUI = owner if owner is GameUI else null
onready var sidebar := game.get_node_or_null("%Sidebar") if game else null
onready var darken = $Darken
onready var foreground := $Foreground
onready var background := $Background
onready var pieces := $Pieces
onready var arrows := $"%Arrows"

var chess := Chess.new()
var local := false
var spectating := false
var team: String
var auto_change_team := false


func _init():
	Globals.grid = self


func _exit_tree():
	Globals.grid = null


func _process(_delta):
	rect_rotation = rot
	foreground.rect_rotation = rot
	if Input.is_action_just_pressed("debug") and Debug.debug:
		print(chess.ascii())


func _resized():
	var old_pc = piece_size
	piece_size = rect_size / 8
	piece_size.x = clamp(piece_size.x, 0, piece_size.y)
	piece_size.y = clamp(piece_size.y, 0, piece_size.x)
	check_circle.width = piece_size.x
	check_circle.height = piece_size.y
	take_circle.width = piece_size.x
	take_circle.height = piece_size.y
	move_circle.width = piece_size.x
	move_circle.height = piece_size.y
	if foreground:
		rect_pivot_offset = (piece_size * 8) / 2
		foreground.rect_pivot_offset = rect_pivot_offset
	if not (board.empty() && background_array.empty()) && piece_size != old_pc:
		resize_board()
		Log.debug("Resizing board")


func set_take_move_circle_color(
	color: Color = Color(overlay_color.r, overlay_color.g, overlay_color.b, .65)
) -> void:
	take_circle.gradient.colors[1] = color
	move_circle.gradient.colors[0] = color


func _ready():
	set_take_move_circle_color()
	_resized()
	Events.connect("turn_over", self, "_on_turn_over")
	PacketHandler.connect("move_data", self, "move", [false, false])
	create_pieces()
	create_squares()
	create_labels()
	yield(get_tree(), "idle_frame")
	if !team:
		team = chess.turn
		auto_change_team = true
	Log.debug("board: ready")


func resize_board():
	resize_pieces()


func create_squares() -> void:  # create the board
	background_array.resize(128)
	for i in Chess.SQUARE_MAP.values():
		var alg := Chess.algebraic(i)
		var square := Square.instance()  # create a square
		square.name = alg
		square.b = self
		square.square = alg
		square.hint_tooltip = alg
		square.color = (Globals.board_color1 if Chess.square_color(alg) == "light" else Globals.board_color2)  # set the color
		background.add_child(square)  # add the square to the background
		square.connect("clicked", self, "square_clicked", [square])  # connect the clicked event
		background_array[i] = square  # add the square to the background array
	arrows._setup(self)  # initialize the arrows


func create_labels() -> void:
	var font: DynamicFont = load("res://ui/ubuntu-bold-regular.tres").duplicate()
	font.size = 15
	for k in Chess.SQUARE_MAP:
		if k == "h1":
			var l = init_label(font, k, k[0], VALIGN_BOTTOM, 0, false)
			var n = init_label(font, k, k[1], 0, VALIGN_BOTTOM, false)
			var h = HBoxContainer.new()
			h.mouse_filter = MOUSE_FILTER_IGNORE
			for i in [l, n]:
				var ic = create_margin_container()
				ic.add_child(i)
				h.add_child(ic)
			labels.numbers.append(n)
			labels.letters.append(l)
			foreground.add_child(h)
		elif k[0] == "h":  # file h contains numbers
			labels.numbers.append(init_label(font, k, k[1], 0, VALIGN_BOTTOM))
		elif k[1] == "1":  # rank 1 contains letters
			labels.letters.append(init_label(font, k, k[0], VALIGN_BOTTOM))
		else:
			var spacer = Control.new()
			spacer.mouse_filter = MOUSE_FILTER_IGNORE
			spacer.name = k + "_space"
			spacer.size_flags_horizontal = SIZE_EXPAND_FILL
			spacer.size_flags_vertical = SIZE_EXPAND_FILL
			foreground.add_child(spacer)


func init_label(font: DynamicFont, alg: String, text: String, valign := 0, align := 0, add := true) -> Label:
	var label := Label.new()
	label.align = align
	label.valign = valign
	label.size_flags_horizontal = SIZE_EXPAND_FILL
	label.size_flags_vertical = SIZE_EXPAND_FILL
	label.mouse_filter = MOUSE_FILTER_IGNORE
	label.name = text
	label.text = text
	label.add_color_override(
		"font_color", Globals.board_color1 if Chess.square_color(alg) == "dark" else Globals.board_color2
	)
	label.add_font_override("font", font)
	if add:
		var container := create_margin_container()
		container.add_child(label)
		foreground.add_child(container)
	return label


func create_margin_container(margin := 5) -> MarginContainer:
	var c := MarginContainer.new()
	c.add_constant_override("margin_top", margin)
	c.add_constant_override("margin_left", margin)
	c.add_constant_override("margin_right", margin)
	c.add_constant_override("margin_bottom", margin)
	c.size_flags_horizontal = SIZE_EXPAND_FILL
	c.size_flags_vertical = SIZE_EXPAND_FILL
	c.mouse_filter = MOUSE_FILTER_IGNORE
	return c


func clear_pieces() -> void:
	for i in Chess.SQUARE_MAP.values():
		var p: Piece = board[i]
		if p:
			p.queue_free()
			board[i] = null


func resize_pieces():
	for i in Chess.SQUARE_MAP.values():
		var p: Piece = board[i]
		if p:
			p.size()


func create_pieces():
	board.resize(128)
	for k in Chess.SQUARE_MAP:
		var piece = chess.get(k)
		if piece:
			make_piece(k, piece.type, piece.color)


func make_piece(algebraic: String, piece_type: String, color := "w") -> void:  # make peace
	var piece := PieceScene.instance()  # create a piece
	piece.name = "%s-%s" % [piece_type, algebraic]
	piece.b = self
	piece.position = algebraic
	piece.type = piece_type
	piece.color = color
	pieces.add_child(piece)  # add the piece to the grid
	set_piece(algebraic, piece)


func flip_pieces() -> void:
	for i in Chess.SQUARE_MAP.values():
		var spot: Piece = board[i]
		if spot:
			spot.sprite.flip_v = flipped
			spot.sprite.flip_h = flipped


func flip_labels() -> void:
	for i in range(8):
		var numlabel: Label = labels.numbers[i]
		var letlabel: Label = labels.letters[i]
		var number := i + 1 if flipped else 8 - i
		numlabel.text = str(number)
		letlabel.text = "hgfedcba"[number - 1]


func flip_board() -> void:
	rot = 0 if rot == 180 else 180
	flipped = rot == 180
	Log.debug(["Flipped the board, now", "flipped" if flipped else "not flipped"])
	if sidebar:
		sidebar.flip_panels()
	flip_pieces()
	flip_labels()


func is_my_turn() -> bool:
	return team == chess.turn


func square_clicked(clicked_square: BackgroundSquare) -> void:
	if Globals.spectating:
		return

	var p := get_piece(clicked_square.square)

	if not is_my_turn() and is_instance_valid(last_clicked):
		# PREMOVE AREA
		var p_sq: int = Chess.SQUARE_MAP[clicked_square.square]
		for m in chess.piece_moves(last_clicked.position, last_clicked.type, team):
			if m.to == p_sq && m.from == Chess.SQUARE_MAP[last_clicked.position]:
				if "from" in premove and "to" in premove:
					background_array[premove.from].premove_indicator.hide()  # hide premove indicators
					background_array[premove.to].premove_indicator.hide()
				if premove && premove.from == m.from && premove.to == m.to:
					premove = {}
					Log.debug("De-selected premove")
				else:
					premove = m
					background_array[premove.from].premove_indicator.show()
					background_array[premove.to].premove_indicator.show()
					if premove.flags & Chess.BITS.PROMOTION:
						p.open_promotion_previews(darken)
						premove.promotion = yield(p, "promotion_decided")
					Log.debug("Selected premove: %s" % premove)
					clear_last_clicked()
					return
	elif (!p or p.color != team) and is_instance_valid(last_clicked):
		# Attempt to make the move (NORMAL MOVE AREA)
		for m in chess.moves({square = last_clicked.position, verbose = true}):
			if m.to == clicked_square.square && m.from == last_clicked.position:
				move(m.san)
				clear_last_clicked()
				return

	clear_last_clicked()

	if p and p.color == team:
		if chess.turn != team:
			if !Globals.premoves:
				return
			clicked_square.show_premove_indicators()
		else:
			clicked_square.show_move_indicators()
		last_clicked = p


func move(san: String, send := true, create_promotion_input := true) -> void:
	var sound_handled = false
	var move_0x88 = chess.__move_from_san(san, true)
	var valid_moves = chess.moves({square = chess.algebraic(move_0x88.from), stripped = true})
	if valid_moves.find(chess.stripped_san(san)) == -1:
		Log.err("Invalid move " + san)
		return
	Log.debug("Making move " + san)
	chess.__make_move(move_0x88)
	if move_0x88.flags & Chess.BITS.CAPTURE:
		board[move_0x88.to].took()
		SoundFx.play("Capture")
		sound_handled = true
	elif move_0x88.flags & Chess.BITS.EP_CAPTURE:
		var to_take := Chess.offset(move_0x88.to, Vector2(0, 1 * -1 if chess.turn == Chess.WHITE else 1))
		get_piece(to_take).took()
		SoundFx.play("Capture")
		sound_handled = true
	elif move_0x88.flags & Chess.BITS.KSIDE_CASTLE:  # kingside castling
		var rook_pos := Chess.offset(move_0x88.to, Vector2(1, 0))
		get_piece(rook_pos).move(Chess.offset(move_0x88.to, Vector2(-1, 0)))
	elif move_0x88.flags & Chess.BITS.QSIDE_CASTLE:  # queenside
		var rook_pos := Chess.offset(move_0x88.to, Vector2(-2, 0))
		get_piece(rook_pos).move(Chess.offset(move_0x88.to, Vector2(1, 0)))
	if move_0x88.flags & Chess.BITS.PROMOTION:  #promotion wow
		var p: Piece = yield(board[move_0x88.from].move(Chess.algebraic(move_0x88.to), true), "completed")
		if create_promotion_input:
			p.open_promotion_previews(darken)
			move_0x88.promotion = yield(p, "promotion_decided")
			san = chess.__move_to_san(move_0x88)  # update the san with new promotion data
			p.queue_free()
		else:  # was opponents turn, this is opponents move: promotion is already chosen
			p.queue_free()
		make_piece(p.position, move_0x88.promotion, p.color)
		SoundFx.play("Move" if move_0x88.flags & Chess.BITS.NORMAL else "Capture")
		sound_handled = true
	else:  # not promotion: from **always** moves to `to`
		var _p = board[move_0x88.from].move(Chess.algebraic(move_0x88.to))
	if send && !local:
		PacketHandler.send_mov(san)
	if !sound_handled:
		SoundFx.play("Move")
	emit_signal("add_to_pgn", san)
	Events.emit_signal("turn_over")


func clear_last_clicked():
	last_clicked = null


func draw(reason := "") -> void:
	var string = "draw by " + reason
	game.set_status(string, 0)
	SoundFx.play("Victory")
	Events.emit_signal("game_over", string)


func win(winner: String, reason := "") -> void:
	var string = "%s won the game by %s" % [Utils.expand_color(winner), reason]
	game.set_status(string, 0)  #: black won the game by checkmate
	Events.emit_signal("game_over", string)
	SoundFx.play("Victory")


func load_pgn(pgn: String) -> void:
	chess.load_pgn(pgn, {sloppy = true})
	clear_pieces()
	create_pieces()
	emit_signal("clear_pgn")
	var pgn_parser := PGN.new()
	var movs: PoolStringArray = pgn_parser.parse(pgn).moves
	emit_signal("load_pgn", movs)
	Log.info("load pgn " + pgn)
	Events.emit_signal("turn_over")


func undo(two: bool = false) -> void:
	Globals.chat.server("undid move %s" % chess.undo().san)
	emit_signal("remove_last")
	if two:
		Globals.chat.server("undid move %s" % chess.undo().san)
		emit_signal("remove_last")
	clear_pieces()
	clear_last_clicked()
	create_pieces()
	Events.emit_signal("turn_over")


func auto_flip():
	if team == Chess.WHITE and flipped:
		flip_board()
	elif team == Chess.BLACK and not flipped:
		flip_board()


func _on_turn_over():
	# if auto_change_team:
	# 	team = chess.turn
	# 	auto_flip()

	if is_my_turn():
		set_take_move_circle_color()
		# use the premove if possible
		if premove:
			background_array[premove.from].premove_indicator.hide()
			background_array[premove.to].premove_indicator.hide()
			if board[premove.from]:  # see if its valid
				if premove.flags & (Chess.BITS.CAPTURE | Chess.BITS.EP_CAPTURE) && not board[premove.to]:
					return
				var san = chess.__move_to_san(premove, chess.__generate_moves({legal = true}), false)
				if san:
					var legal_moves = chess.moves({square = chess.algebraic(premove.from), stripped = true})
					var is_possible_move = legal_moves.find(chess.stripped_san(san)) != -1
					if chess.__move_from_san(san, true) and (is_possible_move):  # it is valid
						Log.debug(["Executing premove:", san])
						move(san, true, false)  # make the move, send it to the opponent, dont prompt for premoves
					premove = {}
	elif Globals.premoves:
		set_take_move_circle_color(premove_color)
	SaveLoad.save("user://game.json", {pgn = chess.pgn(), fen = chess.fen()})
	clear_last_clicked()
	check_game_over()
	create_last_move_indicators()


func check_game_over():
	if chess.in_checkmate():
		# they won if its my turn, i won if its their turn.
		win(team if is_my_turn() else Chess.__swap_color(team), "checkmate")
	elif chess.half_moves >= 50:
		draw("fifty move rule")
	elif chess.in_stalemate():
		draw("stalemate")
	elif chess.insufficient_material():
		draw("insufficient material")
	elif chess.in_threefold_repetition():
		draw("threefold repetition")


func create_last_move_indicators():
	for i in move_indicators:
		background_array[i].move_indicator.color = last_move_indicator_color
		background_array[i].move_indicator.hide()
	if !chess.__history:
		return
	var m: Dictionary = chess.__history[-1].move
	background_array[m.from].move_indicator.show()
	if m.flags & Chess.BITS.CAPTURE:
		background_array[m.to].move_indicator.color = last_move_take_indicator
	background_array[m.to].move_indicator.show()
	move_indicators = [m.from, m.to]


func reload():
	premove = {}
	chess = Chess.new()
	clear_last_clicked()
	clear_pieces()
	create_pieces()
	create_last_move_indicators()  # it hides the indicators :/
	Events.emit_signal("turn_over")
	emit_signal("clear_pgn")  # clears the san displays