Skip to content

Commit e1567b3

Browse files
authored
Merge pull request #42 from ASecondGuy/connect-4-physics
Connect 4 physics
2 parents 2e04289 + 0792131 commit e1567b3

18 files changed

+1029
-0
lines changed

game/games/asecondguy_connect/ai.gd

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
extends Node2D
2+
3+
const CHIPSCRIPT := preload("res://games/asecondguy_connect/chip.gd")
4+
const CONNECTSCRIPT := preload("res://games/asecondguy_connect/connect.gd")
5+
6+
export var player_id := 1
7+
8+
var chip: CHIPSCRIPT
9+
var _curve := Curve2D.new()
10+
var _goal_position := Vector2()
11+
var _picked := false
12+
13+
onready var _chips_node := $"../Chips"
14+
onready var _grid := $"../Grid"
15+
onready var _game: CONNECTSCRIPT = $".."
16+
onready var _timer := $Timer
17+
onready var _pickup_timer := $PickupTimer
18+
19+
20+
func _ready():
21+
set_process(false)
22+
seed(hash(str(OS.get_unix_time(), get_instance_id())))
23+
24+
25+
func _process(_delta):
26+
if _is_chip_stopped() and !_picked:
27+
_start_pickup()
28+
if _picked:
29+
var pos = (
30+
(_timer.wait_time - _timer.time_left)
31+
/ _timer.wait_time
32+
* _curve.get_baked_length()
33+
)
34+
chip.target_position = _curve.interpolate_baked(pos, true)
35+
if is_equal_approx(pos, _curve.get_baked_length()):
36+
_unpick()
37+
38+
39+
func _on_chip_spawn(new_chip: CHIPSCRIPT):
40+
if new_chip.player_id == player_id:
41+
chip = new_chip
42+
chip.set_mouse_control(false)
43+
_choose_goal()
44+
set_process(true)
45+
_pickup_timer.start()
46+
47+
48+
# drawing the path for debugging
49+
#func _draw():
50+
# var points := _curve.get_baked_points()
51+
# for i in range(points.size() - 1):
52+
# draw_line(points[i], points[i + 1], Color.red)
53+
54+
55+
func _choose_goal():
56+
var field = _game.fallen_chips
57+
58+
# find possible columns.
59+
# better columns will be added more often and therefore are more likely
60+
var possible_columns := []
61+
for i in range(field.size()):
62+
if field[i][0] != null:
63+
continue # can't choose a column that is full
64+
possible_columns.push_back(i)
65+
# find the lowest position in the column
66+
var y := 0
67+
while true:
68+
if !_grid.is_in_grid(Vector2(i, y + 1)):
69+
break
70+
if field[i][y] != null:
71+
break
72+
y += 1
73+
var pos := Vector2(i, y)
74+
# positions with more chips in a line next to them are likely better
75+
for axis in [Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(-1, 1)]:
76+
var run := 0
77+
run += _game.mesure_chip_run(pos, axis, player_id)
78+
run += _game.mesure_chip_run(pos, -axis, player_id)
79+
if run >= 3:
80+
run *= 100
81+
else:
82+
run *= 2
83+
for _j in range(run):
84+
possible_columns.push_back(i)
85+
86+
# choose a column and its global position
87+
var column = possible_columns[randi() % possible_columns.size()]
88+
var column_x = _grid.tile_size.x * column + _grid.tile_size.x / 2 + _grid.position.x
89+
_curve.clear_points()
90+
_curve.add_point(Vector2(column_x, _grid.global_position.y), Vector2(0, -2 * _grid.tile_size.y))
91+
92+
93+
# finish setting up the curve and set the chip to pickup
94+
func _start_pickup():
95+
_curve.add_point(
96+
Vector2(chip.position.x, _grid.global_position.y),
97+
Vector2(),
98+
Vector2(0, -2 * _grid.tile_size.y),
99+
0
100+
)
101+
_curve.add_point(chip.position, Vector2(), Vector2(), 0)
102+
_picked = true
103+
chip.pick()
104+
_timer.start()
105+
update()
106+
107+
108+
# reset everything and let the chip fall
109+
func _unpick():
110+
_picked = false
111+
chip.unpick()
112+
set_process(false)
113+
_curve.clear_points()
114+
update()
115+
116+
117+
func _is_chip_stopped():
118+
if !_pickup_timer.time_left == 0:
119+
return false
120+
return chip.linear_velocity.length() < 5 or chip.sleeping

game/games/asecondguy_connect/ai.tscn

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[gd_scene load_steps=2 format=2]
2+
3+
[ext_resource path="res://games/asecondguy_connect/ai.gd" type="Script" id=1]
4+
5+
[node name="AI" type="Node2D"]
6+
script = ExtResource( 1 )
7+
8+
[node name="Timer" type="Timer" parent="."]
9+
one_shot = true
10+
11+
[node name="PickupTimer" type="Timer" parent="."]
12+
one_shot = true

