extends ColorRect export var rows: int = 44 export var cols: int = 63 export var h_margin: int = 2 export var v_margin: int = 2 export var grid_color: Color = Color.black var READY := false const ByteArray2D = Common.ByteArray2D const IntArray2D = Common.IntArray2D onready var cell_numbers: ByteArray2D const NUMBER_NONE := 255 onready var cell_colors: ByteArray2D onready var corner_marks: IntArray2D var cell_image := Image.new() var cell_texture := ImageTexture.new() var grid_image := Image.new() var grid_texture := ImageTexture.new() enum UndoActions { SET_L_RIGHT, SET_L_DOWN, SET_X_RIGHT, SET_X_DOWN, CLEAR_L_RIGHT, CLEAR_L_DOWN, CLEAR_X_RIGHT, CLEAR_X_DOWN, SET_COLOR } enum CornerMark { L_RIGHT=1, X_RIGHT=2, L_DOWN=4, X_DOWN=8, DARC_1=16, DARC_2=32, DARC_3=64, DARC_4=128, SARC_1=256, SARC_2=512, SARC_3=1024, SARC_4=2048 } func _ready(): # load_puzzle('res://slither_blank.txt') load_puzzle('res://slither-202005192059.txt') func draw_string_centered(font, position, string, color := Color.white): draw_string(font, Vector2(position.x - font.get_string_size(string).x/2.0, position.y + font.get_ascent()), string, color) const font := preload('res://dynamicfont.tres') const COLORS = [null, Color.lightgreen, Color.lightyellow, Color.lightblue, Color.purple] const N_COLORS = 5 var grid_0 := Vector2(0.0, 0.0) var grid_space := Vector2(1.0, 1.0) var grid_rect := Rect2(0.0, 0.0, 1.0, 1.0) var h0 := 0.0 var v0 := 0.0 var h_space := 1.0 var v_space := 1.0 func update_grid_spacing() -> void: h_space = rect_size.x / (h_margin * 2 + cols) v_space = rect_size.y / (v_margin * 2 + rows) if h_space > v_space: h_space = v_space else: v_space = h_space h0 = h_space * h_margin v0 = v_space * v_margin grid_0 = Vector2(h0, v0) grid_space = Vector2(h_space, v_space) grid_rect = Rect2(h0, v0, h_space*cols, v_space*rows) func grid_corner(row, col) -> Vector2: return (Vector2(col, row) * grid_space) + grid_0 func to_grid_space(position: Vector2) -> Vector2: return (position - grid_0)/grid_space func regenerate_cell_texture() -> void: # cell_image.create_from_data(rows, cols, false, Image.FORMAT_R8, cell_colors._array) cell_image.create(cols, rows, false, Image.FORMAT_RGBA8) cell_image.lock() for row in rows: for col in cols: var color = COLORS[cell_colors.get_cell(row, col)] if color != null: cell_image.set_pixel(col, row, color) cell_image.unlock() cell_texture.create_from_image(cell_image, 0) func regenerate_grid_texture() -> void: # grid_image.create_from_data(rows, cols, false, Image.FORMAT_R8, cell_corners._array) grid_image.create(cols+1, rows+1, false, Image.FORMAT_RGBA8) grid_image.lock() for row in rows+1: for col in cols+1: var flags = corner_marks.get_cell(row, col) grid_image.set_pixel(col, row, Color(float(flags&3)/2.0, float((flags>>2)&3)/2.0, 0.0)) grid_image.unlock() grid_texture.create_from_image(grid_image, 0) $TextureRect.set_texture(grid_texture) func _draw() -> void: if not READY: return update_grid_spacing() font.set_size(int(ceil(v_space-5))) # Colors # for row in rows: # for col in cols: # var color = COLORS[cell_colors.get_cell(row, col)] # if color != null: # draw_rect(Rect2(grid_corner(row, col), Vector2(h_space, v_space)), color, true) regenerate_cell_texture() draw_texture_rect(cell_texture, Rect2(h0, v0, h_space*cols, v_space*rows), false) # Lines # for row in rows: # for col in cols: # if corner_marks.get_flag(row, col, CornerMark.L_RIGHT): # draw_line(grid_corner(row, col), grid_corner(row, col+1), Color.blue) # elif corner_marks.get_flag(row, col, CornerMark.X_RIGHT): # draw_line(grid_corner(row, col+0.4), grid_corner(row, col+0.6), Color.red) # else: # draw_line(grid_corner(row, col), grid_corner(row, col+1), Color.white) # # if corner_marks.get_flag(row, col, CornerMark.L_DOWN): # draw_line(grid_corner(row, col), grid_corner(row+1, col), Color.blue) # elif corner_marks.get_flag(row, col, CornerMark.X_DOWN): # draw_line(grid_corner(row+0.4, col), grid_corner(row+0.6, col), Color.red) # else: # draw_line(grid_corner(row, col), grid_corner(row+1, col), Color.white) # for col in cols: # if corner_marks.get_flag(rows, col, CornerMark.L_RIGHT): # draw_line(grid_corner(rows, col), grid_corner(rows, col+1), Color.blue) # elif corner_marks.get_flag(rows, col, CornerMark.X_RIGHT): # draw_line(grid_corner(rows, col+0.4), grid_corner(rows, col+0.6), Color.red) # else: # draw_line(grid_corner(rows, col), grid_corner(rows, col+1), Color.white) # for row in rows: # if corner_marks.get_flag(row, cols, CornerMark.L_DOWN): # draw_line(grid_corner(row, cols), grid_corner(row+1, cols), Color.blue) # elif corner_marks.get_flag(row, cols, CornerMark.X_DOWN): # draw_line(grid_corner(row+0.4, cols), grid_corner(row+0.6, cols), Color.red) # else: # draw_line(grid_corner(row, cols), grid_corner(row+1, cols), Color.white) regenerate_grid_texture() $TextureRect.margin_left = h0 $TextureRect.margin_top = v0 $TextureRect.rect_size = Vector2(h_space*cols, v_space*rows) # Arc pencil marks # Numbers for row in rows: for col in cols: var num = cell_numbers.get_cell(row, col) if num < 4: draw_string_centered(font, grid_corner(row, col+0.5), str(num), Color.black) # COORDS for row in rows: draw_string_centered(font, Vector2(h0/2, v0+row*v_space), '%02d'%row, Color.black) for col in cols: draw_string_centered(font, Vector2(h0+(col+0.5)*h_space, v0/4), Common.num2alpha(col), Color.black) # Undo Stack var u = len(undo_stack) for i in u: draw_string(font, grid_corner(i, cols+1), str(undo_stack[u-i-1]), Color.black if i>=undo_preview_pos else Color.gray) # print(action) func _process(delta: float) -> void: # update() pass const num_dict := {'.': NUMBER_NONE, '0': 0, '1': 1, '2': 2, '3': 3} const colornum_dict := {'0': 0, '1': 1, '2': 2} func load_puzzle(filename: String): var file := File.new() var error = file.open(filename, File.READ) if file.get_line() != 'pzprv3.1': return if file.get_line() != 'slither': return rows = int(file.get_line()) cols = int(file.get_line()) cell_numbers = ByteArray2D.new(rows, cols) cell_colors = ByteArray2D.new(rows, cols) corner_marks = IntArray2D.new(rows+1, cols+1, 0) var row = 0 var stage = 0 var until = [rows, rows, rows, rows+1, 0] while not file.eof_reached(): if row >= until[stage]: stage += 1 row = 0 match stage: 0: # Puzzle definition numbers var line = file.get_csv_line(' ') var col = 0 for numstr in line: if col < cols: cell_numbers.set_cell(row, col, num_dict.get(numstr, NUMBER_NONE)) col += 1 1: # cell colors var line = file.get_csv_line(' ') var col = 0 for numstr in line: if col < cols: cell_colors.set_cell(row, col, colornum_dict.get(numstr, 0)) col += 1 2: # vert lines var line = file.get_csv_line(' ') var col = 0 for numstr in line: if col < cols+1: match int(numstr): 1: corner_marks.set_flag(row, col, CornerMark.L_DOWN) -1: corner_marks.set_flag(row, col, CornerMark.X_DOWN) col += 1 3: # horiz lines var line = file.get_csv_line(' ') var col = 0 for numstr in line: if col < cols+1: match int(numstr): 1: corner_marks.set_flag(row, col, CornerMark.L_RIGHT) -1: corner_marks.set_flag(row, col, CornerMark.X_RIGHT) col += 1 4: break row += 1 file.close() READY = true enum DragAction {TEST_LINE, DRAW_LINE, REMOVE_LINE, DRAW_X, REMOVE_X, DRAW_COLOR} var drag_action = 0 var drag_color := 0 func _input(event: InputEvent) -> void: if event is InputEventMouse: if !grid_rect.has_point(event.position): return var gridpos = to_grid_space(event.position) var dx = fmod(gridpos.x, 1.0) var dy = fmod(gridpos.y, 1.0) var adx = 0.5 - abs(dx-0.5) var ady = 0.5 - abs(dy-0.5) var threshold = 0.2 var sel_col var sel_row if event is InputEventMouseButton and event.pressed: if adx < threshold: if ady < threshold: drag_action = DragAction.TEST_LINE else: sel_row = int(gridpos.y) sel_col = int(round(gridpos.x)) if corner_marks.get_flag(sel_row, sel_col, CornerMark.X_DOWN): drag_action = DragAction.REMOVE_X clear_x_down(sel_row, sel_col) else: drag_action = DragAction.DRAW_X set_x_down(sel_row, sel_col) else: if ady < threshold: sel_row = int(round(gridpos.y)) sel_col = int(gridpos.x) if corner_marks.get_flag(sel_row, sel_col, CornerMark.X_RIGHT): drag_action = DragAction.REMOVE_X clear_x_right(sel_row, sel_col) else: drag_action = DragAction.DRAW_X set_x_right(sel_row, sel_col) else: sel_row = int(gridpos.y) sel_col = int(gridpos.x) drag_action = DragAction.DRAW_COLOR drag_color = posmod(cell_colors.get_cell(sel_row, sel_col) + (1 if event.button_index==BUTTON_LEFT else -1), N_COLORS) set_cell_color(sel_row, sel_col, drag_color) elif event is InputEventMouseMotion and event.button_mask: if drag_action == DragAction.TEST_LINE: if adx < threshold and ady > threshold: drag_action = DragAction.REMOVE_LINE if corner_marks.get_flag(int(gridpos.y), int(round(gridpos.x)), CornerMark.L_DOWN) else DragAction.DRAW_LINE elif adx > threshold and ady < threshold: drag_action = DragAction.REMOVE_LINE if corner_marks.get_flag(int(round(gridpos.y)), int(gridpos.x), CornerMark.L_RIGHT) else DragAction.DRAW_LINE match drag_action: DragAction.DRAW_LINE: if adx < threshold and ady > threshold: set_line_down(int(gridpos.y), int(round(gridpos.x))) elif adx > threshold and ady < threshold: set_line_right(int(round(gridpos.y)), int(gridpos.x)) DragAction.REMOVE_LINE: if adx < threshold and ady > threshold: clear_line_down(int(gridpos.y), int(round(gridpos.x))) elif adx > threshold and ady < threshold: clear_line_right(int(round(gridpos.y)), int(gridpos.x)) DragAction.DRAW_X: if adx < threshold and ady > threshold: set_x_down(int(gridpos.y), int(round(gridpos.x))) elif adx > threshold and ady < threshold: set_x_right(int(round(gridpos.y)), int(gridpos.x)) DragAction.REMOVE_X: if adx < threshold and ady > threshold: clear_x_down(int(gridpos.y), int(round(gridpos.x))) elif adx > threshold and ady < threshold: clear_x_right(int(round(gridpos.y)), int(gridpos.x)) DragAction.DRAW_COLOR: sel_row = int(gridpos.y) sel_col = int(gridpos.x) set_cell_color(sel_row, sel_col, drag_color) elif event is InputEventKey and event.pressed: match event.scancode: KEY_Z: undo() KEY_DOWN: self.undo_preview_pos += 1 KEY_UP: self.undo_preview_pos -= 1 var undo_stack = [] var undo_preview_pos := 0 setget set_undo_preview_pos func apply_undo_preview(): undo_stack.resize(len(undo_stack)-undo_preview_pos) undo_preview_pos = 0 update() func do_action(action): if undo_preview_pos > 0: apply_undo_preview() action.activate() undo_stack.append(action) update() func undo(): if !undo_stack.empty(): if undo_preview_pos > 0: apply_undo_preview() else: var action = undo_stack.pop_back() action.undo() update() func set_undo_preview_pos(value): if value < 0: # Negative is past the end of the stack value = 0 elif value > len(undo_stack): value = len(undo_stack) var old_pos = undo_preview_pos undo_preview_pos = value var delta = value - old_pos if delta > 0: # We are undoing for i in delta: undo_stack[-old_pos-1-i].undo() elif delta < 0: # We are redoing for i in -delta: undo_stack[-old_pos+i].activate() update() class UndoAction: var action_type var cell_row: int var cell_col: int var activated: bool func _init(action, row, col) -> void: action_type = action cell_row = row cell_col = col activated = false class UndoActionCorner extends UndoAction: var corner_marks var corner_mark var set: bool # False = clear func _init(row, col, corner_mark, set, corner_marks).(corner_mark, row, col) -> void: self.corner_marks = corner_marks self.corner_mark = corner_mark self.set = set func _to_string() -> String: var prefix = '+' if set else '-' match corner_mark: CornerMark.L_RIGHT: return prefix+'L %s%d:%d'%[Common.num2alpha(cell_col), cell_row-1, cell_row] CornerMark.X_RIGHT: return prefix+'X %s%d:%d'%[Common.num2alpha(cell_col), cell_row-1, cell_row] CornerMark.L_DOWN: return prefix+'L %s%d:%s'%[Common.num2alpha(cell_col-1), cell_row, Common.num2alpha(cell_col)] CornerMark.X_DOWN: return prefix+'X %s%d:%s'%[Common.num2alpha(cell_col-1), cell_row, Common.num2alpha(cell_col)] _: return 'unk %s%d %s%d'%[prefix, corner_mark, Common.num2alpha(cell_col), cell_row] func activate() -> void: activated = true if set: corner_marks.set_flag(cell_row, cell_col, corner_mark) else: corner_marks.clear_flag(cell_row, cell_col, corner_mark) func undo() -> void: activated = false if set: corner_marks.clear_flag(cell_row, cell_col, corner_mark) else: corner_marks.set_flag(cell_row, cell_col, corner_mark) class UndoActionCell extends UndoAction: var set_state var old_state var cell_colors func _init(action, row, col, set_state, cell_colors).(action, row, col) -> void: self.set_state = set_state self.old_state = set_state self.cell_colors = cell_colors func _to_string() -> String: match action_type: UndoActions.SET_COLOR: return 'C%d<-%d %s%d'%[set_state, old_state, Common.num2alpha(cell_col), cell_row] _: return 'unk %d %s%d'%[action_type, Common.num2alpha(cell_col), cell_row] func activate() -> void: assert(not activated) activated = true match action_type: UndoActions.SET_COLOR: old_state = cell_colors.get_cell(cell_row, cell_col) cell_colors.set_cell(cell_row, cell_col, set_state) func undo() -> void: assert(activated) activated = false match action_type: UndoActions.SET_COLOR: cell_colors.set_cell(cell_row, cell_col, old_state) func set_cell_color(row, col, new_color): if new_color != cell_colors.get_cell(row, col): do_action(UndoActionCell.new(UndoActions.SET_COLOR, row, col, new_color, cell_colors)) # TODO: Refactor all this nonsense a bit more func set_line_right(row, col): if !corner_marks.get_flag(row, col, CornerMark.L_RIGHT) and !corner_marks.get_flag(row, col, CornerMark.X_RIGHT): do_action(UndoActionCorner.new(row, col, CornerMark.L_RIGHT, true, corner_marks)) func set_line_down(row, col): if !corner_marks.get_flag(row, col, CornerMark.L_DOWN) and !corner_marks.get_flag(row, col, CornerMark.X_DOWN): do_action(UndoActionCorner.new(row, col, CornerMark.L_DOWN, true, corner_marks)) func clear_line_right(row, col): if corner_marks.get_flag(row, col, CornerMark.L_RIGHT): do_action(UndoActionCorner.new(row, col, CornerMark.L_RIGHT, false, corner_marks)) func clear_line_down(row, col): if corner_marks.get_flag(row, col, CornerMark.L_DOWN): do_action(UndoActionCorner.new(row, col, CornerMark.L_DOWN, false, corner_marks)) func set_x_right(row, col): if !corner_marks.get_flag(row, col, CornerMark.L_RIGHT) and !corner_marks.get_flag(row, col, CornerMark.X_RIGHT): do_action(UndoActionCorner.new(row, col, CornerMark.X_RIGHT, true, corner_marks)) func set_x_down(row, col): if !corner_marks.get_flag(row, col, CornerMark.L_DOWN) and !corner_marks.get_flag(row, col, CornerMark.X_DOWN): do_action(UndoActionCorner.new(row, col, CornerMark.X_DOWN, true, corner_marks)) func clear_x_right(row, col): if corner_marks.get_flag(row, col, CornerMark.X_RIGHT): do_action(UndoActionCorner.new(row, col, CornerMark.X_RIGHT, false, corner_marks)) func clear_x_down(row, col): if corner_marks.get_flag(row, col, CornerMark.X_DOWN): do_action(UndoActionCorner.new(row, col, CornerMark.X_DOWN, false, corner_marks))