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
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 piece_size: Vector2

export(Color) var overlay_color := Color(0.078431, 0.333333, 0.117647, 0.498039)
export(Color) var last_move_indicator_color := Color(0.74902, 0.662745, 0.223529, 0.498039)
export(Color) var last_move_take_indicator := Color(0.74902, 0.407843, 0.223529, 0.498039)
export(Color) var clockrunning_color := Color(0.219608, 0.278431, 0.133333)
export(Color) var clockrunninglow := Color(0.47451, 0.172549, 0.164706)
export(Color) var clocklow := Color(0.313726, 0.156863, 0.14902)

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
var check_circle: GradientTexture2D = load("res://piece/check-circle.tres")
var take_circle: GradientTexture2D = load("res://piece/takeable-circle.tres")

export(NodePath) var sidebar_path = @""
onready var sidebar := get_node_or_null(sidebar_path)
export(NodePath) var ui_path = @""
onready var darken = $Darken
onready var ui := get_node_or_null(ui_path)
onready var foreground := $Foreground
onready var background := $Background
onready var pieces := $Pieces

var chess := Chess.new()


func _init():
	Globals.grid = self


func _exit_tree():
	Globals.grid = null


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)
	rect_pivot_offset = (piece_size * 8) / 2
	foreground.rect_pivot_offset = rect_pivot_offset
	check_circle.width = piece_size.x
	check_circle.height = piece_size.y
	take_circle.width = piece_size.x
	take_circle.height = piece_size.y
	if !(board.empty() && background_array.empty()) and piece_size != old_pc:
		resize_board()
		Log.debug("Resizing board")


func _ready():
	take_circle.gradient.colors[1] = Color(overlay_color.r, overlay_color.g, overlay_color.b, 1)
	_resized()
	Events.connect("turn_over", self, "_on_turn_over")
	PacketHandler.connect("move_data", self, "move")
	create_pieces()
	create_squares()
	create_labels()


func resize_board():
	resize_squares()
	resize_pieces()


func resize_squares() -> void:
	for i in Chess.SQUARE_MAP.values():
		var square: BackgroundSquare = background_array[i]
		square.size()


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.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", [alg])  # connect the clicked event
		background_array[i] = square  # add the square to the background array
	find_node("Arrows")._setup(self)  # initialize the arrows


func create_labels() -> void:
	var font: DynamicFont = load("res://ui/ubuntu-bold.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.name = text
	label.size_flags_horizontal = SIZE_EXPAND_FILL
	label.size_flags_vertical = SIZE_EXPAND_FILL
	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 container := MarginContainer.new()
	container.add_constant_override("margin_top", margin)
	container.add_constant_override("margin_left", margin)
	container.add_constant_override("margin_right", margin)
	container.add_constant_override("margin_bottom", margin)
	return container


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.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:
	rect_rotation = 0 if rect_rotation == 180 else 180
	foreground.rect_rotation = rect_rotation
	flipped = rect_rotation == 180
	Log.debug(["Flipped the board, now", "flipped" if flipped else "not flipped"])
	sidebar.flip_panels()
	flip_pieces()
	flip_labels()


func square_clicked(clicked_square: String) -> void:
	if chess.turn != Globals.team or Globals.spectating:
		return
	var p := get_piece(clicked_square)
	if !p or p.color != Globals.team:
		if !is_instance_valid(last_clicked):
			return
		for m in chess.moves({square = last_clicked.position, verbose = true}):
			if m.to == clicked_square && m.from == last_clicked.position:
				move(m.san, false)
				break
		clear_last_clicked()
		return
	last_clicked = p


func move(san: String, is_recieved_move := true) -> void:
	resize_board()
	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")
		return
	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))
		var _rook := 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))
		var _rook := 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 = board[move_0x88.from].move(Chess.algebraic(move_0x88.to))
		if !is_recieved_move:  # was my turn, this is my move
			yield(p.tween, "tween_all_completed")
			p.open_promotion_previews()
			yield(p, "promotion_decided")
			move_0x88["promotion"] = p.promote_to
			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()  # the q_f above happens after a dozen yields
		# the move animation is useless if its not my turn
		# but it changes p.position, so its usefull.
		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 !is_recieved_move:
		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
	darken.hide()


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


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


func load_pgn(pgn: String) -> void:
	chess.load_pgn(pgn, {sloppy = true})
	clear_pieces()
	create_pieces()
	emit_signal("clear_pgn")
	var movs: PoolStringArray = Pgn.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 _on_turn_over():
	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(Globals.team if Globals.team != chess.turn else Chess.__swap_color(Globals.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]