26 Commits

85 changed files with 2461 additions and 1046 deletions

View File

@@ -2,3 +2,9 @@ root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
tab_width = 2
[*.{cs,vb}]
dotnet_diagnostic.CA1050.severity = none

4
.gitignore vendored
View File

@@ -3,7 +3,11 @@
/android/
.idea/
*.csproj
*.csproj*
*.csproj.old
*.csproj.old*
*.sln
*.sln.*
.DS_Store
**/.DS_Store
Folder.DotSettings.user

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Joshua Higgins
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1 +1,9 @@
# Connect4 Moderator - Observer
The front end for the [server](https://github.com/joshuafhiggins/connect4-moderator-server) made in [Godot](https://godotengine.org/), an open-soruce game engine.
# Downloads
See [releases](https://github.com/joshuafhiggins/connect4-moderator-observer/releases)
# For Future Maintainers
This was made in Godot 4.5. Due to the use of C# for a lot of the scripting, we are unable to export for the web but [progress is being made](https://github.com/godotengine/godot/pull/106125). An icon in Icon Composer was made for macOS builds but can't be used till Godot 4.6. Currently, there is also a bug in Godot's Websocket implementation that causes random disconnects, options are submitting upstream fixes and debugging or wrapping a third party library to use. It would also be nice to rework the bracket screen to show the bracket and be more thematic with the other pixel art pieces. Some scripts are leftovers/incomplete implementations of having this program act as a client, which would be helpful to finish.

BIN
assets/.DS_Store vendored

Binary file not shown.

1
assets/music/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
jazz_music.mp3 filter=lfs diff=lfs merge=lfs -text

BIN
assets/music/jazz_music.mp3 LFS Normal file

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://csy7ltflvsjq5"
path="res://.godot/imported/jazz_music.mp3-51c0488dbe42064eda25eafa56d387ed.mp3str"
[deps]
source_file="res://assets/music/jazz_music.mp3"
dest_files=["res://.godot/imported/jazz_music.mp3-51c0488dbe42064eda25eafa56d387ed.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://dauf0pi1pkd3x"
path="res://.godot/imported/chip_collide_1.ogg-406dd38ea9c3d5652eebd8457cb16d54.oggvorbisstr"
[deps]
source_file="res://assets/sfx/chip_collide_1.ogg"
dest_files=["res://.godot/imported/chip_collide_1.ogg-406dd38ea9c3d5652eebd8457cb16d54.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://b6b7b7sc038n7"
path="res://.godot/imported/chip_collide_2.ogg-f3e4af1639a40a7e5696a0d76240e083.oggvorbisstr"
[deps]
source_file="res://assets/sfx/chip_collide_2.ogg"
dest_files=["res://.godot/imported/chip_collide_2.ogg-f3e4af1639a40a7e5696a0d76240e083.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://c6a4wqoopu53j"
path="res://.godot/imported/chip_collide_3.ogg-8dacdc0da1447d7f98373dc814c23342.oggvorbisstr"
[deps]
source_file="res://assets/sfx/chip_collide_3.ogg"
dest_files=["res://.godot/imported/chip_collide_3.ogg-8dacdc0da1447d7f98373dc814c23342.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://5liedrachob2"
path="res://.godot/imported/chip_collide_4.ogg-a636adca2293a212ad4dd420b3445fd5.oggvorbisstr"
[deps]
source_file="res://assets/sfx/chip_collide_4.ogg"
dest_files=["res://.godot/imported/chip_collide_4.ogg-a636adca2293a212ad4dd420b3445fd5.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://dsyovynhhbmw8"
path="res://.godot/imported/chip_collide_5.ogg-43d68269cdbbab204cb581d3f2f55acc.oggvorbisstr"
[deps]
source_file="res://assets/sfx/chip_collide_5.ogg"
dest_files=["res://.godot/imported/chip_collide_5.ogg-43d68269cdbbab204cb581d3f2f55acc.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://c0a5rl5q04noq"
path="res://.godot/imported/chip_collide_6.ogg-277685742efbfc8894a8af489e0a5d40.oggvorbisstr"
[deps]
source_file="res://assets/sfx/chip_collide_6.ogg"
dest_files=["res://.godot/imported/chip_collide_6.ogg-277685742efbfc8894a8af489e0a5d40.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://b6d73wiiqxles"
path="res://.godot/imported/chip_collide_7.ogg-7f61ae7f863bd72768f0889e7ed11946.oggvorbisstr"
[deps]
source_file="res://assets/sfx/chip_collide_7.ogg"
dest_files=["res://.godot/imported/chip_collide_7.ogg-7f61ae7f863bd72768f0889e7ed11946.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

BIN
assets/sfx/game_end.ogg Normal file

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://crxjuk1vyq331"
path="res://.godot/imported/game_end.ogg-8ceab2b7181c15a955c6f20334947ba7.oggvorbisstr"
[deps]
source_file="res://assets/sfx/game_end.ogg"
dest_files=["res://.godot/imported/game_end.ogg-8ceab2b7181c15a955c6f20334947ba7.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View File

@@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View File

@@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View File

@@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

BIN
assets/sprites/back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://2plbtkcttn7o"
path="res://.godot/imported/back.png-73d0d71f353725c6ba9218045b96399b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/back.png"
dest_files=["res://.godot/imported/back.png-73d0d71f353725c6ba9218045b96399b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/sprites/bracket.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cdhuhbt2ws5sy"
path="res://.godot/imported/bracket.png-31c6864ee533d160bca76e6c85f99c4e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/bracket.png"
dest_files=["res://.godot/imported/bracket.png-31c6864ee533d160bca76e6c85f99c4e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/sprites/button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cftuywdnwvop0"
path="res://.godot/imported/button.png-01faf565b773239305f3664038f20e61.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/button.png"
dest_files=["res://.godot/imported/button.png-01faf565b773239305f3664038f20e61.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/sprites/cancel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://stk7umv2ppss"
path="res://.godot/imported/cancel.png-dcc5e0579b1b7ac5745b3b3804e100ab.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/cancel.png"
dest_files=["res://.godot/imported/cancel.png-dcc5e0579b1b7ac5745b3b3804e100ab.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bvind4dms08sd"
path="res://.godot/imported/long_button.png-97ae4bec79a5441e5b3d9a75cbdf673f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/long_button.png"
dest_files=["res://.godot/imported/long_button.png-97ae4bec79a5441e5b3d9a75cbdf673f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/sprites/observe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://da13ksuf4vkqe"
path="res://.godot/imported/observe.png-5a0a3c79f51f4f13ffdfce42e4ad04da.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/observe.png"
dest_files=["res://.godot/imported/observe.png-5a0a3c79f51f4f13ffdfce42e4ad04da.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/sprites/player.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b6iac21s36dku"
path="res://.godot/imported/player.png-e1eaffe0873063c60a0d0b322e4d87d9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/player.png"
dest_files=["res://.godot/imported/player.png-e1eaffe0873063c60a0d0b322e4d87d9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dta0orurmv5qj"
path="res://.godot/imported/player_badge.png-63850d039b551977db5c0c02f6402465.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/player_badge.png"
dest_files=["res://.godot/imported/player_badge.png-63850d039b551977db5c0c02f6402465.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

BIN
assets/sprites/rpi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://ebf78p6bpq5m"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
uid="uid://uritd4ygetrk"
path="res://.godot/imported/rpi.png-0f0faa9ccfa1d0b656d9c381bb4a7a6d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
source_file="res://assets/sprites/rpi.png"
dest_files=["res://.godot/imported/rpi.png-0f0faa9ccfa1d0b656d9c381bb4a7a6d.ctex"]
[params]
@@ -38,6 +38,3 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

6
assets/theme.tres Normal file
View File

@@ -0,0 +1,6 @@
[gd_resource type="Theme" load_steps=2 format=3 uid="uid://bbgxacei1vwba"]
[ext_resource type="FontFile" uid="uid://c3jmev24lo6ci" path="res://assets/fonts/PixelOperator8.ttf" id="1_5x3i2"]
[resource]
default_font = ExtResource("1_5x3i2")

View File

@@ -1,19 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "connect4-moderator-observer", "connect4-moderator-observer.csproj", "{88C189D2-5C9A-411D-8713-291AC0841D8C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{88C189D2-5C9A-411D-8713-291AC0841D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88C189D2-5C9A-411D-8713-291AC0841D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88C189D2-5C9A-411D-8713-291AC0841D8C}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{88C189D2-5C9A-411D-8713-291AC0841D8C}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{88C189D2-5C9A-411D-8713-291AC0841D8C}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{88C189D2-5C9A-411D-8713-291AC0841D8C}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -3,7 +3,6 @@
name="macOS"
platform="macOS"
runnable=true
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
@@ -11,6 +10,11 @@ include_filter=""
exclude_filter=""
export_path="../../Downloads/connect4-moderator-observer.dmg"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
@@ -25,13 +29,14 @@ binary_format/architecture="universal"
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=0
application/icon="uid://ckmfi0cjgxgyk"
application/icon_interpolation=4
application/liquid_glass_icon="res://icon.icon"
application/icon="uid://dd7lvnidxr5ss"
application/icon_interpolation=0
application/bundle_identifier="com.abunchofknowitalls.connect4"
application/signature=""
application/app_category="Games"
application/short_version="0.1.1"
application/version="0.1.1"
application/short_version=""
application/version=""
application/copyright="RPI Minds & Machines"
application/copyright_localized={}
application/min_macos_version_x86_64="10.12"
@@ -49,7 +54,7 @@ xcode/xcode_build="14C18"
codesign/codesign=3
codesign/installer_identity=""
codesign/apple_team_id="8S7C654DQ4"
codesign/identity="Developer ID Application: Joshua Higgins (8S7C654DQ4)"
codesign/identity="73BA692FE950ABC209210ACAA8AD412BD9C6C4A3"
codesign/entitlements/custom_file=""
codesign/entitlements/allow_jit_code_execution=false
codesign/entitlements/allow_unsigned_executable_memory=false
@@ -185,10 +190,10 @@ privacy/collected_data/browsing_history/collected=false
privacy/collected_data/browsing_history/linked_to_user=false
privacy/collected_data/browsing_history/used_for_tracking=false
privacy/collected_data/browsing_history/collection_purposes=0
privacy/collected_data/search_hhistory/collected=false
privacy/collected_data/search_hhistory/linked_to_user=false
privacy/collected_data/search_hhistory/used_for_tracking=false
privacy/collected_data/search_hhistory/collection_purposes=0
privacy/collected_data/search_history/collected=false
privacy/collected_data/search_history/linked_to_user=false
privacy/collected_data/search_history/used_for_tracking=false
privacy/collected_data/search_history/collection_purposes=0
privacy/collected_data/user_id/collected=false
privacy/collected_data/user_id/linked_to_user=false
privacy/collected_data/user_id/used_for_tracking=false
@@ -255,20 +260,28 @@ rm -rf \"{temp_dir}\""
dotnet/include_scripts_content=false
dotnet/include_debug_symbols=false
dotnet/embed_build_outputs=false
privacy/collected_data/search_hhistory/collected=false
privacy/collected_data/search_hhistory/linked_to_user=false
privacy/collected_data/search_hhistory/used_for_tracking=false
privacy/collected_data/search_hhistory/collection_purposes=0
[preset.1]
name="Windows (x86_64)"
platform="Windows Desktop"
runnable=true
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../../Downloads/connect4-moderator-observer (win-x86_64) (0.1.0).exe"
export_path="../../Downloads/connect4-moderator-observer (win-x86_64).exe"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
@@ -293,7 +306,7 @@ codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
application/icon=""
application/icon="uid://dd7lvnidxr5ss"
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
@@ -301,7 +314,7 @@ application/product_version=""
application/company_name=""
application/product_name=""
application/file_description=""
application/copyright=""
application/copyright="RPI Minds & Machines"
application/trademarks=""
application/export_angle=0
application/export_d3d12=0
@@ -325,21 +338,25 @@ Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorActi
Remove-Item -Recurse -Force '{temp_dir}'"
dotnet/include_scripts_content=false
dotnet/include_debug_symbols=false
dotnet/embed_build_outputs=false
dotnet/embed_build_outputs=true
[preset.2]
name="Windows (arm64)"
platform="Windows Desktop"
runnable=false
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../../Downloads/connect4-moderator-observer (win-arm64) (0.1.0).exe"
export_path="../../Downloads/connect4-moderator-observer (win-arm64).exe"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
@@ -364,7 +381,7 @@ codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
application/icon=""
application/icon="uid://dd7lvnidxr5ss"
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
@@ -396,21 +413,25 @@ Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorActi
Remove-Item -Recurse -Force '{temp_dir}'"
dotnet/include_scripts_content=false
dotnet/include_debug_symbols=false
dotnet/embed_build_outputs=false
dotnet/embed_build_outputs=true
[preset.3]
name="Linux (x86_64)"
platform="Linux"
runnable=true
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../../Downloads/connect4-moderator-observer (linux-x86_64) (0.1.0)"
export_path="../../Downloads/connect4-moderator-observer (linux-x86_64).out"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
@@ -426,7 +447,7 @@ debug/export_console_wrapper=0
binary_format/embed_pck=true
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
shader_baker/enabled=true
shader_baker/enabled=false
binary_format/architecture="x86_64"
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
@@ -442,21 +463,25 @@ kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
dotnet/include_scripts_content=false
dotnet/include_debug_symbols=false
dotnet/embed_build_outputs=false
dotnet/embed_build_outputs=true
[preset.4]
name="Linux (arm64)"
platform="Linux"
runnable=false
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../../Downloads/connect4-moderator-observer (linux-arm64) (0.1.0)"
export_path="../../Downloads/connect4-moderator-observer (linux-arm64).out"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
patch_delta_min_reduction=0.1
patch_delta_include_filters="*"
patch_delta_exclude_filters=""
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
@@ -472,7 +497,7 @@ debug/export_console_wrapper=0
binary_format/embed_pck=true
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
shader_baker/enabled=true
shader_baker/enabled=false
binary_format/architecture="arm64"
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
@@ -488,4 +513,4 @@ kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
dotnet/include_scripts_content=false
dotnet/include_debug_symbols=false
dotnet/embed_build_outputs=false
dotnet/embed_build_outputs=true

BIN
icon.icon/Assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dbn4ae2a7ao0t"
path="res://.godot/imported/icon.png-d1b269abce4c61d818c5589654ffbcc2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.icon/Assets/icon.png"
dest_files=["res://.godot/imported/icon.png-d1b269abce4c61d818c5589654ffbcc2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

37
icon.icon/icon.json Normal file
View File

@@ -0,0 +1,37 @@
{
"fill" : {
"automatic-gradient" : "extended-srgb:0.00000,0.53333,1.00000,1.00000"
},
"groups" : [
{
"layers" : [
{
"glass" : true,
"image-name" : "icon.png",
"name" : "icon",
"position" : {
"scale" : 0.2,
"translation-in-points" : [
0,
0
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

40
icon.png.import Normal file
View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dd7lvnidxr5ss"
path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.png"
dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

Before

Width:  |  Height:  |  Size: 995 B

View File

@@ -8,16 +8,31 @@
config_version=5
[animation]
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application]
config/name="connect4-moderator-observer"
config/name="Connect4 Observer"
config/version="1.0.1"
run/main_scene="uid://dcx5nvs0pa7me"
config/features=PackedStringArray("4.5", "C#", "Forward Plus")
config/icon="uid://ckmfi0cjgxgyk"
config/features=PackedStringArray("4.6", "C#", "Forward Plus")
boot_splash/image="uid://dd7lvnidxr5ss"
config/icon="uid://dd7lvnidxr5ss"
[autoload]
Connection="*res://scripts/Connection.cs"
BackgroundMusic="*res://scripts/background_music.gd"
[display]
window/size/viewport_width=1280
window/size/viewport_height=720
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
window/vsync/vsync_mode=2
[dotnet]
@@ -28,3 +43,5 @@ project/assembly_name="connect4-moderator-observer"
textures/canvas_textures/default_texture_filter=0
textures/vram_compression/import_s3tc_bptc=true
textures/vram_compression/import_etc2_astc=true
anti_aliasing/quality/msaa_2d=2
anti_aliasing/quality/msaa_3d=2

View File

@@ -1,13 +1,16 @@
[gd_scene load_steps=14 format=3 uid="uid://m542qwlp7hl7"]
[gd_scene load_steps=17 format=3 uid="uid://m542qwlp7hl7"]
[ext_resource type="Script" uid="uid://dg5jt0o0r0v3r" path="res://scripts/BoardScreen.cs" id="1_b3w8x"]
[ext_resource type="PackedScene" uid="uid://rl33x81cxlh0" path="res://scenes/bracket_view.tscn" id="2_u1oi2"]
[ext_resource type="AudioStream" uid="uid://crxjuk1vyq331" path="res://assets/sfx/game_end.ogg" id="2_kseed"]
[ext_resource type="Texture2D" uid="uid://dlx02qat7j6lf" path="res://assets/sprites/AssetTileset.png" id="3_1tlhv"]
[ext_resource type="Theme" uid="uid://bbgxacei1vwba" path="res://assets/theme.tres" id="3_3louw"]
[ext_resource type="FontFile" uid="uid://c3jmev24lo6ci" path="res://assets/fonts/PixelOperator8.ttf" id="3_rjcmr"]
[ext_resource type="Texture2D" uid="uid://ckmfi0cjgxgyk" path="res://assets/sprites/RedChip.png" id="4_1hrcj"]
[ext_resource type="Texture2D" uid="uid://qy30emdgrk7o" path="res://assets/sprites/YellowChip.png" id="5_i2o8i"]
[ext_resource type="Script" uid="uid://cwfg17tdbk44b" path="res://scripts/thinking.gd" id="5_wjs8a"]
[ext_resource type="Texture2D" uid="uid://8un28mol7qow" path="res://assets/sprites/BoardTileMap.png" id="6_i2o8i"]
[ext_resource type="PackedScene" uid="uid://pdean68jjg80" path="res://scenes/button_small.tscn" id="7_glh1q"]
[ext_resource type="Script" uid="uid://b3q4gq63qmx23" path="res://scripts/BackButton.cs" id="8_u1oi2"]
[sub_resource type="WorldBoundaryShape2D" id="WorldBoundaryShape2D_b3w8x"]
@@ -31,10 +34,10 @@ sources/0 = SubResource("TileSetAtlasSource_i2o8i")
atlas = ExtResource("3_1tlhv")
region = Rect2(112, 32, 16, 16)
[node name="BoardScreen" type="Node2D" node_paths=PackedStringArray("BackButton")]
[node name="BoardScreen" type="Node2D"]
script = ExtResource("1_b3w8x")
BracketScene = ExtResource("2_u1oi2")
BackButton = NodePath("BracketButton")
endingSfx = ExtResource("2_kseed")
theme = ExtResource("3_3louw")
[node name="Floor Collider" type="StaticBody2D" parent="."]
position = Vector2(0, 200)
@@ -77,6 +80,7 @@ position = Vector2(274, 0)
shape = SubResource("SegmentShape2D_i2o8i")
[node name="Player1Card" type="Node2D" parent="."]
position = Vector2(0, 8)
[node name="Left" type="Sprite2D" parent="Player1Card"]
position = Vector2(-556, -300)
@@ -117,12 +121,13 @@ offset_left = -529.0
offset_top = -292.0
offset_right = -427.0
offset_bottom = -284.0
theme_override_colors/font_color = Color(1, 0, 0, 1)
theme_override_fonts/font = ExtResource("3_rjcmr")
theme_override_font_sizes/font_size = 8
text = "NOT READY"
text = "THINKING"
script = ExtResource("5_wjs8a")
[node name="Player2Card" type="Node2D" parent="."]
position = Vector2(989, -64)
[node name="Left" type="Sprite2D" parent="Player2Card"]
position = Vector2(-556, -228)
@@ -163,10 +168,10 @@ offset_left = -530.0
offset_top = -220.0
offset_right = -428.0
offset_bottom = -212.0
theme_override_colors/font_color = Color(1, 0, 0, 1)
theme_override_fonts/font = ExtResource("3_rjcmr")
theme_override_font_sizes/font_size = 8
text = "NOT READY"
text = "THINKING"
script = ExtResource("5_wjs8a")
[node name="TileMap" type="TileMap" parent="."]
position = Vector2(39, 200)
@@ -181,6 +186,7 @@ offset_left = -566.0
offset_top = 281.0
offset_right = -550.0
offset_bottom = 297.0
script = ExtResource("8_u1oi2")
[node name="Sprite2D" type="Sprite2D" parent="BracketButton"]
position = Vector2(8, 8)

View File

@@ -1,10 +1,22 @@
[gd_scene load_steps=5 format=3 uid="uid://rl33x81cxlh0"]
[gd_scene load_steps=8 format=3 uid="uid://rl33x81cxlh0"]
[ext_resource type="Theme" uid="uid://bbgxacei1vwba" path="res://assets/theme.tres" id="1_as653"]
[ext_resource type="Script" uid="uid://dm25u0a2lqk2x" path="res://scripts/BracketScene.cs" id="1_dvj3m"]
[ext_resource type="Texture2D" uid="uid://ckmfi0cjgxgyk" path="res://assets/sprites/RedChip.png" id="2_7c11m"]
[ext_resource type="PackedScene" uid="uid://b4tujjdhmk4h" path="res://scenes/red_chip.tscn" id="3_1511b"]
[ext_resource type="Texture2D" uid="uid://da13ksuf4vkqe" path="res://assets/sprites/observe.png" id="2_mbqc8"]
[ext_resource type="Texture2D" uid="uid://stk7umv2ppss" path="res://assets/sprites/cancel.png" id="3_as653"]
[ext_resource type="Script" uid="uid://1y72woiynf31" path="res://scripts/AdminControls.cs" id="4_mbqc8"]
[sub_resource type="Gradient" id="Gradient_wu84c"]
colors = PackedColorArray(0, 0.07058824, 0.101960786, 1, 0.39215687, 0.39215687, 0.39215687, 1)
[sub_resource type="GradientTexture2D" id="GradientTexture2D_as653"]
gradient = SubResource("Gradient_wu84c")
width = 1024
height = 1024
fill_from = Vector2(0.5, 1)
fill_to = Vector2(0.5, 0)
metadata/_snap_enabled = true
[node name="BracketView" type="Control" node_paths=PackedStringArray("Players", "Matches")]
layout_mode = 3
anchors_preset = 15
@@ -12,11 +24,23 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_as653")
script = ExtResource("1_dvj3m")
Players = NodePath("HBoxContainer/PlayerList")
Matches = NodePath("HBoxContainer/MatchList")
JoinButton = ExtResource("2_7c11m")
BoardScene = ExtResource("3_1511b")
WatchButton = ExtResource("2_mbqc8")
TerminateKickButton = ExtResource("3_as653")
[node name="Background" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = -152.0
offset_bottom = 152.0
grow_horizontal = 2
grow_vertical = 2
texture = SubResource("GradientTexture2D_as653")
[node name="ColorRect" type="ColorRect" parent="."]
custom_minimum_size = Vector2(0, 36)
@@ -26,7 +50,7 @@ anchor_right = 1.0
grow_horizontal = 2
color = Color(0.13319641, 0.13319641, 0.13319638, 1)
[node name="AdminControls" type="HBoxContainer" parent="ColorRect" node_paths=PackedStringArray("BecomeAdmin", "StartTournament")]
[node name="AdminControls" type="HBoxContainer" parent="ColorRect" node_paths=PackedStringArray("BecomeAdmin", "StartTournament", "CancelTournament", "Label", "Timeout")]
custom_minimum_size = Vector2(0, 36)
layout_mode = 1
anchors_preset = 10
@@ -36,6 +60,9 @@ grow_horizontal = 2
script = ExtResource("4_mbqc8")
BecomeAdmin = NodePath("BecomeAdmin")
StartTournament = NodePath("StartTournament")
CancelTournament = NodePath("CancelTournament")
Label = NodePath("Label")
Timeout = NodePath("HSlider")
[node name="BecomeAdmin" type="Button" parent="ColorRect/AdminControls"]
layout_mode = 2
@@ -45,6 +72,25 @@ text = "Become Admin"
layout_mode = 2
text = "Start Tournament"
[node name="CancelTournament" type="Button" parent="ColorRect/AdminControls"]
layout_mode = 2
text = "Cancel Tournament"
[node name="Label" type="Label" parent="ColorRect/AdminControls"]
layout_mode = 2
text = "Wait To Move: 5.0s "
[node name="HSlider" type="HSlider" parent="ColorRect/AdminControls"]
custom_minimum_size = Vector2(256, 0)
layout_mode = 2
size_flags_vertical = 1
min_value = 0.1
max_value = 5.0
step = 0.2
value = 5.0
tick_count = 25
ticks_on_borders = true
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 1
anchors_preset = -1

View File

@@ -1,10 +1,33 @@
[gd_scene load_steps=3 format=3 uid="uid://cr8fi0e4r88s8"]
[gd_scene load_steps=5 format=3 uid="uid://cr8fi0e4r88s8"]
[ext_resource type="PackedScene" uid="uid://cct663hb47yka" path="res://scenes/create_join_room.tscn" id="1_yqjtg"]
[ext_resource type="PackedScene" uid="uid://m542qwlp7hl7" path="res://scenes/board_screen.tscn" id="2_lnu2h"]
[sub_resource type="Gradient" id="Gradient_wu84c"]
colors = PackedColorArray(0, 0.07058824, 0.101960786, 1, 0.39215687, 0.39215687, 0.39215687, 1)
[sub_resource type="GradientTexture2D" id="GradientTexture2D_yqjtg"]
gradient = SubResource("Gradient_wu84c")
width = 1024
height = 1024
fill_from = Vector2(0.5, 1)
fill_to = Vector2(0.5, 0)
metadata/_snap_enabled = true
[node name="Game" type="Node2D"]
[node name="Background" type="TextureRect" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -658.0
offset_top = -547.0
offset_right = 668.0
offset_bottom = 657.0
grow_horizontal = 2
grow_vertical = 2
texture = SubResource("GradientTexture2D_yqjtg")
[node name="GameManager" type="Node" parent="."]
[node name="Camera2D" type="Camera2D" parent="."]

View File

@@ -1,7 +1,20 @@
[gd_scene load_steps=3 format=3 uid="uid://dcx5nvs0pa7me"]
[gd_scene load_steps=7 format=3 uid="uid://dcx5nvs0pa7me"]
[ext_resource type="Script" uid="uid://bk22f71oximjk" path="res://scripts/AddressUI.cs" id="1_l6cm7"]
[ext_resource type="Theme" uid="uid://bbgxacei1vwba" path="res://assets/theme.tres" id="1_wu84c"]
[ext_resource type="Script" uid="uid://cpjbiqn26khck" path="res://scripts/ConnectButtonUI.cs" id="2_ekxnf"]
[ext_resource type="Texture2D" uid="uid://uritd4ygetrk" path="res://assets/sprites/rpi.png" id="3_bqqt6"]
[sub_resource type="Gradient" id="Gradient_wu84c"]
colors = PackedColorArray(0, 0.07058824, 0.101960786, 1, 0.39215687, 0.39215687, 0.39215687, 1)
[sub_resource type="GradientTexture2D" id="GradientTexture2D_yqjtg"]
gradient = SubResource("Gradient_wu84c")
width = 1024
height = 1024
fill_from = Vector2(0.5, 1)
fill_to = Vector2(0.5, 0)
metadata/_snap_enabled = true
[node name="Control" type="Control"]
layout_mode = 3
@@ -11,6 +24,63 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Background" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = SubResource("GradientTexture2D_yqjtg")
[node name="RPI Minds and Machines" type="Label" parent="."]
layout_mode = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -193.5
offset_top = 128.0
offset_right = 193.5
offset_bottom = 152.0
grow_horizontal = 2
theme = ExtResource("1_wu84c")
theme_type_variation = &"HeaderMedium"
text = "RPI Minds & Machines"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Connect" type="Label" parent="."]
layout_mode = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -100.0
offset_top = 200.0
offset_right = 68.00122
offset_bottom = 229.0
grow_horizontal = 2
theme = ExtResource("1_wu84c")
theme_type_variation = &"HeaderLarge"
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 16
text = "Connect"
[node name="4" type="Label" parent="."]
layout_mode = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = 78.0
offset_top = 200.0
offset_right = 118.00116
offset_bottom = 229.0
grow_horizontal = 2
theme = ExtResource("1_wu84c")
theme_type_variation = &"HeaderLarge"
theme_override_colors/font_color = Color(1, 0, 0, 1)
theme_override_constants/outline_size = 16
text = "4"
[node name="Address" type="TextEdit" parent="."]
layout_mode = 1
anchors_preset = 8
@@ -24,10 +94,13 @@ offset_right = 250.0
offset_bottom = 20.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_wu84c")
placeholder_text = "Server Address"
emoji_menu_enabled = false
scroll_smooth = true
script = ExtResource("1_l6cm7")
[node name="Button" type="Button" parent="." node_paths=PackedStringArray("AddressUi", "ErrorLabel")]
[node name="Button" type="Button" parent="." node_paths=PackedStringArray("AddressField", "ErrorLabel")]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
@@ -40,9 +113,10 @@ offset_right = 250.0
offset_bottom = 67.25
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_wu84c")
text = "Connect"
script = ExtResource("2_ekxnf")
AddressUi = NodePath("../Address")
AddressField = NodePath("../Address")
ErrorLabel = NodePath("../Label")
[node name="Label" type="Label" parent="."]
@@ -58,4 +132,17 @@ offset_right = 68.5
offset_bottom = 98.149994
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_wu84c")
theme_override_colors/font_color = Color(1, 0, 0, 1)
[node name="TextureRect" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 2
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 5.0
offset_top = -69.0
offset_right = 69.0
offset_bottom = -5.0
grow_vertical = 0
texture = ExtResource("3_bqqt6")

View File

@@ -1,13 +1,25 @@
[gd_scene load_steps=4 format=3 uid="uid://b4tujjdhmk4h"]
[gd_scene load_steps=11 format=3 uid="uid://b4tujjdhmk4h"]
[ext_resource type="Texture2D" uid="uid://ckmfi0cjgxgyk" path="res://assets/sprites/RedChip.png" id="1_qsflu"]
[ext_resource type="Script" uid="uid://dd5nu6037qsr0" path="res://scripts/red_chip.gd" id="1_tfypd"]
[ext_resource type="Script" uid="uid://dd5nu6037qsr0" path="res://scripts/chip_sfx.gd" id="1_tfypd"]
[ext_resource type="AudioStream" uid="uid://dauf0pi1pkd3x" path="res://assets/sfx/chip_collide_1.ogg" id="2_g7r6w"]
[ext_resource type="AudioStream" uid="uid://b6b7b7sc038n7" path="res://assets/sfx/chip_collide_2.ogg" id="3_l66m7"]
[ext_resource type="AudioStream" uid="uid://c6a4wqoopu53j" path="res://assets/sfx/chip_collide_3.ogg" id="4_5isma"]
[ext_resource type="AudioStream" uid="uid://5liedrachob2" path="res://assets/sfx/chip_collide_4.ogg" id="5_sa1hx"]
[ext_resource type="AudioStream" uid="uid://dsyovynhhbmw8" path="res://assets/sfx/chip_collide_5.ogg" id="6_752ap"]
[ext_resource type="AudioStream" uid="uid://c0a5rl5q04noq" path="res://assets/sfx/chip_collide_6.ogg" id="7_ky42j"]
[ext_resource type="AudioStream" uid="uid://b6d73wiiqxles" path="res://assets/sfx/chip_collide_7.ogg" id="8_lgcne"]
[sub_resource type="CircleShape2D" id="CircleShape2D_tfypd"]
radius = 13.017083
radius = 12.8
[node name="RedChip" type="RigidBody2D"]
[node name="RedChip" type="RigidBody2D" node_paths=PackedStringArray("audio_stream_player_2d")]
lock_rotation = true
contact_monitor = true
max_contacts_reported = 3
script = ExtResource("1_tfypd")
audio_stream_player_2d = NodePath("AudioStreamPlayer2D")
sounds = Array[AudioStream]([ExtResource("2_g7r6w"), ExtResource("3_l66m7"), ExtResource("4_5isma"), ExtResource("5_sa1hx"), ExtResource("6_752ap"), ExtResource("7_ky42j"), ExtResource("8_lgcne")])
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(3, 3)
@@ -16,3 +28,8 @@ texture = ExtResource("1_qsflu")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
scale = Vector2(3, 3)
shape = SubResource("CircleShape2D_tfypd")
[node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="."]
volume_db = -5.0
[connection signal="body_entered" from="." to="." method="_on_body_entered"]

View File

@@ -1,13 +1,25 @@
[gd_scene load_steps=4 format=3 uid="uid://lruk652t0xe5"]
[gd_scene load_steps=11 format=3 uid="uid://lruk652t0xe5"]
[ext_resource type="Script" uid="uid://ddg7dv686sbrb" path="res://scripts/yellow_chip.gd" id="1_epi6l"]
[ext_resource type="Script" uid="uid://dd5nu6037qsr0" path="res://scripts/chip_sfx.gd" id="1_epi6l"]
[ext_resource type="Texture2D" uid="uid://qy30emdgrk7o" path="res://assets/sprites/YellowChip.png" id="1_eu0sq"]
[ext_resource type="AudioStream" uid="uid://dauf0pi1pkd3x" path="res://assets/sfx/chip_collide_1.ogg" id="2_ki13g"]
[ext_resource type="AudioStream" uid="uid://b6b7b7sc038n7" path="res://assets/sfx/chip_collide_2.ogg" id="3_kic5w"]
[ext_resource type="AudioStream" uid="uid://c6a4wqoopu53j" path="res://assets/sfx/chip_collide_3.ogg" id="4_nbcpr"]
[ext_resource type="AudioStream" uid="uid://5liedrachob2" path="res://assets/sfx/chip_collide_4.ogg" id="5_wq2sm"]
[ext_resource type="AudioStream" uid="uid://dsyovynhhbmw8" path="res://assets/sfx/chip_collide_5.ogg" id="6_ik045"]
[ext_resource type="AudioStream" uid="uid://c0a5rl5q04noq" path="res://assets/sfx/chip_collide_6.ogg" id="7_evobg"]
[ext_resource type="AudioStream" uid="uid://b6d73wiiqxles" path="res://assets/sfx/chip_collide_7.ogg" id="8_b44ux"]
[sub_resource type="CircleShape2D" id="CircleShape2D_epi6l"]
radius = 13.004272
radius = 12.8
[node name="YellowChip" type="RigidBody2D"]
[node name="YellowChip" type="RigidBody2D" node_paths=PackedStringArray("audio_stream_player_2d")]
lock_rotation = true
contact_monitor = true
max_contacts_reported = 3
script = ExtResource("1_epi6l")
audio_stream_player_2d = NodePath("AudioStreamPlayer2D")
sounds = Array[AudioStream]([ExtResource("2_ki13g"), ExtResource("3_kic5w"), ExtResource("4_nbcpr"), ExtResource("5_wq2sm"), ExtResource("6_ik045"), ExtResource("7_evobg"), ExtResource("8_b44ux")])
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(3, 3)
@@ -16,3 +28,8 @@ texture = ExtResource("1_eu0sq")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
scale = Vector2(3, 3)
shape = SubResource("CircleShape2D_epi6l")
[node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="."]
volume_db = -5.0
[connection signal="body_entered" from="." to="." method="_on_body_entered"]

View File

@@ -1,10 +1,6 @@
using Godot;
using System;
public partial class AddressUI : TextEdit
{
public override void _Ready()
{
Text = Connection.WS_DEFAULT_ADDRESS;
}
public partial class AddressUI : TextEdit {
public override void _Ready() { Text = Connection.WS_DEFAULT_ADDRESS; }
}

View File

@@ -1,123 +1,133 @@
using Godot;
using System;
using System.Collections.Generic;
public partial class AdminControls : HBoxContainer
{
[Export] public Button BecomeAdmin;
[Export] public Button StartTournament;
public override void _Ready()
public partial class AdminControls : HBoxContainer {
[Export] public Button BecomeAdmin;
[Export] public Button StartTournament;
[Export] public Button CancelTournament;
[Export] public Label Label;
[Export] public Slider Timeout;
public override void _Ready() {
Connection.Instance.OnBecomeAdmin += UpdateUI;
Connection.Instance.OnTournamentEnd += UpdateUI;
Connection.Instance.OnStartTournament += UpdateUI;
Connection.Instance.OnCancelTournamentAck += UpdateUI;
Connection.Instance.OnGetDataAcks += UpdateUI;
Connection.Instance.OnSetDataAcks += UpdateUI;
StartTournament.Pressed += () => Connection.Instance.StartTournament();
CancelTournament.Pressed += () => Connection.Instance.CancelTournament();
UpdateUI();
Timeout.ValueChanged += value =>
{
Connection.Instance.OnBecomeAdmin += OnBecomeAdmin;
Connection.Instance.OnTournamentEnd += OnEndTournament;
StartTournament.Pressed += StartTournamentCommand;
if (!Connection.Instance.IsAdmin || Connection.Instance.ActiveTournament)
{
StartTournament.Hide();
Connection.Instance.SetMoveWait((float)value);
var time = Connection.Instance.CurrentWaitTimeout.ToString();
if (time.Length > 3) {
time = time.Substring(0, 3);
}
Label.Text = "Wait To Move: " + time + "s ";
};
BecomeAdmin.Pressed += showAuthPopup;
}
public override void _ExitTree() {
Connection.Instance.OnBecomeAdmin -= UpdateUI;
Connection.Instance.OnTournamentEnd -= UpdateUI;
Connection.Instance.OnStartTournament -= UpdateUI;
Connection.Instance.OnCancelTournamentAck -= UpdateUI;
Connection.Instance.OnGetDataAcks -= UpdateUI;
Connection.Instance.OnSetDataAcks -= UpdateUI;
}
private void UpdateUI() {
if (!Connection.Instance.IsAdmin) {
BecomeAdmin.Show();
StartTournament.Hide();
CancelTournament.Hide();
Label.Hide();
Timeout.Hide();
} else {
BecomeAdmin.Hide();
Label.Show();
Timeout.Show();
}
if (Connection.Instance.IsAdmin && Connection.Instance.DemoMode) {
StartTournament.Hide();
CancelTournament.Hide();
} else if (Connection.Instance.IsAdmin && Connection.Instance.ActiveTournament != TournamentType.None) {
StartTournament.Hide();
CancelTournament.Show();
} else if (Connection.Instance.IsAdmin) {
StartTournament.Show();
CancelTournament.Hide();
}
Timeout.Value = Connection.Instance.CurrentWaitTimeout;
var time = Connection.Instance.CurrentWaitTimeout.ToString();
if (time.Length > 3) {
time = time.Substring(0, 3);
}
Label.Text = "Wait To Move: " + time + "s ";
}
private void showAuthPopup() {
var authWindow = new Window();
authWindow.Theme = GD.Load<Theme>("res://assets/theme.tres");
authWindow.AlwaysOnTop = true;
authWindow.MaximizeDisabled = true;
authWindow.Unresizable = true;
authWindow.InitialPosition = Window.WindowInitialPosition.CenterMainWindowScreen;
authWindow.Size = new Vector2I(256, 128);
authWindow.CloseRequested += () =>
{
GetTree().Root.CallDeferred(Node.MethodName.RemoveChild, authWindow);
};
var vbox = new VBoxContainer();
vbox.LayoutMode = 1;
vbox.AnchorBottom = 1.0f;
vbox.AnchorRight = 1.0f;
vbox.GrowHorizontal = GrowDirection.Both;
vbox.GrowVertical = GrowDirection.Both;
vbox.Alignment = AlignmentMode.Center;
var passwordBox = new TextEdit();
passwordBox.PlaceholderText = "Password";
passwordBox.SetCustomMinimumSize(new Vector2(32, 32));
passwordBox.GuiInput += e =>
{
if (passwordBox.HasFocus() && e is InputEventKey inputEventKey && inputEventKey.IsPressed()) {
if (inputEventKey.KeyLabel == Key.Enter) {
Connection.Instance.AdminAuth(passwordBox.Text);
GetTree().Root.CallDeferred(Node.MethodName.RemoveChild, authWindow);
GetViewport().SetInputAsHandled();
}
BecomeAdmin.Pressed += ShowAuthPopup;
}
public override void _ExitTree()
{
Connection.Instance.OnBecomeAdmin -= OnBecomeAdmin;
Connection.Instance.OnTournamentEnd -= OnEndTournament;
}
private void StartTournamentCommand()
{
Connection.Instance.StartTournament();
}
private void OnEndTournament(List<(string, int)> playerScoreboard)
{
StartTournament.Show();
ShowTournamentScoreboard(playerScoreboard);
}
private void ShowAuthPopup()
{
var authWindow = new Window();
authWindow.AlwaysOnTop = true;
authWindow.MaximizeDisabled = true;
authWindow.Unresizable = true;
authWindow.InitialPosition = Window.WindowInitialPosition.CenterMainWindowScreen;
authWindow.Size = new Vector2I(256, 128);
authWindow.CloseRequested += () =>
{
GetTree().Root.RemoveChild(authWindow);
};
var vbox = new VBoxContainer();
vbox.LayoutMode = 1;
vbox.AnchorBottom = 1.0f;
vbox.AnchorRight = 1.0f;
vbox.GrowHorizontal = GrowDirection.Both;
vbox.GrowVertical = GrowDirection.Both;
vbox.Alignment = AlignmentMode.Center;
var passwordBox = new TextEdit();
passwordBox.PlaceholderText = "Password";
passwordBox.SetCustomMinimumSize(new Vector2(32, 32));
var button = new Button();
button.Text = "Login";
button.Pressed += () =>
{
Connection.Instance.AdminAuth(passwordBox.Text);
GetTree().Root.RemoveChild(authWindow);
};
vbox.AddChild(passwordBox);
vbox.AddChild(button);
authWindow.AddChild(vbox);
GetTree().Root.AddChild(authWindow);
}
private void ShowTournamentScoreboard(List<(string, int)> playerScoreboard)
{
var scoreboardWindow = new Window();
scoreboardWindow.AlwaysOnTop = true;
scoreboardWindow.MaximizeDisabled = true;
scoreboardWindow.Unresizable = true;
scoreboardWindow.InitialPosition = Window.WindowInitialPosition.CenterMainWindowScreen;
scoreboardWindow.Size = new Vector2I(256, 512);
scoreboardWindow.CloseRequested += () =>
{
GetTree().Root.RemoveChild(scoreboardWindow);
};
var tree = new Tree();
tree.HideRoot = true;
tree.Columns = 2;
tree.ColumnTitlesVisible = true;
tree.SetColumnTitle(0, "Player");
tree.SetColumnTitle(1, "Score");
var root = tree.CreateItem();
foreach ((string, int) entry in playerScoreboard)
{
var item = tree.CreateItem(root);
item.SetText(0, entry.Item1);
item.SetText(1, entry.Item2.ToString());
if (inputEventKey.KeyLabel == Key.Space) {
GetViewport().SetInputAsHandled();
}
scoreboardWindow.AddChild(tree);
GetTree().Root.AddChild(scoreboardWindow);
}
}
};
private void OnBecomeAdmin()
var button = new Button();
button.Text = "Login";
button.Pressed += () =>
{
BecomeAdmin.Hide();
if (!Connection.Instance.ActiveTournament)
{
StartTournament.Show();
}
}
Connection.Instance.AdminAuth(passwordBox.Text);
GetTree().Root.CallDeferred(Node.MethodName.RemoveChild, authWindow);
};
vbox.AddChild(passwordBox);
vbox.AddChild(button);
authWindow.AddChild(vbox);
GetTree().Root.AddChild(authWindow);
}
}

13
scripts/BackButton.cs Normal file
View File

@@ -0,0 +1,13 @@
using Godot;
using System;
public partial class BackButton : TextureButton {
private const string BRACKET_SCENE_PATH = "res://scenes/bracket_view.tscn";
public override void _Pressed() {
transitionToBracket();
base._Pressed();
}
private void transitionToBracket() { GetTree().ChangeSceneToFile(BRACKET_SCENE_PATH); }
}

View File

@@ -0,0 +1 @@
uid://b3q4gq63qmx23

View File

@@ -1,255 +1,241 @@
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
public partial class BoardScreen : Node2D {
[Export] public BaseButton BackButton;
private const string RED_CHIP_PATH = "res://scenes/red_chip.tscn";
private const string YELLOW_CHIP_PATH = "res://scenes/yellow_chip.tscn";
private const string BRACKET_SCENE_PATH = "res://scenes/bracket_view.tscn";
private const int CHIP_SCALE = 3;
private const int CHIP_SIZE = 24;
private const int CHIP_PADDING = 2;
private const int CHIP_X_OFF = -(CHIP_SIZE + CHIP_PADDING) * 7 / 2 + (CHIP_SIZE + CHIP_PADDING) / 2;
private const int Y_OFF = 300;
private const int CARD_CENTER_X_DEFAULT = -536;
[Export] private AudioStream endingSfx;
[Export] private Theme theme;
private PackedScene redChip;
private PackedScene ylwChip;
private const string RED_CHIP_PATH = "res://scenes/red_chip.tscn";
private const string YELLOW_CHIP_PATH = "res://scenes/yellow_chip.tscn";
private const string BRACKET_SCENE_PATH = "res://scenes/bracket_view.tscn";
private const int CHIP_SCALE = 3;
private const int CHIP_SIZE = 24;
private const int CHIP_PADDING = 2;
private const int CHIP_X_OFF = -(CHIP_SIZE + CHIP_PADDING) * 7 / 2 + (CHIP_SIZE + CHIP_PADDING) / 2;
private const int Y_OFF = 300;
private const int CARD_CENTER_X_DEFAULT = -536;
private const double MOVE_TIMEOUT_BEFORE_PLACE = 1.0f;
private Node2D player1Card;
private Node2D player2Card;
private MatchData matchData;
private double currentTimeout = 0.0f;
private RigidBody2D[,] chips = new RigidBody2D[6, 7]; // 6 rows 7 cols | 0, 0 is top left
private PackedScene redChip;
private PackedScene ylwChip;
// Called when the node enters the scene tree for the first time.
public override void _Ready() {
// Node initialization
player1Card = GetNode<Node2D>("Player1Card");
player2Card = GetNode<Node2D>("Player2Card");
private Node2D player1Card;
private Node2D player2Card;
private MatchData matchData;
player1Card.GetNode<Label>("Name").Resized += () => setPlayerCardScale((player1Card.GetNode<Label>("Name").GetRect().Size.X + 7) / 16, player1Card);
player2Card.GetNode<Label>("Name").Resized += () => setPlayerCardScale((player2Card.GetNode<Label>("Name").GetRect().Size.X + 7) / 16, player2Card);
private RigidBody2D[,] chips = new RigidBody2D[6, 7]; // 6 rows 7 cols | 0, 0 is top left
redChip = GD.Load<PackedScene>(RED_CHIP_PATH);
ylwChip = GD.Load<PackedScene>(YELLOW_CHIP_PATH);
matchData = Connection.Instance.CurrentObservingMatch;
player1Card.GetNode<Label>("Name").Text = matchData.player1;
player2Card.GetNode<Label>("Name").Text = matchData.player2;
private bool _lastMove = false;
private float _lastMoveTimer = 2.5f;
private string _winner = "";
GetNode<TextureButton>("BracketButton").Pressed += TransitionToBracket;
// Called when the node enters the scene tree for the first time.
public override void _Ready() {
// Node initialization
player1Card = GetNode<Node2D>("Player1Card");
player2Card = GetNode<Node2D>("Player2Card");
Connection.Instance.OnObserveWin += ObserveWin;
Connection.Instance.OnObserveDraw += ObserveDraw;
Connection.Instance.OnObserveTerminated += ObserveTerminated;
Connection.Instance.OnObserveMove += ObserveMove;
Connection.Instance.OnTournamentEnd += ShowTournamentScoreboard;
player1Card.GetNode<Label>("Name").Resized += () =>
setPlayerCardScale((player1Card.GetNode<Label>("Name").GetRect().Size.X + 7) / 16, player1Card);
player2Card.GetNode<Label>("Name").Resized += () =>
setPlayerCardScale((player2Card.GetNode<Label>("Name").GetRect().Size.X + 7) / 16, player2Card);
BackButton.Pressed += () =>
{
TransitionToBracket();
};
redChip = GD.Load<PackedScene>(RED_CHIP_PATH);
ylwChip = GD.Load<PackedScene>(YELLOW_CHIP_PATH);
matchData = Connection.Instance.CurrentObservingMatch;
player1Card.GetNode<Label>("Name").Text = matchData.player1;
player2Card.GetNode<Label>("Name").Text = matchData.player2;
if (Connection.Instance.PreviousMoves.Count == 0) {
player1Card.GetNode<Label>("Status").Show();
player2Card.GetNode<Label>("Status").Hide();
} else if (Connection.Instance.PreviousMoves.Last().Item1 == matchData.player1) {
player1Card.GetNode<Label>("Status").Hide();
player2Card.GetNode<Label>("Status").Show();
} else {
player1Card.GetNode<Label>("Status").Show();
player2Card.GetNode<Label>("Status").Hide();
}
public override void _ExitTree()
{
Connection.Instance.OnObserveWin -= ObserveWin;
Connection.Instance.OnObserveDraw -= ObserveDraw;
Connection.Instance.OnObserveTerminated -= ObserveTerminated;
Connection.Instance.OnObserveMove -= ObserveMove;
Connection.Instance.OnTournamentEnd -= ShowTournamentScoreboard;
Connection.Instance.OnObserveWin += OnObserveWin;
Connection.Instance.OnObserveDraw += OnObserveDraw;
Connection.Instance.OnObserveTerminated += OnObserveTerminated;
Connection.Instance.OnObserveMove += OnObserveMove;
}
public override void _Process(double delta) {
if (Connection.Instance.PreviousMoves.Count != 0 && currentTimeout <= 0.0f) {
var move = Connection.Instance.PreviousMoves[0];
Connection.Instance.PreviousMoves.RemoveAt(0);
if (move.Item1 == matchData.player1) {
spawnRed(move.Item2);
} else {
spawnYellow(move.Item2);
}
currentTimeout = MOVE_TIMEOUT_BEFORE_PLACE;
} else if (currentTimeout >= 0.0f) {
currentTimeout -= delta;
}
private void ObserveMove(string username, int column)
{
if (username == matchData.player1)
{
spawnRed(column);
}
else
{
spawnYellow(column);
}
if (_lastMove) {
_lastMoveTimer -= (float)delta;
}
private void ObserveWin(string winner)
{
var popup = new Popup();
popup.AlwaysOnTop = true;
popup.PopupCentered();
popup.Size = new Vector2I(128, 128);
var text = new Label();
text.Text = winner + " wins!";
popup.AddChild(text);
GetTree().Root.AddChild(popup);
TransitionToBracket();
if (_lastMoveTimer <= 0.0f) {
if (_winner == "") {
showPopupMessage("Draw!");
} else {
showPopupMessage(_winner + " wins!");
}
}
private void ObserveDraw()
{
var popup = new Popup();
popup.AlwaysOnTop = true;
popup.PopupCentered();
popup.Size = new Vector2I(128, 128);
var text = new Label();
text.Text = "Draw!";
popup.AddChild(text);
GetTree().Root.AddChild(popup);
TransitionToBracket();
}
private void ObserveTerminated()
{
var popup = new Popup();
popup.AlwaysOnTop = true;
popup.PopupCentered();
popup.Size = new Vector2I(128, 128);
var text = new Label();
text.Text = "Match Terminated";
popup.AddChild(text);
GetTree().Root.AddChild(popup);
TransitionToBracket();
}
private void ShowTournamentScoreboard(List<(string, int)> playerScoreboard)
{
var scoreboardWindow = new Window();
scoreboardWindow.AlwaysOnTop = true;
scoreboardWindow.MaximizeDisabled = true;
scoreboardWindow.Unresizable = true;
scoreboardWindow.InitialPosition = Window.WindowInitialPosition.CenterMainWindowScreen;
scoreboardWindow.Size = new Vector2I(256, 512);
scoreboardWindow.CloseRequested += () =>
{
GetTree().Root.RemoveChild(scoreboardWindow);
};
var tree = new Tree();
tree.HideRoot = true;
tree.Columns = 2;
tree.ColumnTitlesVisible = true;
tree.SetColumnTitle(0, "Player");
tree.SetColumnTitle(1, "Score");
var root = tree.CreateItem();
foreach ((string, int) entry in playerScoreboard)
{
var item = tree.CreateItem(root);
item.SetText(0, entry.Item1);
item.SetText(1, entry.Item2.ToString());
}
scoreboardWindow.AddChild(tree);
GetTree().Root.AddChild(scoreboardWindow);
}
private void TransitionToBracket()
{
GetTree().ChangeSceneToFile(BRACKET_SCENE_PATH);
}
public override void _ExitTree() {
Connection.Instance.OnObserveWin -= OnObserveWin;
Connection.Instance.OnObserveDraw -= OnObserveDraw;
Connection.Instance.OnObserveTerminated -= OnObserveTerminated;
Connection.Instance.OnObserveMove -= OnObserveMove;
}
private void OnObserveWin(string winner) {
_lastMove = true;
_winner = winner;
player1Card.GetNode<Label>("Status").Hide();
player2Card.GetNode<Label>("Status").Hide();
}
private void OnObserveDraw() {
_lastMove = true;
player1Card.GetNode<Label>("Status").Hide();
player2Card.GetNode<Label>("Status").Hide();
}
private void OnObserveTerminated() {
showPopupMessage("Match Terminated");
player1Card.GetNode<Label>("Status").Hide();
player2Card.GetNode<Label>("Status").Hide();
}
private void OnObserveMove(string username, int column) {
if (username == matchData.player1) {
if (!_lastMove)
player2Card.GetNode<Label>("Status").Show();
player1Card.GetNode<Label>("Status").Hide();
} else {
if (!_lastMove)
player1Card.GetNode<Label>("Status").Show();
player2Card.GetNode<Label>("Status").Hide();
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta) {
/*
if(p1 != null) {
Label username = player1Card.GetNode<Label>("Name");
username.Text = p1.username;
if(p1.isReady) {
Label status = player1Card.GetNode<Label>("Status");
status.Text = "Ready!";
status.AddThemeColorOverride("font_color", Colors.LimeGreen);
}
}
if(p2 != null) {
Label username = player2Card.GetNode<Label>("Name");
username.Text = p2.username;
if(p2.isReady) {
Label status = player2Card.GetNode<Label>("Status");
status.Text = "Ready!";
status.AddThemeColorOverride("font_color", Colors.LimeGreen);
}
}
*/
Connection.Instance.PreviousMoves.Add((username, column));
}
private void showPopupMessage(string message) {
var popup = new Popup();
popup.AlwaysOnTop = true;
popup.Size = new Vector2I(200, 100);
popup.Theme = GD.Load<Theme>("res://assets/theme.tres");
var text = new Label();
text.Text = message;
var sfx = new AudioStreamPlayer();
sfx.Stream = endingSfx;
sfx.VolumeDb = -2;
popup.AddChild(sfx);
popup.AddChild(text);
text.GrowHorizontal = Control.GrowDirection.Both;
text.GrowVertical = Control.GrowDirection.Both;
text.HorizontalAlignment = HorizontalAlignment.Center;
text.VerticalAlignment = VerticalAlignment.Center;
text.AnchorsPreset = (int)Control.LayoutPreset.FullRect;
popup.InitialPosition = Window.WindowInitialPosition.CenterMainWindowScreen;
popup.PopupHide += () => popup.QueueFree();
GetTree().Root.AddChild(popup);
popup.PopupCentered();
sfx.Play();
popup.Show();
transitionToBracket();
}
private void transitionToBracket() { GetTree().ChangeSceneToFile(BRACKET_SCENE_PATH); }
/*
* Determines if the column can have a new chip placed
* Will return -1 if invalid
* or
* Will return row of board in which chip can be placed
*/
private int canPlaceOnCol(int col) {
if (col < 0 || col > 6) // Col out of range
return -1;
return getNextAvailRow(col);
}
private int getNextAvailRow(int col) {
for (int i = chips.GetLength(0) - 1; i >= 0; i--) {
// Start at bottom
if (chips[i, col] == null)
return i;
}
/*
* Determines if the column can have a new chip placed
* Will return -1 if invalid
* or
* Will return row of board in which chip can be placed
*/
public int canPlaceOnCol(int col) {
if(col < 0 || col > 6) // Col out of range
return -1;
return -1;
}
return getNextAvailRow(col);
private void spawnRed(int col) {
int row = canPlaceOnCol(col);
if (row == -1) {
GD.Print("Invalid Placement!");
return;
}
public int getNextAvailRow(int col) {
for(int i = chips.GetLength(0) - 1; i >= 0; i--) { // Start at bottom
if(chips[i, col] == null)
return i;
}
RigidBody2D newNode = redChip.Instantiate<RigidBody2D>();
AddChild(newNode);
newNode.Position = new Vector2(CHIP_SCALE * (CHIP_X_OFF + (CHIP_SIZE + CHIP_PADDING) * col),
-(CHIP_SIZE + CHIP_PADDING) * 7);
return -1;
chips[row, col] = newNode;
}
private void spawnYellow(int col) {
int row = canPlaceOnCol(col);
if (row == -1) {
GD.Print("Invalid Placement!");
return;
}
private void spawnRed(int col) {
int row = canPlaceOnCol(col);
if(row == -1) {
GD.Print("Invalid Placement!");
return;
}
RigidBody2D newNode = ylwChip.Instantiate<RigidBody2D>();
AddChild(newNode);
newNode.Position = new Vector2(CHIP_SCALE * (CHIP_X_OFF + (CHIP_SIZE + CHIP_PADDING) * col),
-(CHIP_SIZE + CHIP_PADDING) * 7);
RigidBody2D newNode = redChip.Instantiate<RigidBody2D>();
AddChild(newNode);
newNode.Position = new Vector2(CHIP_SCALE * (CHIP_X_OFF + (CHIP_SIZE + CHIP_PADDING) * col), -(CHIP_SIZE + CHIP_PADDING) * 7);
chips[row, col] = newNode;
}
chips[row, col] = newNode;
}
private void setPlayerCardScale(float x, Node2D playerCard) {
Sprite2D cardCenter = playerCard.GetNode<Sprite2D>("Center");
Sprite2D cardRight = playerCard.GetNode<Sprite2D>("Right");
private void spawnYellow(int col) {
int row = canPlaceOnCol(col);
if(row == -1) {
GD.Print("Invalid Placement!");
return;
}
float offX = 16 * x / 2;
cardCenter.Scale = new Vector2(x, 2);
cardCenter.Position = new Vector2(CARD_CENTER_X_DEFAULT + offX, cardCenter.Position.Y);
RigidBody2D newNode = ylwChip.Instantiate<RigidBody2D>();
AddChild(newNode);
newNode.Position = new Vector2(CHIP_SCALE * (CHIP_X_OFF + (CHIP_SIZE + CHIP_PADDING) * col), -(CHIP_SIZE + CHIP_PADDING) * 7);
chips[row, col] = newNode;
}
private void setPlayerCardScale(float x, Node2D playerCard) {
Sprite2D cardCenter = playerCard.GetNode<Sprite2D>("Center");
Sprite2D cardRight = playerCard.GetNode<Sprite2D>("Right");
float offX = 16 * x / 2;
cardCenter.Scale = new Vector2(x, 2);
cardCenter.Position = new Vector2(CARD_CENTER_X_DEFAULT + offX, cardCenter.Position.Y);
cardRight.Position = new Vector2(CARD_CENTER_X_DEFAULT + offX * 2 + 8, cardRight.Position.Y); // 8 is a magic number (im too lazy) for the size of the card edge multipled by 2
}
cardRight.Position =
new Vector2(CARD_CENTER_X_DEFAULT + offX * 2 + 8,
cardRight.Position.Y); // 8 is a magic number (im too lazy) for the size of the card edge multipled by 2
}
}
enum Direction
{
UP,
UP_RIGHT,
RIGHT,
DOWN_RIGHT,
DOWN,
DOWN_LEFT,
LEFT,
UP_LEFT
enum Direction {
UP,
UP_RIGHT,
RIGHT,
DOWN_RIGHT,
DOWN,
DOWN_LEFT,
LEFT,
UP_LEFT
}

View File

@@ -1,95 +1,91 @@
using Godot;
using System;
using System.Collections.Generic;
public partial class BracketScene : Control
{
[Export] public Tree Players;
[Export] public Tree Matches;
[Export] public Texture2D JoinButton;
private const string BOARD_SCENE_PATH = "res://scenes/game.tscn";
private List<PlayerData> _playerList;
private List<MatchData> _matchList;
public override void _Ready()
{
Players.SetColumnTitle(0, "Name");
Players.SetColumnTitle(1, "Ready");
Players.SetColumnTitle(2, "Playing");
Players.HideRoot = true;
Players.ButtonClicked += KickPlayer;
Matches.SetColumnTitle(0, "Match");
Matches.SetColumnTitle(1, "Red");
Matches.SetColumnTitle(2, "Yellow");
Matches.HideRoot = true;
Matches.ButtonClicked += WatchGame;
Connection.Instance.OnUpdatedPlayers += UpdatePlayers;
Connection.Instance.OnUpdatedMatches += UpdateMatches;
Connection.Instance.OnWatchGameAck += TransitionToBoard;
}
public partial class BracketScene : Control {
[Export] public Tree Players;
[Export] public Tree Matches;
[Export] public Texture2D WatchButton;
[Export] public Texture2D TerminateKickButton;
private const string BOARD_SCENE_PATH = "res://scenes/game.tscn";
public override void _ExitTree()
{
Connection.Instance.OnUpdatedPlayers -= UpdatePlayers;
Connection.Instance.OnUpdatedMatches -= UpdateMatches;
Connection.Instance.OnWatchGameAck -= TransitionToBoard;
}
private List<PlayerData> playerList;
private List<MatchData> matchList;
private void UpdatePlayers(List<PlayerData> playerList)
{
Players.Clear();
_playerList = playerList;
var root = Players.CreateItem();
for (int i = 0; i < _playerList.Count; i++)
{
var item = Players.CreateItem(root);
item.SetText(0, playerList[i].username);
item.SetText(1, playerList[i].isReady ? "Yes" : "No");
item.SetText(2, playerList[i].isPlaying ? "Yes" : "No");
if (Connection.Instance.IsAdmin)
{
item.AddButton(0, JoinButton, i, false, "Kick"); // TODO
}
}
}
private void UpdateMatches(List<MatchData> matchList)
{
Matches.Clear();
_matchList = matchList;
var root = Matches.CreateItem();
for (int i = 0; i < matchList.Count; i++)
{
var item = Matches.CreateItem(root);
item.SetText(0, matchList[i].matchId.ToString());
item.SetText(1, matchList[i].player1);
item.SetText(2, matchList[i].player2);
item.AddButton(0, JoinButton, i, false, "Watch");
}
}
public override void _Ready() {
Players.SetColumnTitle(0, "Name");
Players.SetColumnTitle(1, "Ready");
Players.SetColumnTitle(2, "Playing");
Players.HideRoot = true;
Players.ButtonClicked += kickPlayer;
private void WatchGame(TreeItem item, long column, long id, long mouseButtonIndex)
{
if (mouseButtonIndex == 1 && column == 0)
{
Connection.Instance.SendWatchGame(_matchList[(int) id].matchId);
}
}
private void KickPlayer(TreeItem item, long column, long id, long mouseButtonIndex)
{
if (mouseButtonIndex == 1 && column == 0)
{
Connection.Instance.KickPlayer(_playerList[(int) id].username);
}
}
private void TransitionToBoard()
{
GetTree().ChangeSceneToFile(BOARD_SCENE_PATH);
Matches.SetColumnTitle(0, "Match");
Matches.SetColumnTitle(1, "Red");
Matches.SetColumnTitle(2, "Yellow");
Matches.HideRoot = true;
Matches.ButtonClicked += watchGame;
Matches.ButtonClicked += terminateGame;
Connection.Instance.OnUpdatedPlayers += updatePlayers;
Connection.Instance.OnUpdatedMatches += updateMatches;
Connection.Instance.OnWatchGameAck += transitionToBoard;
}
public override void _ExitTree() {
Connection.Instance.OnUpdatedPlayers -= updatePlayers;
Connection.Instance.OnUpdatedMatches -= updateMatches;
Connection.Instance.OnWatchGameAck -= transitionToBoard;
}
private void updatePlayers(List<PlayerData> newPlayerList) {
Players.Clear();
playerList = newPlayerList;
playerList.Sort((a, b) => a.Username.CompareTo(b.Username));
var root = Players.CreateItem();
for (int i = 0; i < playerList.Count; i++) {
var item = Players.CreateItem(root);
item.SetText(0, newPlayerList[i].Username);
item.SetText(1, newPlayerList[i].IsReady ? "Yes" : "No");
item.SetText(2, newPlayerList[i].IsPlaying ? "Yes" : "No");
if (Connection.Instance.IsAdmin) {
item.AddButton(0, TerminateKickButton, i, false, "Kick");
}
}
}
private void updateMatches(List<MatchData> newMatchList) {
Matches.Clear();
matchList = newMatchList;
var root = Matches.CreateItem();
for (int i = 0; i < newMatchList.Count; i++) {
var item = Matches.CreateItem(root);
item.SetText(0, newMatchList[i].matchId.ToString());
item.SetText(1, newMatchList[i].player1);
item.SetText(2, newMatchList[i].player2);
item.AddButton(0, WatchButton, i, false, "Watch");
if (Connection.Instance.IsAdmin) {
item.AddButton(0, TerminateKickButton, 128 + i, false, "Terminate");
}
}
}
private void watchGame(TreeItem item, long column, long id, long mouseButtonIndex) {
if (mouseButtonIndex == 1 && column == 0 && id < 128) {
Connection.Instance.SendWatchGame(matchList[(int)id].matchId);
}
}
private void terminateGame(TreeItem item, long column, long id, long mouseButtonIndex) {
if (mouseButtonIndex == 1 && column == 0 && id - 128 >= 0 && matchList[(int)id - 128] != null) {
Connection.Instance.TerminateGame(matchList[(int)id - 128].matchId);
}
}
private void kickPlayer(TreeItem item, long column, long id, long mouseButtonIndex) {
if (mouseButtonIndex == 1 && column == 0) {
Connection.Instance.KickPlayer(playerList[(int)id].Username);
}
}
private void transitionToBoard() { GetTree().ChangeSceneToFile(BOARD_SCENE_PATH); }
}

View File

@@ -0,0 +1,404 @@
using System;
using System.Numerics;
/// <summary>
/// Estimates Red/Yellow/Draw chances from a Connect 4 board state.
///
/// Implementation notes:
/// - Uses a near-perfect solver core (negamax + alpha-beta + transposition table) on a standard 7x6 bitboard.
/// - Converts the exact (perfect-play) score into a "chess.com-like" practical win% using:
/// (1) a sigmoid mapping of engine score -> win probability,
/// (2) an optional complexity adjustment based on how many moves preserve the best outcome.
/// </summary>
public static class Connect4WinProbability {
public const int Width = 7;
public const int Height = 6;
/// <summary>
/// Cell content.
/// </summary>
public enum Cell {
None = 0,
Red = 1,
Yellow = 2,
}
public readonly record struct Chances(double RedWinChance, double YellowWinChance, double DrawChance) {
public Chances Normalize() {
var sum = RedWinChance + YellowWinChance + DrawChance;
if (sum <= 0) return new Chances(0.5, 0.5, 0.0);
return new Chances(RedWinChance / sum, YellowWinChance / sum, DrawChance / sum);
}
}
/// <summary>
/// Estimate win/draw chances for the given board.
///
/// Expected board shape is [7,6]. The first index is column (0..6) and the second is row (0..5).
/// Row orientation is auto-detected: this method will accept either row=0 bottom or row=0 top,
/// as long as the position is gravity-valid.
///
/// <paramref name="toMove"/> must be Red or Yellow.
/// </summary>
/// <param name="board">2D array [7,6].</param>
/// <param name="toMove">Who is to play next.</param>
/// <param name="nodeBudget">Maximum explored nodes before falling back to the best-so-far estimate.</param>
/// <param name="enableComplexityAdjustment">If true, adjusts probabilities using move uniqueness/fragility.</param>
public static Chances Evaluate(Cell[,] board, Cell toMove, int nodeBudget = 350_000, bool enableComplexityAdjustment = true) {
if (board == null) throw new ArgumentNullException(nameof(board));
if (board.GetLength(0) != Width || board.GetLength(1) != Height)
throw new ArgumentException($"Board must be [{Width},{Height}]", nameof(board));
if (toMove is not Cell.Red and not Cell.Yellow)
throw new ArgumentException("toMove must be Cell.Red or Cell.Yellow", nameof(toMove));
if (!TryParseBoard(board, out var redBits, out var yellowBits)) {
// If the board is invalid, avoid lying with a confident number.
return new Chances(0.45, 0.45, 0.10).Normalize();
}
var mask = redBits | yellowBits;
var nbMoves = BitOperations.PopCount(mask);
// If someone has already won (shouldn't happen in a "next move" position, but observers might see it).
if (HasAlignment(redBits) && HasAlignment(yellowBits)) {
// Illegal: both cannot have 4-in-a-row in a legal game.
return new Chances(0.45, 0.45, 0.10).Normalize();
}
if (HasAlignment(redBits)) return new Chances(1.0, 0.0, 0.0);
if (HasAlignment(yellowBits)) return new Chances(0.0, 1.0, 0.0);
var position = Position.FromBitboards(mask, toMove == Cell.Red ? redBits : yellowBits);
// Solve the exact perfect-play score.
var tt = new TranspositionTable(1 << 20);
var solver = new Solver(tt, nodeBudget);
int bestScore = solver.Negamax(position, alpha: -Position.MaxScore, beta: Position.MaxScore);
// Optional complexity: score all immediate child moves (only up to 7) to see how "fragile" the outcome is.
int legalMoves = 0;
int bestMoves = 0;
int drawingMoves = 0;
if (enableComplexityAdjustment) {
for (int col = 0; col < Width; col++) {
if (!position.CanPlay(col)) continue;
legalMoves++;
var child = position;
child.Play(col);
// Reuse the same TT for speed.
int score = -solver.Negamax(child, alpha: -Position.MaxScore, beta: Position.MaxScore);
if (score == bestScore) bestMoves++;
if (score == 0) drawingMoves++;
}
if (legalMoves == 0) {
// Board full.
return new Chances(0.0, 0.0, 1.0);
}
} else {
for (int col = 0; col < Width; col++) if (position.CanPlay(col)) legalMoves++;
if (legalMoves == 0) return new Chances(0.0, 0.0, 1.0);
bestMoves = Math.Max(1, legalMoves / 2);
drawingMoves = 0;
}
var (pCurrentWin, pDraw) = ScoreToPracticalProbabilities(bestScore, nbMoves, legalMoves, bestMoves, drawingMoves);
var pCurrentLoss = Math.Max(0.0, 1.0 - pDraw - pCurrentWin);
// Map from current-player POV to Red/Yellow.
Chances result = toMove == Cell.Red
? new Chances(pCurrentWin, pCurrentLoss, pDraw)
: new Chances(pCurrentLoss, pCurrentWin, pDraw);
return result.Normalize();
}
private static (double pCurrentWin, double pDraw) ScoreToPracticalProbabilities(
int score,
int nbMoves,
int legalMoves,
int bestMoves,
int drawingMoves
) {
// Normalize score by the maximum possible magnitude at this ply.
// The classic perfect-solver scoring is within [-21, 21] on a 7x6 board.
var maxAtPly = Math.Max(1, (Width * Height + 1 - nbMoves) / 2); // similar to gamesolver.org tutorial scoring
double s = Math.Clamp(score / (double)maxAtPly, -1.0, 1.0);
// Base win probability ignoring draws: a sigmoid curve similar in spirit to chess eval->win% mappings.
const double sigmoidScale = 3.0;
double pWinNoDraw = Sigmoid(s * sigmoidScale);
// Complexity/fragility: if only a few moves preserve the best outcome, the practical win% should be less extreme.
// complexity = 0 means many best moves (easy), 1 means only one best move (fragile).
double complexity = 1.0;
if (legalMoves > 0) {
complexity = 1.0 - Math.Clamp(bestMoves / (double)legalMoves, 0.0, 1.0);
}
// Blend toward 50% based on complexity.
// (If there are many good moves, keep the evaluation confident; if there's only one, flatten it.)
double flatten = 0.60 * complexity;
pWinNoDraw = Lerp(pWinNoDraw, 0.5, flatten);
// Draw propensity.
// - If perfect play draws (score == 0), put a significant mass on draw, more so if many moves keep it drawn.
// - If perfect play is decisive, keep draw small but non-zero (practical mistakes can still drift to a draw).
double drawMoveRatio = legalMoves > 0 ? (drawingMoves / (double)legalMoves) : 0.0;
double pDraw;
if (score == 0) {
pDraw = 0.55 + 0.35 * drawMoveRatio; // 0.55..0.90
// If draw is very "fragile" (few drawing moves), reduce draw slightly.
pDraw -= 0.10 * complexity;
} else {
// Keep it small, but let it rise a bit for positions where many moves still lead to a theoretical draw.
pDraw = 0.02 + 0.10 * drawMoveRatio;
// If the position is very complex, increase draw slightly (practical play drifts).
pDraw += 0.03 * complexity;
}
pDraw = Math.Clamp(pDraw, 0.0, 0.90);
// Combine.
double pWin = (1.0 - pDraw) * pWinNoDraw;
pWin = Math.Clamp(pWin, 0.0, 1.0 - pDraw);
return (pWin, pDraw);
}
private static double Sigmoid(double x) => 1.0 / (1.0 + Math.Exp(-x));
private static double Lerp(double a, double b, double t) => a + (b - a) * Math.Clamp(t, 0.0, 1.0);
private static bool TryParseBoard(Cell[,] board, out ulong redBits, out ulong yellowBits) {
// We accept either row=0 bottom OR row=0 top as long as it is gravity-valid.
// We try both and select the first valid representation.
if (TryParseBoard(board, row0IsBottom: true, out redBits, out yellowBits)) return true;
if (TryParseBoard(board, row0IsBottom: false, out redBits, out yellowBits)) return true;
redBits = 0;
yellowBits = 0;
return false;
}
private static bool TryParseBoard(Cell[,] board, bool row0IsBottom, out ulong redBits, out ulong yellowBits) {
redBits = 0;
yellowBits = 0;
for (int col = 0; col < Width; col++) {
bool seenEmptyBelow = false;
for (int rowIdx = 0; rowIdx < Height; rowIdx++) {
int row = row0IsBottom ? rowIdx : (Height - 1 - rowIdx);
var cell = board[col, row];
if (cell == Cell.None) {
seenEmptyBelow = true;
continue;
}
if (seenEmptyBelow) {
// A disc is "floating" above an empty cell in this interpretation.
return false;
}
int bitRow = rowIdx; // bottom=0
ulong bit = 1UL << (col * (Height + 1) + bitRow);
if (cell == Cell.Red) redBits |= bit;
else if (cell == Cell.Yellow) yellowBits |= bit;
else return false;
}
}
// Additional sanity: overlap check.
return (redBits & yellowBits) == 0;
}
private static bool HasAlignment(ulong pos) {
// Checks 4-in-a-row for a bitboard with (Height+1)=7 stride per column.
// Shifts correspond to:
// - 1: vertical
// - (Height+1): horizontal
// - (Height+1)+1: diagonal /
// - (Height+1)-1: diagonal \
int h1 = Height + 1;
// vertical
if (HasFour(pos, 1)) return true;
// horizontal
if (HasFour(pos, h1)) return true;
// diag / (up-right)
if (HasFour(pos, h1 + 1)) return true;
// diag \ (down-right)
if (HasFour(pos, h1 - 1)) return true;
return false;
}
private static bool HasFour(ulong pos, int shift) {
ulong m = pos & (pos >> shift);
return (m & (m >> (2 * shift))) != 0;
}
private struct Position {
// Bitboard representation (Pascal Pons / Tromp style):
// - mask: all occupied cells
// - current: stones of the player to move
public ulong Mask;
public ulong Current;
public const int MaxScore = (Width * Height + 1) / 2; // 21
private static readonly ulong[] BottomMask = new ulong[Width];
private static readonly ulong[] TopMask = new ulong[Width];
private static readonly ulong[] ColumnMask = new ulong[Width];
private static readonly ulong BoardMask;
static Position() {
for (int c = 0; c < Width; c++) {
BottomMask[c] = 1UL << (c * (Height + 1));
TopMask[c] = 1UL << (c * (Height + 1) + (Height - 1));
ulong colMask = 0;
for (int r = 0; r < Height; r++) colMask |= 1UL << (c * (Height + 1) + r);
ColumnMask[c] = colMask;
}
ulong bm = 0;
for (int c = 0; c < Width; c++) bm |= ColumnMask[c];
BoardMask = bm;
}
public static Position FromBitboards(ulong mask, ulong currentToMoveBits) {
return new Position { Mask = mask, Current = currentToMoveBits };
}
public int NbMoves() => BitOperations.PopCount(Mask);
public bool CanPlay(int col) {
if ((uint)col >= Width) return false;
return (Mask & TopMask[col]) == 0;
}
public void Play(int col) {
// Switch side-to-move by XOR with mask (classic trick).
Current ^= Mask;
// Drop a disc into the given column.
Mask |= Mask + BottomMask[col];
// Ensure Mask only contains board cells.
Mask &= BoardMask;
}
public bool IsWinningMove(int col) {
// Compute the position of the current player AFTER playing this column.
ulong pos = Current;
ulong m = Mask;
// play into column: get the bit for the new disc
ulong newMask = (m | (m + BottomMask[col])) & BoardMask;
ulong moveBit = newMask ^ m;
pos |= moveBit;
return HasAlignment(pos);
}
public ulong Key() {
// Mix mask + current into a stable 64-bit key.
// Good enough for a fixed-size transposition table.
unchecked {
ulong x = Mask * 6364136223846793005UL + 1442695040888963407UL;
return x ^ (Current * 11400714819323198485UL);
}
}
}
private sealed class TranspositionTable {
private struct Entry {
public ulong Key;
public sbyte Value;
public byte Used;
}
private readonly Entry[] _entries;
private readonly int _mask;
public TranspositionTable(int sizePowerOfTwo) {
if (sizePowerOfTwo <= 0 || (sizePowerOfTwo & (sizePowerOfTwo - 1)) != 0)
throw new ArgumentException("TT size must be a power of two", nameof(sizePowerOfTwo));
_entries = new Entry[sizePowerOfTwo];
_mask = sizePowerOfTwo - 1;
}
public bool TryGet(ulong key, out int value) {
ref var e = ref _entries[(int)key & _mask];
if (e.Used != 0 && e.Key == key) {
value = e.Value;
return true;
}
value = 0;
return false;
}
public void Put(ulong key, int value) {
ref var e = ref _entries[(int)key & _mask];
e.Key = key;
e.Value = (sbyte)Math.Clamp(value, -127, 127);
e.Used = 1;
}
}
private sealed class Solver {
private readonly TranspositionTable _tt;
private readonly int _nodeBudget;
private int _nodes;
public Solver(TranspositionTable tt, int nodeBudget) {
_tt = tt;
_nodeBudget = Math.Max(10_000, nodeBudget);
_nodes = 0;
}
public int Negamax(Position p, int alpha, int beta) {
// Budget guard: if we run out, return a conservative estimate.
if (_nodes++ > _nodeBudget) return 0;
int moves = p.NbMoves();
if (moves >= Width * Height) return 0; // draw by full board
// Tight theoretical bounds for this ply (helps alpha-beta).
int max = (Width * Height + 1 - moves) / 2;
int min = -(Width * Height - moves) / 2;
if (alpha < min) alpha = min;
if (beta > max) beta = max;
if (alpha >= beta) return alpha;
// Immediate win check.
for (int col = 0; col < Width; col++) {
if (!p.CanPlay(col)) continue;
if (p.IsWinningMove(col)) return max;
}
ulong key = p.Key();
if (_tt.TryGet(key, out int cached)) return cached;
int best = min;
// Center-first move ordering (classic Connect 4 heuristic).
// Order: 3,4,2,5,1,6,0
Span<int> order = stackalloc int[Width] { 3, 4, 2, 5, 1, 6, 0 };
for (int i = 0; i < order.Length; i++) {
int col = order[i];
if (!p.CanPlay(col)) continue;
var child = p;
child.Play(col);
int score = -Negamax(child, -beta, -alpha);
if (score > best) best = score;
if (score > alpha) alpha = score;
if (alpha >= beta) break;
}
_tt.Put(key, best);
return best;
}
}
}

View File

@@ -1,23 +1,47 @@
using Godot;
using System;
public partial class ConnectButtonUI : Button
{
[Export] public TextEdit AddressUi;
[Export] public Label ErrorLabel;
private const string BRACKET_SCENE_PATH = "res://scenes/bracket_view.tscn";
public override void _Pressed()
{
if (Connection.Instance.Connect(AddressUi.Text))
{
GD.Print("Success!");
GetTree().ChangeSceneToFile(BRACKET_SCENE_PATH);
}
else
{
ErrorLabel.Text = "Couldn't connect to server!";
}
base._Pressed();
public partial class ConnectButtonUI : Button {
[Export] public TextEdit AddressField;
[Export] public Label ErrorLabel;
private const string BRACKET_SCENE_PATH = "res://scenes/bracket_view.tscn";
public override void _Ready() {
Connection.Instance.OnWsConnectionSuccess += OnConnectionSuccess;
Connection.Instance.OnWsConnectionFailed += OnConnectionFailed;
if (Connection.Instance.LastUsedConnectionAddress.Length > 0) {
AddressField.Text = Connection.Instance.LastUsedConnectionAddress;
}
if (Connection.Instance.LastError.Length > 0) {
ErrorLabel.Text = Connection.Instance.LastError;
}
AddressField.GuiInput += e =>
{
if (AddressField.HasFocus() && e is InputEventKey inputEventKey && inputEventKey.IsPressed()) {
if (inputEventKey.KeyLabel == Key.Enter) {
Connection.Instance.Connect(AddressField.Text);
GetViewport().SetInputAsHandled();
}
if (inputEventKey.KeyLabel == Key.Space) {
GetViewport().SetInputAsHandled();
}
}
};
}
public override void _ExitTree() {
Connection.Instance.OnWsConnectionSuccess -= OnConnectionSuccess;
Connection.Instance.OnWsConnectionFailed -= OnConnectionFailed;
}
public override void _Pressed() { Connection.Instance.Connect(AddressField.Text); }
private void OnConnectionSuccess() { GetTree().ChangeSceneToFile(BRACKET_SCENE_PATH); }
private void OnConnectionFailed() {
ErrorLabel.Text = "Couldn't connect to server! " + Connection.Instance.LastError;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,11 @@
public class MatchData
{
public int matchId { get; private set; }
public string player1 { get; private set; }
public string player2 { get; private set; }
public class MatchData {
public int matchId { get; private set; }
public string player1 { get; private set; }
public string player2 { get; private set; }
public MatchData(int matchId, string player1, string player2)
{
this.matchId = matchId;
this.player1 = player1;
this.player2 = player2;
}
}
public MatchData(int matchId, string player1, string player2) {
this.matchId = matchId;
this.player1 = player1;
this.player2 = player2;
}
}

View File

@@ -1,13 +1,11 @@
public class PlayerData
{
public string username { get; private set; }
public bool isReady { get; private set; }
public bool isPlaying { get; private set; }
public class PlayerData {
public string Username { get; private set; }
public bool IsReady { get; private set; }
public bool IsPlaying { get; private set; }
public PlayerData(string username, bool isReady, bool isPlaying)
{
this.username = username;
this.isReady = isReady;
this.isPlaying = isPlaying;
}
public PlayerData(string username, bool isReady, bool isPlaying) {
Username = username;
IsReady = isReady;
IsPlaying = isPlaying;
}
}

View File

@@ -0,0 +1,4 @@
public enum TournamentType {
None,
RoundRobin
}

View File

@@ -0,0 +1 @@
uid://dwb6ioubllb24

View File

@@ -0,0 +1,8 @@
extends Node
func _ready() -> void:
var music = AudioStreamPlayer.new()
add_child(music)
music.stream = load("res://assets/music/jazz_music.mp3")
music.volume_db = -10
music.play()

View File

@@ -0,0 +1 @@
uid://hh10ct26xx1b

13
scripts/chip_sfx.gd Normal file
View File

@@ -0,0 +1,13 @@
extends RigidBody2D
@export var audio_stream_player_2d: AudioStreamPlayer2D
@export var velocity_for_sfx: float = 2.0
@export var sounds: Array[AudioStream]
func _on_body_entered(body: Node) -> void:
if body.name.begins_with("@RigidBody2D@") || body.name.begins_with("Floor"):
var rng = RandomNumberGenerator.new()
rng.randomize()
var index = rng.randi_range(0, sounds.size() - 1)
audio_stream_player_2d.stream = sounds[index]
audio_stream_player_2d.play();

View File

@@ -10,5 +10,5 @@ func _ready() -> void:
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
func _process(_delta: float) -> void:
pass

View File

@@ -1,12 +0,0 @@
extends RigidBody2D
var color_ = "red";
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta: float) -> void:
pass

10
scripts/thinking.gd Normal file
View File

@@ -0,0 +1,10 @@
extends Label
@export var hue = 0.0
func _process(delta):
modulate = Color.from_hsv(hue, 1.0, 1.0, 1.0)
if hue < 1.0:
hue += 0.1 * delta
else:
hue = 0.0

1
scripts/thinking.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://cwfg17tdbk44b

View File

@@ -1,12 +0,0 @@
extends RigidBody2D
var color_ = "yellow";
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta: float) -> void:
pass

View File

@@ -1 +0,0 @@
uid://ddg7dv686sbrb