game/games/asecondguy_connect/chip.gd

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
extends RigidBody2D
2+
3+
var color := Color.green
4+
var player_id := -1
5+
6+
var target_position: Vector2
7+
var _picked := false
8+
9+
10+
func _ready():
11+
apply_central_impulse(Vector2.DOWN * 500)
12+
13+
14+
func _integrate_forces(state):
15+
if _picked:
16+
var move: Vector2 = target_position - global_position
17+
18+
var result := Physics2DTestMotionResult.new()
19+
if !test_motion(move, true, 0.08, result):
20+
state.linear_velocity = move / state.step
21+
else:
22+
state.linear_velocity = move / state.step * result.collision_safe_fraction
23+
state.linear_velocity += result.collision_normal * result.collision_unsafe_fraction
24+
state.angular_velocity = 0
25+
26+
27+
func _on_chip_input_event(_viewport, event, _shape_idx):
28+
if !event is InputEventMouseButton:
29+
return
30+
if event.pressed:
31+
pick()
32+
else:
33+
unpick()
34+
35+
36+
func _input(event):
37+
if mode == MODE_STATIC:
38+
return
39+
if event is InputEventMouseButton:
40+
if !event.pressed:
41+
unpick()
42+
if event is InputEventMouseMotion:
43+
target_position = event.global_position
44+
45+
46+
func _draw():
47+
var radius: float = $CollisionShape2D.shape.radius
48+
draw_circle(Vector2(), radius, color)
49+
draw_circle(Vector2(), radius - 2, color.darkened(0.5))
50+
draw_arc(Vector2(), radius - 3, 0.1, 2, 10, color.lightened(0.5))
51+
52+
53+
func pick():
54+
_picked = true
55+
inertia = 1
56+
sleeping = false
57+
58+
59+
func unpick():
60+
_picked = false
61+
sleeping = false
62+
63+
64+
func set_mouse_control(val: bool):
65+
set_process_input(val)
66+
input_pickable = val
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[gd_scene load_steps=4 format=2]
2+
3+
[ext_resource path="res://games/asecondguy_connect/chip.gd" type="Script" id=1]
4+
5+
[sub_resource type="PhysicsMaterial" id=3]
6+
bounce = 0.1
7+
8+
[sub_resource type="CircleShape2D" id=2]
9+
radius = 39.0
10+
11+
[node name="chip" type="RigidBody2D"]
12+
collision_layer = 3
13+
input_pickable = true
14+
physics_material_override = SubResource( 3 )
15+
gravity_scale = 10.0
16+
continuous_cd = 2
17+
angular_damp = 2.0
18+
script = ExtResource( 1 )
19+
20+
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
21+
shape = SubResource( 2 )
22+
23+
[connection signal="input_event" from="." to="." method="_on_chip_input_event"]
+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
extends Node2D
2+
3+
signal chip_spawned(chip)
4+
5+
const END_MESSAGE := "%s won"
6+
const CHIP_SCRIPT := preload("res://games/asecondguy_connect/chip.gd")
7+
8+
var player_names := []
9+
var player_colors := []
10+
var fallen_chips := []
11+
12+
var _last_chip: CHIP_SCRIPT
13+
var _end_condition := 0
14+
var _chips_played := 0
15+
var _win_chip_pos := []
16+
17+
onready var _play_area := $PlayArea
18+
onready var _grid := $Grid
19+
onready var _chips := $Chips
20+
onready var _spawners := [$Spawner1, $Spawner2]
21+
onready var _info_label := $UI/PanelContainer/InfoLabel
22+
23+
24+
func _ready():
25+
randomize()
26+
# setup the code representation of all played chips
27+
for _x in range(_grid.grid_size.x):
28+
var tmp := []
29+
for _y in range(_grid.grid_size.y):
30+
tmp.push_back(null)
31+
fallen_chips.push_back(tmp)
32+
33+
34+
func start():
35+
_spawn_chip()
36+
37+
38+
func _empty():
39+
$Bounds/Bottom.set_deferred("disabled", true)
40+
for c in _chips.get_children():
41+
# set all chips back to rigid mode
42+
c.set_deferred("mode", 0)
43+
if !_win_chip_pos.empty():
44+
var line: Line2D = preload("res://games/asecondguy_connect/win_line.tscn").instance()
45+
line.grid_points = _win_chip_pos
46+
line.default_color = player_colors[_last_chip.player_id]
47+
_grid.add_child(line)
48+
if _end_condition == 0:
49+
_info_label.start("Draw")
50+
else:
51+
_info_label.start(END_MESSAGE % player_names[_end_condition - 1])
52+
53+
54+
func _on_PlayArea_body_exited(_body):
55+
if _end_condition == -1:
56+
return
57+
if _play_area.get_overlapping_bodies().size() == 0:
58+
if _end_condition == 0:
59+
GameManager.end_game("Draw")
60+
else:
61+
GameManager.end_game(END_MESSAGE % player_names[_end_condition - 1])
62+
63+
64+
func _on_chip_sleep(chip: RigidBody2D):
65+
if !chip.sleeping:
66+
return
67+
_last_chip = chip
68+
var grid_pos: Vector2 = _grid.global_to_grid_pos(chip.position)
69+
if _grid.is_in_grid(grid_pos):
70+
chip.set_deferred("mode", chip.MODE_STATIC)
71+
_chips_played += 1
72+
fallen_chips[grid_pos.x][grid_pos.y] = chip.player_id
73+
_check_game_end_from_chip(grid_pos, chip.player_id)
74+
# max number of chips is 42
75+
if _chips_played == 42 or _end_condition != 0:
76+
_empty()
77+
else:
78+
# [1, 0][last_id] ==> 0 -> 1, 1 -> 0
79+
_spawn_chip([1, 0][chip.player_id])
80+
81+
82+
func _spawn_chip(player_id := 0):
83+
var chip: RigidBody2D = preload("res://games/asecondguy_connect/chip.tscn").instance()
84+
chip.player_id = player_id
85+
chip.color = player_colors[player_id]
86+
chip.global_position = (
87+
_spawners[player_id].global_position
88+
+ Vector2(rand_range(-10, 10), rand_range(-10, 10))
89+
)
90+
if chip.connect("sleeping_state_changed", self, "_on_chip_sleep", [chip]) != OK:
91+
GameManager.end_game("A fatal error occured.")
92+
_chips.call_deferred("add_child", chip)
93+
_info_label.start(player_names[player_id] + "'s turn")
94+
emit_signal("chip_spawned", chip)
95+
96+
97+
# checks the chip and updates _end_condition
98+
func _check_game_end_from_chip(at: Vector2, player_id):
99+
# check all 4 valid axis
100+
for axis in [Vector2(1, 0), Vector2(0, 1), Vector2(1, 1), Vector2(-1, 1)]:
101+
# check both directions of the axis and add them together
102+
var dir1: int = mesure_chip_run(at, axis, player_id)
103+
var dir2: int = mesure_chip_run(at, axis * -1, player_id)
104+
var count := 1 + dir1 + dir2
105+
# if the row is at least 4 long the end condition changes
106+
if count >= 4:
107+
_end_condition = player_id + 1
108+
_win_chip_pos.push_back(at)
109+
for i in range(dir1):
110+
_win_chip_pos.push_back(at + axis * (i + 1))
111+
_win_chip_pos.invert()
112+
for i in range(dir2):
113+
_win_chip_pos.push_back(at - axis * (i + 1))
114+
return
115+
116+
117+
# assumes origin is a valid chip and has the belongs to the player of player_id
118+
# this doesn't count the first chip
119+
# it goes only in one direction (dir) untill it leaves the grid
120+
func mesure_chip_run(origin: Vector2, dir: Vector2, player_id: int) -> int:
121+
var pos := origin + dir
122+
# next position is outside the grid -> run ends
123+
if !_grid.is_in_grid(pos):
124+
return 0
125+
var chip = fallen_chips[pos.x][pos.y]
126+
# the next chip belongs to another player -> run ends
127+
if chip != player_id:
128+
return 0
129+
# the next chip belongs to this run
130+
# so the total run lenght is the run lengh from the next chip +1 (this chip)
131+
return mesure_chip_run(pos, dir, player_id) + 1
132+
133+
134+
func _on_PlayArea_tree_exiting():
135+
_end_condition = -1
3.41 KB
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[remap]
2+
3+
importer="texture"
4+
type="StreamTexture"
5+
path="res://.import/connect.png-1e1b3e1432e1c7036c8850d4920dd90b.stex"
6+
metadata={
7+
"vram_texture": false
8+
}
9+
10+
[deps]
11+
12+
source_file="res://games/asecondguy_connect/connect.png"
13+
dest_files=[ "res://.import/connect.png-1e1b3e1432e1c7036c8850d4920dd90b.stex" ]
14+
15+
[params]
16+
17+
compress/mode=0
18+
compress/lossy_quality=0.7
19+
compress/hdr_mode=0
20+
compress/bptc_ldr=0
21+
compress/normal_map=0
22+
flags/repeat=0
23+
flags/filter=true
24+
flags/mipmaps=false
25+
flags/anisotropic=false
26+
flags/srgb=2
27+
process/fix_alpha_border=true
28+
process/premult_alpha=false
29+
process/HDR_as_SRGB=false
30+
process/invert_color=false
31+
process/normal_map_invert_y=false
32+
stream=false
33+
size_limit=0
34+
detect_3d=true
35+
svg/scale=1.0

0 commit comments

Comments
 (0)