diff --git a/projects/.DS_Store b/projects/.DS_Store deleted file mode 100644 index f243165f73d04cdc05874414cd3f5f0675611e41..0000000000000000000000000000000000000000 Binary files a/projects/.DS_Store and /dev/null differ diff --git a/projects/2026project_class/.DS_Store b/projects/2026project_class/.DS_Store deleted file mode 100644 index d13b3e883e97ec8bedb0ddd7a7fd1b0a1c6d4f7a..0000000000000000000000000000000000000000 Binary files a/projects/2026project_class/.DS_Store and /dev/null differ diff --git a/projects/2026project_class/examples/.DS_Store b/projects/2026project_class/examples/.DS_Store deleted file mode 100644 index 4d5e1937af5d5001facc541d87b8f16507870bca..0000000000000000000000000000000000000000 Binary files a/projects/2026project_class/examples/.DS_Store and /dev/null differ diff --git "a/projects/DEMO/Student1/MakeCode \350\267\263\350\210\236\346\251\237/link.txt" "b/projects/DEMO/Student1/MakeCode \350\267\263\350\210\236\346\251\237/link.txt" new file mode 100644 index 0000000000000000000000000000000000000000..41a1c5df53e26fe11e4081446f1f952bcd54dcce --- /dev/null +++ "b/projects/DEMO/Student1/MakeCode \350\267\263\350\210\236\346\251\237/link.txt" @@ -0,0 +1 @@ +https://makecode.com/_fcva84c8D5jD diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/ball_blue.png b/projects/DEMO/Student1/Pygame_Breakout/assets/ball_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..23957f2d983a31a9b1449ba3d80557273554442a --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/ball_blue.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:386c1d802b999b44dc23f7b664c5a83d9145d2a1998e10af4e1b5d1c573aff11 +size 13315 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_blue.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..e7cf058c84193e8346bc5b3323bfedc351981334 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_blue.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be442525f1668f389cb2cb0872cb57bf1421c2576fbafba38df8f1b6ae8f88b4 +size 1825 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_blue_cracked.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_blue_cracked.png new file mode 100644 index 0000000000000000000000000000000000000000..8465fc0fd7d762a92ca27e9d92bec959d65e8c7d --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_blue_cracked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb223c990af9fcc1377176a9f5323cadca54583253655d921b3ce9ddf6e4875c +size 10883 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_brown.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_brown.png new file mode 100644 index 0000000000000000000000000000000000000000..91262e5ce8613f9b2e23e4e2dc40ec238903a96a --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_brown.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37b33819b944c72ef1d0fda730969d98df370305d06a734cee76adaccb848000 +size 1880 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_brown_cracked.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_brown_cracked.png new file mode 100644 index 0000000000000000000000000000000000000000..d9350114850e45a846f8732fc850f47f42f22086 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_brown_cracked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a92fa6903ae9c9bc9d4a9068e79dd0f6756c853c4409eabbe22f30115f4ccb9b +size 12296 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_cyan.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_cyan.png new file mode 100644 index 0000000000000000000000000000000000000000..9d6565757556fd2761cc09142adbf074b4691cce --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_cyan.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76324575e57f6e7e1d68e553ac54707e2c447ad7860f11360d6159955e898145 +size 1861 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_cyan_cracked.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_cyan_cracked.png new file mode 100644 index 0000000000000000000000000000000000000000..66498291086098361fae0548e3fca553717e9485 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_cyan_cracked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1213b25e949635bd84143144c1a9c6ae7832db6864a191ad39ff32428bdf694 +size 13400 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_dark_green.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_dark_green.png new file mode 100644 index 0000000000000000000000000000000000000000..dc3e952d5e79bb31e515b91f9a7e04023d6dfa2e --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_dark_green.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea7e7b40d46961684f556d6aaa05ecc4691bfd01562f3ba892072339f6514c24 +size 1826 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_dark_green_cracked.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_dark_green_cracked.png new file mode 100644 index 0000000000000000000000000000000000000000..9adb9a26ab50146776a99ad5117692795981c68d --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_dark_green_cracked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c098a90154bc12dd1c0337b815caa88cb27b4d4afbc51b416136a23d8755a16e +size 11172 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_green.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_green.png new file mode 100644 index 0000000000000000000000000000000000000000..eac56361863a15a51f439c8a5e445bc52eae3b9b --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_green.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ffad84b56cb3813a745080e477c2aff83e1f074ce2674b2a7a184543aa79c3b +size 1815 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_green_cracked.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_green_cracked.png new file mode 100644 index 0000000000000000000000000000000000000000..94cf3fbb6fabe7befd0ca434f3c3ab94a9744942 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_green_cracked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6d0bf4bcf004396af553c05812760e49b97ccefc4343eb8e9208c0a99a83a2e +size 14389 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_grey.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_grey.png new file mode 100644 index 0000000000000000000000000000000000000000..f9245161100038a3194811908cf2da2352a6a4d9 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_grey.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ad09acc38d4707113e49f995c98ece908698ec052b178b9cfdaa609f1b9d128 +size 1808 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_grey_cracked.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_grey_cracked.png new file mode 100644 index 0000000000000000000000000000000000000000..a14bc967f8d2dd767108a8f90feb8015d50278a1 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_grey_cracked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9c57fe577f1445fc777fc7957a26186e1e00d1a8ab9f3cb8d63ce4324c6c079 +size 12854 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_orange.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_orange.png new file mode 100644 index 0000000000000000000000000000000000000000..3d32611a99dbab8eaf9ec96ddc5c066ae2a92a8e --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_orange.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5b5fffa1ca91a69297d1c7f95ee5420f13cd63a6b6ad583bf750c0bac0b7679 +size 1821 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_orange_cracked.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_orange_cracked.png new file mode 100644 index 0000000000000000000000000000000000000000..ce8ecde886f684d28da817a171cf2b18a2bb490d --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_orange_cracked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f6bf0a75c0bd738c2c9d7ccab65d937d318daf2ab6968cff5bd9427b6c282bd +size 13354 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_purple.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_purple.png new file mode 100644 index 0000000000000000000000000000000000000000..104cad59e249fb59eb78b21f88de79e1571d5331 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_purple.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4dcab1021c0deb727497d093e60a3dedbdb998c39c42d01299c61ed8cbdd93c9 +size 1831 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_purple_cracked.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_purple_cracked.png new file mode 100644 index 0000000000000000000000000000000000000000..ea9c0a72054955b046351063f93debce226facc5 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_purple_cracked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a9f450218acc6dc6ae8f87a3a0259ae3c3ac621b4035f06ec36162921ece8bc +size 13241 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_red.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_red.png new file mode 100644 index 0000000000000000000000000000000000000000..1fc1272eb0c870df08c8440d03e2752599c8718b --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_red.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:400de5aa0ae64094f97abada5b1f4864d15c2a8757a34d7fceaa9d0a1ea7e5e9 +size 1876 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_red_cracked.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_red_cracked.png new file mode 100644 index 0000000000000000000000000000000000000000..afa0ea37458bf599da74b48d4b8710a29e9e3e2f --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_red_cracked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acbc7771e1827b55bf468ab8fe8125ea86952aae8e6106ab02435e17837c8841 +size 13259 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_yellow.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..71baf9290dfbd4417cec082187d544ce4dd98360 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_yellow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fa521d0403f8b02699a9929ae1a252b5ea02f863865a1900c634c6d091da6ba +size 1822 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/brick_yellow_cracked.png b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_yellow_cracked.png new file mode 100644 index 0000000000000000000000000000000000000000..c282bbbc3ccf55a3515fa7627b91f5014ac1f934 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/brick_yellow_cracked.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06b67fd88f869cbb84bd0fa032ef4d594788ab9cac51925a72b987cb3ca6f937 +size 13125 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/bullet.png b/projects/DEMO/Student1/Pygame_Breakout/assets/bullet.png new file mode 100644 index 0000000000000000000000000000000000000000..74390ff90179eb84563d801306e502182c24d43b --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/bullet.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e47b0124d969d6f91c87355cce1062918055a1cad37dd4f6b7be4edac75c413 +size 4039 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/heart.png b/projects/DEMO/Student1/Pygame_Breakout/assets/heart.png new file mode 100644 index 0000000000000000000000000000000000000000..76111843e8a69842163c48e5778577f89a86c6fe --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/heart.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9ec55cc56ed5e734f995023093d43e89ed5dd6b22b560bd216bb6702c7558ac +size 8196 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_1.png b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_1.png new file mode 100644 index 0000000000000000000000000000000000000000..3989b6f2cd952c80ad8652dd3e61706e35a48b9d --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:311a19275961fb10ae1c39b88190a74ef6ff30fc0be0fe949b767eb9a21a44b5 +size 12455 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_2.png b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_2.png new file mode 100644 index 0000000000000000000000000000000000000000..7be27b5d7e4c88606b98ff637f1f6f889433be60 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d28181d820ee9de7da61da52dd191bf8f8a273128cba11acea367921486eb8d +size 12510 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_3.png b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_3.png new file mode 100644 index 0000000000000000000000000000000000000000..91e4792a28d557f520a7084387ba7728fcc63a83 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48cad734d91773b0863c647061cbe9e40adcd5707383dd98641f93d2fe296620 +size 12870 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_gun_1.png b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_gun_1.png new file mode 100644 index 0000000000000000000000000000000000000000..dd2cd6ffe3b0422f527cf9510536661c021da6fc --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_gun_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:320b963be985885b2d1696bfe0a26528b1263633731fadb249ac99e67cf19886 +size 12557 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_gun_2.png b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_gun_2.png new file mode 100644 index 0000000000000000000000000000000000000000..fd9e2964ceaf7a50ffbd65d84b22dbdd57888f64 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_gun_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:551a9b4913d7b46732e1eb99fcd2ab10ce67d663bcd764e16f44950f947d7752 +size 13171 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_gun_3.png b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_gun_3.png new file mode 100644 index 0000000000000000000000000000000000000000..21faa7652f3b27d406f3a3e724e2fb786b7edfb7 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_gun_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:038b48138609798647351d97afd45a3ea1a793258928a8d8763ceab1e8fc2b34 +size 12620 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_long.png b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_long.png new file mode 100644 index 0000000000000000000000000000000000000000..8a432ce8c4d6aaad1acaaf90f4f8ca8da531b82e --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/paddle_long.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5bf31101b7fa6ee1b4105e40a4cb3dce2c893ee01e8b5bc6bfa5010f3d30e50 +size 14830 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_acid_ball.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_acid_ball.png new file mode 100644 index 0000000000000000000000000000000000000000..b468f4d7b7575fe2737ebbbf2bb3270179ccbaee --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_acid_ball.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53d6e9098f25894ce90a67f6d42bb229721bda6985600e532faafac34a657a75 +size 14763 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_capsule_empty.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_capsule_empty.png new file mode 100644 index 0000000000000000000000000000000000000000..f1008e2fdba110fa708c0f0dada0e298b39ef9a4 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_capsule_empty.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99ab17ab5acf4e015993c7b2ceb46065e65c674a12830ab4e6d9385eabb26aab +size 9074 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_expand.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_expand.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea33843fbb59f098536fd2762caa097114b131d --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_expand.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5fba1431ce34e2aca0c354a9b41049aed8ce61adb5ecdbf65b6558b1bc5af9b +size 10453 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_fast.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_fast.png new file mode 100644 index 0000000000000000000000000000000000000000..e42a97fc64b7ba673aff8937e4d97e2bdab55dc5 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_fast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a71f41b8df56be1bd35892ea6b2d5a755705c4b8b816f8804809b2f205e5ee9 +size 11786 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_fireball.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_fireball.png new file mode 100644 index 0000000000000000000000000000000000000000..3652cd5b85a0913a9c9c462a2e97e21e8e5862af --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_fireball.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a3da7acab4203acbb32a9aa6a41898e06387e1b3749e9202058f98416a9affe +size 14048 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_laser.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_laser.png new file mode 100644 index 0000000000000000000000000000000000000000..7270f9da1615905f04e116ad1a350b223e61bce0 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_laser.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eae6cb052fc26d5732d967359e9d1671a27d9c6ffd6878c1db6903956e914269 +size 9939 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_multi_ball.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_multi_ball.png new file mode 100644 index 0000000000000000000000000000000000000000..e156fda53bc2bdc9c2ba3668140fbca36bb5ecf5 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_multi_ball.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fea5e16ed04fb7fa1094d6f829518d64d9f2f38a046fa09f3c25000e377f2d4 +size 12686 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_100.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_100.png new file mode 100644 index 0000000000000000000000000000000000000000..2feac262aa034a5e98c4a59e437eaffc27cb14c4 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff7dd9a00be46b21ca9c99227511515e7d51ec67a3a6c0b21131bab6c2ab854c +size 12242 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_250.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_250.png new file mode 100644 index 0000000000000000000000000000000000000000..2296b81e8b453a35c7c5829e2a2d850fea9e7c6d --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_250.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11828eccc18039a380a3d3fe6b8f6479a5cccd7f7b06fb72257a1caeefc0a587 +size 12515 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_50.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_50.png new file mode 100644 index 0000000000000000000000000000000000000000..d2363f79e93b6bd1bd01739f97246f2d4c70f8be --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_50.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b016855f0cbdb1e4991838c6c6729581d72b2b2e952cd52dc1857bb8adf4d31f +size 11677 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_500.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_500.png new file mode 100644 index 0000000000000000000000000000000000000000..02dfb2e06138627abaf54dfb242a0bda2716a982 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_plus_500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c8a5f4e926a67c23b0eca18b10c1e8bc110ddd7ea949f5f693385bc405ecb4c +size 12663 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_shrink.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_shrink.png new file mode 100644 index 0000000000000000000000000000000000000000..b247f13e4a098e10423684dc071db8874a266e20 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_shrink.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a988444810ccf68ae21b8f565a78d6dbd715f24763032422b77d37435b886d8f +size 10438 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_slow.png b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_slow.png new file mode 100644 index 0000000000000000000000000000000000000000..0712c03fc43917d5ff7a2373880a194851afc11b --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/powerup_slow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e67a01d09ed3dec411ecc206840b6d88df26df102f1ec1e1955653a6d447b15c +size 13038 diff --git a/projects/DEMO/Student1/Pygame_Breakout/assets/star_yellow.png b/projects/DEMO/Student1/Pygame_Breakout/assets/star_yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..c6ecbba50c1d41051fb33c504ce02f942f189ce4 --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/assets/star_yellow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ff1a84ad38b9eb9bfb777e0e0d7ebc9ccbdde8db1f1faeba6dedd64c57af990 +size 9558 diff --git a/projects/DEMO/Student1/Pygame_Breakout/main.py b/projects/DEMO/Student1/Pygame_Breakout/main.py new file mode 100644 index 0000000000000000000000000000000000000000..8992cf194bba39677ac1dc76604cbc0f3d4e046d --- /dev/null +++ b/projects/DEMO/Student1/Pygame_Breakout/main.py @@ -0,0 +1,257 @@ +import pygame +import random + +SCREEN_WIDTH = 800 +SCREEN_HEIGHT = 600 + +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +YELLOW = (255, 255, 0) +RED = (255, 0, 0) + +PADDLE_NEW_WIDTH = 110 +PADDLE_NEW_HEIGHT = 22 +BRICK_NEW_WIDTH = 80 +BRICK_NEW_HEIGHT = 25 + +BALL_RADIUS = 10 + +def draw_text(surface, text, size, x, y): + font = pygame.font.Font(None, size) + text_surface = font.render(text, True, WHITE) + text_rect = text_surface.get_rect() + text_rect.midtop = (x, y) + surface.blit(text_surface, text_rect) + +class Paddle(pygame.sprite.Sprite): + def __init__(self): + super().__init__() + + self.animation_frames = [ + pygame.transform.scale( pygame.image.load("assets/paddle_1.png").convert_alpha(), (PADDLE_NEW_WIDTH, PADDLE_NEW_HEIGHT) ), + pygame.transform.scale( pygame.image.load("assets/paddle_2.png").convert_alpha(), (PADDLE_NEW_WIDTH, PADDLE_NEW_HEIGHT) ), + pygame.transform.scale( pygame.image.load("assets/paddle_3.png").convert_alpha(), (PADDLE_NEW_WIDTH, PADDLE_NEW_HEIGHT) ) + ] + self.current_frame = 0 + self.image = self.animation_frames[self.current_frame] + + self.rect = self.image.get_rect() + self.rect.x = (SCREEN_WIDTH - self.rect.width) // 2 + self.rect.y = SCREEN_HEIGHT - self.rect.height - 10 + + self.last_update = pygame.time.get_ticks() + self.frame_rate = 100 + + def update(self): + now = pygame.time.get_ticks() + if now - self.last_update > self.frame_rate: + self.last_update = now + self.current_frame = (self.current_frame + 1) % len(self.animation_frames) + self.image = self.animation_frames[self.current_frame] + + def move(self,keys): + if keys[pygame.K_LEFT]: + self.rect.x -= 7 + if keys[pygame.K_RIGHT]: + self.rect.x += 7 + if keys[pygame.K_UP]: + self.rect.y -= 7 + if keys[pygame.K_DOWN]: + self.rect.y += 7 + if self.rect.right >= SCREEN_WIDTH: + self.rect.right = SCREEN_WIDTH + if self.rect.left <= 0: + self.rect.left = 0 + def widden(self): + print("球拍變寬了") + center = self.rect.x + new_width = int(PADDLE_NEW_WIDTH*1.5) + self.animation_frames = [ + pygame.transform.scale(frame, (new_width, PADDLE_NEW_HEIGHT)) + for frame in self.animation_frames + ] + + +class Ball(pygame.sprite.Sprite): + def __init__(self): + super().__init__() + self.image = pygame.image.load("assets/ball_blue.png").convert_alpha() + self.image = pygame.transform.scale(self.image, (BALL_RADIUS * 2, BALL_RADIUS * 2)) + self.rect = self.image.get_rect() + self.reset() + + def reset(self): + self.rect.x = SCREEN_WIDTH // 2 + self.rect.y = SCREEN_HEIGHT // 2 + 100 + self.speed_x = random.choice([4, -4]) + self.speed_y = -4 + + def update(self): + self.rect.x += self.speed_x + self.rect.y += self.speed_y + if self.rect.left <= 0 or self.rect.right >= SCREEN_WIDTH: + self.speed_x *= -1 + + if self.rect.top <= 0: + self.speed_y *= -1 + +class PowerUp(pygame.sprite.Sprite): + def __init__(self, x, y, color): + super().__init__() + self.color = color + self.image = pygame.Surface((15,15)) + self.image.fill(self.color) + self.rect = self.image.get_rect() + self.rect.centerx = x + self.rect.centery = y + self.speed_y = 3 + + def update(self): + self.rect.y += self.speed_y + if self.rect.top > SCREEN_HEIGHT: + self.kill() + + + +class Brick(pygame.sprite.Sprite): + def __init__(self, x, y, color, points): + super().__init__() + self.points = points + self.color = color + self.image_normal = pygame.transform.scale(pygame.image.load(f"assets/brick_{self.color}.png").convert_alpha(), (BRICK_NEW_WIDTH, BRICK_NEW_HEIGHT)) + self.image_cracked = pygame.transform.scale(pygame.image.load(f"assets/brick_{self.color}_cracked.png").convert_alpha(), (BRICK_NEW_WIDTH, BRICK_NEW_HEIGHT)) + + self.image = self.image_normal + self.rect = self.image.get_rect() + self.rect.x = x + self.rect.y = y + self.health = 2 + + self.all_sprites_group = None + self.powerups_group = None + + def hit(self): + self.health -= 1 + if self.health <= 0: + if random.randint(1,100) <= 20: + new_powerup = PowerUp(self.rect.x, self.rect.y, random.choice([YELLOW, RED])) + self.all_sprites_group.add(new_powerup) + self.powerups_group.add(new_powerup) + self.kill() + return self.points + else: + self.image = self.image_cracked + return 0 + +def game_loop(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("打磚塊遊戲") + clock = pygame.time.Clock() + + game_state = "start" + score = 0 + + all_sprites = pygame.sprite.Group() + bricks = pygame.sprite.Group() + powerups = pygame.sprite.Group() + paddle = Paddle() + ball = Ball() + + def create_brick_wall(): + bricks.empty() + powerups.empty() + all_sprites.empty() + all_sprites.add(paddle) + all_sprites.add(ball) + + + BRICK_COLORS = ["red", "orange", "yellow", "green", "cyan", "blue", "purple"] + for row, color in enumerate(BRICK_COLORS): + points = (7 - row) * 10 + for column in range(SCREEN_WIDTH // (BRICK_NEW_WIDTH + 5)): + brick = Brick( + column * (BRICK_NEW_WIDTH + 5) + 10, + row * (BRICK_NEW_HEIGHT + 10) + 50, + color, + points + ) + all_sprites.add(brick) + bricks.add(brick) + + brick.all_sprites_group = all_sprites + brick.powerups_group = powerups + + + + running = True + while running: + clock.tick(60) + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + if event.type == pygame.KEYDOWN: + if game_state == "start": + game_state = "playing" + score = 0 + paddle.rect.x = (SCREEN_WIDTH - paddle.rect.width) // 2 + paddle.rect.y = SCREEN_HEIGHT - paddle.rect.height - 10 + ball.reset() + create_brick_wall() + elif game_state == "game_over": + game_state = "start" + + if game_state == "start": + screen.fill(BLACK) + draw_text(screen, "BREAKOUT", 64, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 4) + draw_text(screen, "Press any key to start", 40, SCREEN_WIDTH / 2, SCREEN_HEIGHT * 3 / 4) + pygame.display.flip() + elif game_state == "playing": + keys = pygame.key.get_pressed() + paddle.move(keys) + all_sprites.update() + + if pygame.sprite.collide_rect(ball, paddle): + ball.speed_y *= -1 + brick_hit_list = pygame.sprite.spritecollide(ball, bricks, False) + if brick_hit_list: + ball.speed_y *= -1 + for brick in brick_hit_list: + score += brick.hit() + powerups_hit_list = pygame.sprite.spritecollide(paddle, powerups, False) + for _ in powerups_hit_list: + if _.color == YELLOW: + paddle.widden() + score += 50 + elif _.color == RED: + ball.speed_y *= 0.3 + ball.speed_x *= 3 + score += 50 + _.kill() + + + if ball.rect.top >= SCREEN_HEIGHT: + game_state = "game_over" + if not bricks: + game_state = "game_over" + + + screen.fill(BLACK) + all_sprites.draw(screen) + draw_text(screen, f"Score: {score}", 24, SCREEN_WIDTH / 2, 10) + pygame.display.flip() + + elif game_state == "game_over": + screen.fill(BLACK) + draw_text(screen, "GAME OVER", 64, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 4) + draw_text(screen, f"Final Score: {score}", 50, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 4 - 50) + draw_text(screen, "Press any key to restart", 40, SCREEN_WIDTH / 2, SCREEN_HEIGHT * 3 / 4) + pygame.display.flip() + + pygame.quit() + +if __name__ == "__main__": + game_loop() + #| | + # ˇ \ No newline at end of file diff --git "a/projects/DEMO/Student1/Pygame\344\271\222\344\271\223/main.py" "b/projects/DEMO/Student1/Pygame\344\271\222\344\271\223/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..2748102c32cac8a8962c520c34f4f10e4142a6cf --- /dev/null +++ "b/projects/DEMO/Student1/Pygame\344\271\222\344\271\223/main.py" @@ -0,0 +1,172 @@ +import pygame, sys, random + +pygame.init() + +SCREEN_WIDTH = 900 +SCREEN_HEIGHT = 600 +BG_COLOR = pygame.Color("#00D000") +PADDLE_COLOR = pygame.Color("#FF0000") +PADDLE_WIDTH = 10 +PADDLE_HEIGHT = 100 +BALL_COLOR = pygame.Color(255, 144, 0) +x = 144 +BALL_SIZE = 15 +LINE_COLOR = pygame.Color("#FFFFFF") +TEXT_COLOR = pygame.Color("#FFFFFF") + + +title_font = pygame.font.Font(None, 64) +game_font = pygame.font.Font(None, 48) +prompt_font = pygame.font.Font(None, 32) +paddle1 = pygame.Rect(20, SCREEN_HEIGHT // 2 - PADDLE_HEIGHT // 2, PADDLE_WIDTH, PADDLE_HEIGHT) +paddle2 = pygame.Rect(SCREEN_WIDTH - 20 - PADDLE_WIDTH, SCREEN_HEIGHT // 2 - PADDLE_HEIGHT // 2, PADDLE_WIDTH, PADDLE_HEIGHT) +ball = pygame.Rect(SCREEN_WIDTH // 2 - BALL_SIZE // 2, SCREEN_HEIGHT // 2 - BALL_SIZE // 2, BALL_SIZE, BALL_SIZE) +player_1_score = 0 +player_2_score = 0 + +screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) +pygame.display.set_caption("Pong by Pygame") +clock = pygame.time.Clock() + +ball_moving = False +running = True +game_start = False +game_over = False +deuce = False +winner_text = "" + + +while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: + if not ball_moving and not game_over: + ball_moving = True + if not deuce : + if (player_1_score + player_2_score) % 4 == 0 or (player_1_score + player_2_score) % 4 == 1: + ball_speed_x = 7 + else: + ball_speed_x = -7 + else: + if (player_1_score + player_2_score) % 2 == 0: + ball_speed_x = 7 + else: + ball_speed_x = -7 + ball_speed_y = 7*random.choice((1, -1)) + + if game_over: + game_over = False + winner_text = "" + player_1_score = 0 + player_2_score = 0 + ball_moving = False + ball.center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2) + if not game_start: + game_start = True + + keys = pygame.key.get_pressed() + if keys[pygame.K_w]: + paddle1.y -= 10 + if keys[pygame.K_s]: + paddle1.y += 10 + if keys[pygame.K_UP]: + paddle2.y -= 10 + if keys[pygame.K_DOWN]: + paddle2.y += 10 + screen.fill(BG_COLOR) + pygame.draw.rect(screen, PADDLE_COLOR, paddle1) + pygame.draw.rect(screen, PADDLE_COLOR, paddle2) + pygame.draw.ellipse(screen, BALL_COLOR, ball) + pygame.draw.aaline(screen, LINE_COLOR, (SCREEN_WIDTH // 2, 0), (SCREEN_WIDTH // 2, SCREEN_HEIGHT)) + + text1 = game_font.render(f"{player_1_score}", True, TEXT_COLOR) + p1_rect = text1.get_rect(topright = (SCREEN_WIDTH // 2 - 60, 20)) + screen.blit(text1, p1_rect) + text2 = game_font.render(f"{player_2_score}", True, TEXT_COLOR) + p2_rect = text2.get_rect(topleft = (SCREEN_WIDTH // 2 + 60, 20)) + screen.blit(text2, p2_rect) + + if not game_start:#開始畫面 + title_surface = title_font.render("Pong Game", True, TEXT_COLOR) + title_rect = title_surface.get_rect(center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - 100)) + + prompt_surface = prompt_font.render("Press Space to Start", True, TEXT_COLOR) + prompt_rect = prompt_rect = prompt_surface.get_rect(center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)) + + screen.blit(title_surface, title_rect) + screen.blit(prompt_surface, prompt_rect) + + pong_img = pygame.image.load("pong.png") + pong_img = pygame.transform.scale(pong_img, (100, 100)) + screen.blit(pong_img, (SCREEN_WIDTH // 2 - pong_img.get_width() // 2, SCREEN_HEIGHT//2+40)) + if not ball_moving and game_start and not game_over: + prompt_surface = prompt_font.render("Press Space to Serve", True, TEXT_COLOR) + prompt_rect = prompt_surface.get_rect(center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT - 50)) + screen.blit(prompt_surface, prompt_rect) + + + if ball_moving: + ball.x += ball_speed_x + ball.y += ball_speed_y + if ball.top <= 0 or ball.bottom >= SCREEN_HEIGHT: + ball_speed_y *= -1 + if ball.colliderect(paddle1) or ball.colliderect(paddle2): + ball_speed_x *= -1.05 + ball_speed_y *= 1.05 + if x > 0: + x -= x/10 + BALL_COLOR = (255, x, 0) + if ball.left <= 0: + player_2_score += 1 + x = 144 + if deuce and player_2_score - player_1_score >= 2: + winner_text = "Player 2 Wins !" + game_over = True + deuce = False + if not deuce and player_2_score >= 11: + winner_text = "Player 2 Wins !" + game_over = True + deuce = False + if not deuce and player_1_score >= 10 and player_2_score >= 10: + deuce = True + ball.center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2) + ball_moving = False + if ball.right >= SCREEN_WIDTH: + player_1_score += 1 + x = 144 + if deuce and player_1_score - player_2_score >= 2: + winner_text = "Player 1 Wins !" + game_over = True + deuce = False + if not deuce and player_1_score >= 11: + winner_text = "Player 1 Wins !" + game_over = True + if not deuce and player_1_score >= 10 and player_2_score >= 10: + deuce = True + deuce = False + ball.center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2) + ball_moving = False + if game_over: + game_over_surface = title_font.render(winner_text, True, TEXT_COLOR) + game_over_rect = game_over_surface.get_rect(center = (SCREEN_WIDTH // 2 , SCREEN_HEIGHT // 2 -50)) + screen.blit(game_over_surface, game_over_rect) + + restart_surface = prompt_font.render("Press Space to Restart", True, TEXT_COLOR) + restart_rect = restart_surface.get_rect(center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 20)) + screen.blit(restart_surface, restart_rect) + + + if paddle1.top <= 0: + paddle1.top = 0 + if paddle1.bottom >= SCREEN_HEIGHT: + paddle1.bottom = SCREEN_HEIGHT + if paddle2.top <= 0: + paddle2.top = 0 + if paddle2.bottom >= SCREEN_HEIGHT: + paddle2.bottom = SCREEN_HEIGHT + pygame.display.flip() + clock.tick(60) + +pygame.quit() +sys.exit() \ No newline at end of file diff --git "a/projects/DEMO/Student1/Pygame\344\271\222\344\271\223/pong.png" "b/projects/DEMO/Student1/Pygame\344\271\222\344\271\223/pong.png" new file mode 100644 index 0000000000000000000000000000000000000000..5908577121c95a65340c4627b1253a95b56c85e0 --- /dev/null +++ "b/projects/DEMO/Student1/Pygame\344\271\222\344\271\223/pong.png" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c70284f960a6c0b7d1359d0a0318492a87af10f343d7b7677d10124d29f5b148 +size 20959 diff --git "a/projects/DEMO/Student1/Scratch \347\265\202\346\245\265\345\257\206\347\242\274/link.txt" "b/projects/DEMO/Student1/Scratch \347\265\202\346\245\265\345\257\206\347\242\274/link.txt" new file mode 100644 index 0000000000000000000000000000000000000000..8c9c4289caf523a2075a77ba8db0c829cc6e48cc --- /dev/null +++ "b/projects/DEMO/Student1/Scratch \347\265\202\346\245\265\345\257\206\347\242\274/link.txt" @@ -0,0 +1 @@ +https://scratch.mit.edu/projects/845366650/ diff --git "a/projects/DEMO/Student1/\346\211\213\345\213\242\346\215\217\345\220\210\346\216\247\345\210\266/hand_landmarker.task" "b/projects/DEMO/Student1/\346\211\213\345\213\242\346\215\217\345\220\210\346\216\247\345\210\266/hand_landmarker.task" new file mode 100644 index 0000000000000000000000000000000000000000..5ecab741879892d97c2f90bbf03bf55d7213db7c --- /dev/null +++ "b/projects/DEMO/Student1/\346\211\213\345\213\242\346\215\217\345\220\210\346\216\247\345\210\266/hand_landmarker.task" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbc2a30080c3c557093b5ddfc334698132eb341044ccee322ccf8bcf3607cde1 +size 7819105 diff --git "a/projects/DEMO/Student1/\346\211\213\345\213\242\346\215\217\345\220\210\346\216\247\345\210\266/main.py" "b/projects/DEMO/Student1/\346\211\213\345\213\242\346\215\217\345\220\210\346\216\247\345\210\266/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..fd47093d988373db13306a41baa3a206c21cbc45 --- /dev/null +++ "b/projects/DEMO/Student1/\346\211\213\345\213\242\346\215\217\345\220\210\346\216\247\345\210\266/main.py" @@ -0,0 +1,179 @@ +""" +Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (Complete 完整版) +============================================================= +本週目標: + 1. 用勾股定理計算大拇指 (4) 與食指 (8) 的距離 + 2. 實作 Pinch (掐合) 偵測:距離 < Threshold 就是捏起來了 + 3. 用手指碰觸 Pygame 畫面上的按鈕 + 掐合 = 魔法點擊 + 4. 使用邊緣觸發 (Edge Detection) 防止重複觸發 + +操作方式: + - 食指移動到按鈕上方 + 大拇指與食指捏合 = 點擊按鈕 + - 顏色會隨點擊切換 + +注意: + - 確保 hand_landmarker.task 檔案和這個腳本放在同一個資料夾。 +""" +import pygame +import cv2 +import mediapipe as mp +from mediapipe.tasks import python +from mediapipe.tasks.python import vision +import math +import sys +import os + +# ============================================ +# 1. 初始化 +# ============================================ +pygame.init() +WIDTH, HEIGHT = 800, 600 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Week 10 - 魔法手勢按鈕 🖐️✨") +clock = pygame.time.Clock() + +# 攝影機 +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("❌ 無法開啟攝影機,請改用影片檔案") + pygame.quit() + sys.exit() + +# MediaPipe (Tasks API) +try: + base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(__file__), 'hand_landmarker.task')) + options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) + detector = vision.HandLandmarker.create_from_options(options) +except Exception as e: + print(f"❌ 錯誤:無法載入 MediaPipe 模型:{e}") + pygame.quit() + sys.exit() + +# ============================================ +# 2. 遊戲變數 +# ============================================ +# 手指座標 +finger_x, finger_y = 400, 300 +thumb_x, thumb_y = 400, 300 + +# Pinch 變數 +is_pinching = False +was_pinching = False # 邊緣觸發:紀錄上一幀的狀態 +PINCH_THRESHOLD = 40 # 距離小於此值就算「捏起來」 +dist = 999 # 兩指距離 + +# 按鈕設定 +button_rect = pygame.Rect(300, 250, 200, 100) +button_colors = [(100, 100, 220), (220, 100, 100), (100, 220, 100), (220, 180, 50)] +current_color_index = 0 + +# 點擊計數器 +click_count = 0 +hand_detected = False + +# ============================================ +# 3. 主迴圈 +# ============================================ +running = True +while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # --- 讀取攝影機 --- + success, img = cap.read() + if success: + img = cv2.flip(img, 1) + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + if results.hand_landmarks: + hand_detected = True + for hand_lms in results.hand_landmarks: + # 取得食指尖 (8) 座標 + lm8 = hand_lms[8] + finger_x = int(lm8.x * WIDTH) + finger_y = int(lm8.y * HEIGHT) + + # 取得大拇指尖 (4) 座標 + lm4 = hand_lms[4] + thumb_x = int(lm4.x * WIDTH) + thumb_y = int(lm4.y * HEIGHT) + + # 用 math.hypot 計算兩指之間的距離 + dist = math.hypot(finger_x - thumb_x, finger_y - thumb_y) + + # 判斷是否掐合 + is_pinching = dist < PINCH_THRESHOLD + else: + hand_detected = False + is_pinching = False + + # --- 邊緣觸發判定 --- + # 只有「前一幀沒捏 + 這一幀捏了」才算一次點擊 + if is_pinching and not was_pinching: + # 檢查手指是否在按鈕範圍內 + finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10) + if finger_rect.colliderect(button_rect): + click_count += 1 + current_color_index = (current_color_index + 1) % len(button_colors) + print(f"✨ 魔法點擊!第 {click_count} 次") + + # 更新上一幀的狀態 + was_pinching = is_pinching + + # --- 繪製畫面 --- + screen.fill((20, 20, 35)) + + # 畫按鈕 + btn_color = button_colors[current_color_index] + pygame.draw.rect(screen, btn_color, button_rect, border_radius=16) + + font = pygame.font.SysFont(None, 32) + btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255)) + screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2, + button_rect.centery - btn_text.get_height() // 2)) + + # 畫提示文字在按鈕下方 + hint_font = pygame.font.SysFont(None, 22) + hint = hint_font.render("Pinch on button to click!", True, (120, 120, 150)) + screen.blit(hint, (button_rect.centerx - hint.get_width() // 2, button_rect.bottom + 12)) + + if hand_detected: + # 畫大拇指位置(紫色小點) + pygame.draw.circle(screen, (139, 92, 246), (thumb_x, thumb_y), 8) + + # 畫兩指之間的連線 + line_color = (50, 255, 50) if is_pinching else (255, 50, 50) + pygame.draw.line(screen, line_color, (thumb_x, thumb_y), (finger_x, finger_y), 2) + + # 畫手指游標(根據 Pinch 狀態改變顏色) + if is_pinching: + cursor_color = (50, 255, 50) # 捏起來 → 綠色 + else: + cursor_color = (255, 50, 50) # 放開 → 紅色 + pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12) + + # 狀態文字 + small_font = pygame.font.SysFont(None, 24) + coord_text = small_font.render(f"Index(8): ({finger_x}, {finger_y}) Thumb(4): ({thumb_x}, {thumb_y})", True, (150, 150, 150)) + screen.blit(coord_text, (10, 10)) + + dist_text = small_font.render(f"Distance: {dist:.0f} px | Threshold: {PINCH_THRESHOLD}", True, (150, 150, 200)) + screen.blit(dist_text, (10, 35)) + + pinch_text = small_font.render(f"Pinching: {'YES ✅' if is_pinching else 'NO ❌'}", True, + (50, 255, 50) if is_pinching else (255, 100, 100)) + screen.blit(pinch_text, (10, 60)) + else: + no_hand = font.render("Show your hand to the camera!", True, (255, 200, 0)) + screen.blit(no_hand, (WIDTH // 2 - no_hand.get_width() // 2, 50)) + + pygame.display.update() + clock.tick(30) + +cap.release() +pygame.quit() +sys.exit() diff --git "a/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/background.jpg" "b/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/background.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..7b98b815bc295e37f93ad72b68f7990170aa05d4 --- /dev/null +++ "b/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/background.jpg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b4550b2073dfd8267a1d2fc94620e82aada100ab95114d44a62eef8e40e3e7e +size 220630 diff --git "a/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/hand_landmarker.task" "b/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/hand_landmarker.task" new file mode 100644 index 0000000000000000000000000000000000000000..5ecab741879892d97c2f90bbf03bf55d7213db7c --- /dev/null +++ "b/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/hand_landmarker.task" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbc2a30080c3c557093b5ddfc334698132eb341044ccee322ccf8bcf3607cde1 +size 7819105 diff --git "a/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/hit.wav" "b/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/hit.wav" new file mode 100644 index 0000000000000000000000000000000000000000..d138a2494493124e0200999b18a4c4315b860a72 --- /dev/null +++ "b/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/hit.wav" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55440a99e00bb9de3a53bcaa709c83d412001f78dcf7103036ec02a5057c2fdd +size 13274 diff --git "a/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/main.py" "b/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..c38f5c3fb4d6a36f18143ccc5e5e502db7ed9fb6 --- /dev/null +++ "b/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/main.py" @@ -0,0 +1,143 @@ +""" +Week 9 - 空中雷射筆 (Complete 完整版) +=========================================== +本週目標: + 1. 用 OpenCV 開啟攝影機 + 2. 用 MediaPipe 偵測手部 21 個關鍵點 + 3. 取得食指尖 (Landmark 8) 的座標 + 4. 把座標映射到 Pygame 畫布上,畫出一顆跟著手指移動的紅色雷射點 + +操作方式: + - 在鏡頭前移動手指,Pygame 視窗的紅點會跟著移動 + - 按鍵盤 Q 或關閉視窗即可結束 + +注意: + - 確保 hand_landmarker.task 檔案和這個腳本放在同一個資料夾。 +""" +import pygame +import cv2 +import mediapipe as mp +from mediapipe.tasks import python +from mediapipe.tasks.python import vision +import sys +import os + +# ============================================ +# 1. 初始化 Pygame +# ============================================ +pygame.init() +WIDTH, HEIGHT = 800, 600 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Week 9 - 空中雷射筆 ✋🔴") +clock = pygame.time.Clock() + +# ============================================ +# 2. 初始化 OpenCV 攝影機 +# ============================================ +cap = cv2.VideoCapture(0) +# 如果沒有攝影機,請改為: +# cap = cv2.VideoCapture("backup_hand_video.mp4") + +if not cap.isOpened(): + print("❌ 錯誤:無法開啟攝影機!") + print(" 請嘗試改用影片檔案:cap = cv2.VideoCapture('backup_hand_video.mp4')") + pygame.quit() + sys.exit() + +# ============================================ +# 3. 初始化 MediaPipe 手部偵測 (Tasks API) +# ============================================ +try: + base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(__file__), 'hand_landmarker.task')) + options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) + detector = vision.HandLandmarker.create_from_options(options) +except Exception as e: + print(f"❌ 錯誤:無法載入 MediaPipe 模型:{e}") + print(" 請確認 'hand_landmarker.task' 是否與這個程式碼在同一個資料夾!") + pygame.quit() + sys.exit() + +# ============================================ +# 4. 雷射筆座標 +# ============================================ +laser_x, laser_y = WIDTH // 2, HEIGHT // 2 # 初始位置在畫面中央 +hand_detected = False # 用來追蹤目前有沒有偵測到手 + +# ============================================ +# 5. 主迴圈 +# ============================================ +running = True +frame_count = 0 # 用來做 Frame Skip(選用進階功能) + +while running: + # --- 事件處理 --- + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # --- 讀取攝影機影格 --- + success, img = cap.read() + if not success: + # 如果是影片檔讀完了,就從頭開始 + cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + continue + + img = cv2.flip(img, 1) # 鏡面翻轉,讓畫面像照鏡子一樣直覺 + + # --- BGR 轉 RGB 並丟給 MediaPipe 分析 --- + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + # --- 提取食指尖 (Landmark 8) 的座標 --- + if results.hand_landmarks: + hand_detected = True + for hand_lms in results.hand_landmarks: + lm8 = hand_lms[8] # Index Finger Tip + + # MediaPipe 回傳 0~1 的比例值,乘上 Pygame 視窗大小 + laser_x = int(lm8.x * WIDTH) + laser_y = int(lm8.y * HEIGHT) + else: + hand_detected = False + + # --- 繪製畫面 --- + screen.fill((15, 15, 35)) # 深色背景 + + # 狀態文字 + font = pygame.font.SysFont(None, 28) + + if hand_detected: + # 畫出雷射光點(紅色圓點 + 外圈光暈) + # 外層光暈 + glow_surf = pygame.Surface((60, 60), pygame.SRCALPHA) + pygame.draw.circle(glow_surf, (255, 50, 50, 40), (30, 30), 28) + screen.blit(glow_surf, (laser_x - 30, laser_y - 30)) + + # 中層 + pygame.draw.circle(screen, (255, 80, 80), (laser_x, laser_y), 16) + # 內核(最亮) + pygame.draw.circle(screen, (255, 200, 200), (laser_x, laser_y), 6) + + # 座標文字 + coord_text = font.render(f"Index Tip (Landmark 8): ({laser_x}, {laser_y})", True, (100, 255, 100)) + screen.blit(coord_text, (10, 10)) + else: + # 沒偵測到手的提示 + no_hand = font.render("No hand detected - please show your hand!", True, (255, 200, 0)) + screen.blit(no_hand, (WIDTH // 2 - no_hand.get_width() // 2, HEIGHT // 2)) + + # 底部提示 + hint = font.render("Move your index finger in front of the camera", True, (100, 100, 130)) + screen.blit(hint, (WIDTH // 2 - hint.get_width() // 2, HEIGHT - 40)) + + pygame.display.update() + clock.tick(30) + frame_count += 1 + +# ============================================ +# 6. 結束清理 +# ============================================ +cap.release() +pygame.quit() +sys.exit() diff --git "a/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/shoot.wav" "b/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/shoot.wav" new file mode 100644 index 0000000000000000000000000000000000000000..e4b1fe39a70052df01006c8b89a352f6e18b3f3d --- /dev/null +++ "b/projects/DEMO/Student1/\346\211\213\345\213\242\351\233\267\345\260\204\347\231\274\345\260\204/shoot.wav" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:091481354d97c5469a2cd40f16d8fad63904eb4676992c34c1f208fc87a8173c +size 17684 diff --git "a/projects/DEMO/Student1/\350\246\226\345\212\233\350\255\267\347\234\274\350\241\233\345\243\253/face_landmarker.task" "b/projects/DEMO/Student1/\350\246\226\345\212\233\350\255\267\347\234\274\350\241\233\345\243\253/face_landmarker.task" new file mode 100644 index 0000000000000000000000000000000000000000..fedb14de6d2b6708a56c04ae259783e23404c1aa --- /dev/null +++ "b/projects/DEMO/Student1/\350\246\226\345\212\233\350\255\267\347\234\274\350\241\233\345\243\253/face_landmarker.task" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64184e229b263107bc2b804c6625db1341ff2bb731874b0bcc2fe6544e0bc9ff +size 3758596 diff --git "a/projects/DEMO/Student1/\350\246\226\345\212\233\350\255\267\347\234\274\350\241\233\345\243\253/main.py" "b/projects/DEMO/Student1/\350\246\226\345\212\233\350\255\267\347\234\274\350\241\233\345\243\253/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..12e284ee816fb7ef317571ef0bc50f19e5d91cc8 --- /dev/null +++ "b/projects/DEMO/Student1/\350\246\226\345\212\233\350\255\267\347\234\274\350\241\233\345\243\253/main.py" @@ -0,0 +1,260 @@ +""" +Week 12 - AI 生活工具箱:護眼大師 (Complete 完整版) +===================================================== +本週目標: + 1. 將 AI 視覺技術整合進一個「解決生活痛點」的完整專案 + 2. 使用 MediaPipe 臉部偵測兩眼距離,判斷是否離螢幕太近 + 3. 在 Pygame 畫面上顯示即時狀態與警報 + 4. 加入防呆機制(try-except、None 檢查) + +護眼大師 EyeGuard: + - 偵測臉部兩眼之間的距離(兩眼距離越大 = 離鏡頭越近) + - 超過安全閾值時發出警報文字 + 閃爍效果 + 音效 + - 安全時累計計時器鼓勵良好坐姿 + +注意: + - 確保 face_landmarker.task 檔案和這個腳本放在同一個資料夾。 +""" +import pygame +import cv2 +import mediapipe as mp +from mediapipe.tasks import python +from mediapipe.tasks.python import vision +import math +import sys +import os +import time + +# ============================================ +# 1. 初始化 +# ============================================ +pygame.init() +pygame.mixer.init() +WIDTH, HEIGHT = 800, 600 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Week 12 - 護眼大師 EyeGuard 👁️🛡️") +clock = pygame.time.Clock() + +font = pygame.font.SysFont(None, 48) +small_font = pygame.font.SysFont(None, 28) +big_font = pygame.font.SysFont(None, 72) +tiny_font = pygame.font.SysFont(None, 22) + +# ============================================ +# 2. 攝影機與 AI 模組 +# ============================================ +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("❌ 無法開啟攝影機,請改用影片檔案") + pygame.quit() + sys.exit() + +# MediaPipe 臉部偵測 (Tasks API) +try: + base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(__file__), 'face_landmarker.task')) + options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) + detector = vision.FaceLandmarker.create_from_options(options) +except Exception as e: + print(f"❌ 錯誤:無法載入 MediaPipe 模型:{e}") + pygame.quit() + sys.exit() + +# 關鍵 Landmark 索引 +LEFT_EYE = 33 # 左眼外角 +RIGHT_EYE = 263 # 右眼外角 + +# ============================================ +# 3. 遊戲變數 +# ============================================ +eye_distance = 0 +lx, ly = 0, 0 # 左眼座標 +rx, ry = 0, 0 # 右眼座標 +face_detected = False + +DANGER_THRESHOLD = 120 # 兩眼像素距離超過此值 = 太近了! +SAFE_THRESHOLD = 100 # 低於此值 = 安全(遲滯效應,避免臨界值抖動) + +# 警報狀態 +is_too_close = False +was_too_close = False # 邊緣觸發 +warning_timer = 0 # 警告閃爍計時器 + +# 統計數據 +total_warnings = 0 +safe_start_time = time.time() +consecutive_safe_seconds = 0 +frame_counter = 0 + +# 載入音效 +try: + alarm_sound = pygame.mixer.Sound("hit.wav") # 用現有的 hit.wav 代替警報音 + print("✅ 警報音效載入成功") +except FileNotFoundError: + alarm_sound = None + print("⚠️ 找不到警報音效,將不播放音效") + +# ============================================ +# 4. 主迴圈 +# ============================================ +running = True +while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # --- 讀取攝影機 --- + success, img = cap.read() + if not success: + cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + continue + + img = cv2.flip(img, 1) + frame_counter += 1 + + # --- 臉部偵測與兩眼距離計算 --- + try: + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + if results.face_landmarks: + face_detected = True + face_lms = results.face_landmarks[0] + + # 取得左眼和右眼座標 + left_eye = face_lms[LEFT_EYE] + right_eye = face_lms[RIGHT_EYE] + + lx = int(left_eye.x * WIDTH) + ly = int(left_eye.y * HEIGHT) + rx = int(right_eye.x * WIDTH) + ry = int(right_eye.y * HEIGHT) + + # 計算兩眼之間的像素距離 + eye_distance = math.hypot(rx - lx, ry - ly) + + # 使用遲滯效應判斷(避免臨界值抖動) + if eye_distance > DANGER_THRESHOLD: + is_too_close = True + elif eye_distance < SAFE_THRESHOLD: + is_too_close = False + else: + face_detected = False + is_too_close = False + except Exception as e: + # 防禦性程式碼:任何異常都不應讓程式閃退 + print(f"偵測異常:{e}") + face_detected = False + + # --- 邊緣觸發判定 --- + if is_too_close and not was_too_close: + # 剛進入危險區 + total_warnings += 1 + warning_timer = 90 # 閃爍 90 幀(約 3 秒) + if alarm_sound: + alarm_sound.play() + safe_start_time = time.time() # 重置安全計時 + print(f"⚠️ 第 {total_warnings} 次警告:你離螢幕太近了!(distance={eye_distance:.0f})") + + if not is_too_close and was_too_close: + # 剛離開危險區 + safe_start_time = time.time() + print("✅ 已回到安全距離") + + was_too_close = is_too_close + + # 計算連續安全秒數 + if not is_too_close and face_detected: + consecutive_safe_seconds = time.time() - safe_start_time + else: + consecutive_safe_seconds = 0 + + # 閃爍倒數 + if warning_timer > 0: + warning_timer -= 1 + + # --- 繪製畫面 --- + # 背景 + if is_too_close: + if warning_timer > 0 and warning_timer % 6 < 3: + screen.fill((90, 15, 15)) # 強烈閃爍紅 + else: + screen.fill((50, 12, 12)) # 暗紅 + else: + screen.fill((12, 30, 18)) # 安全深綠 + + if face_detected: + # --- 距離量表 --- + gauge_x, gauge_y = 50, 50 + gauge_w, gauge_h = 700, 30 + + # 軌道背景 + pygame.draw.rect(screen, (40, 40, 55), (gauge_x, gauge_y, gauge_w, gauge_h), border_radius=8) + + # 填充條(根據距離改變顏色與長度) + fill_ratio = min(eye_distance / 200, 1.0) + fill_w = int(fill_ratio * gauge_w) + if eye_distance > DANGER_THRESHOLD: + fill_color = (239, 68, 68) # 紅色 + elif eye_distance > SAFE_THRESHOLD: + fill_color = (245, 158, 11) # 黃色 + else: + fill_color = (34, 197, 94) # 綠色 + pygame.draw.rect(screen, fill_color, (gauge_x, gauge_y, fill_w, gauge_h), border_radius=8) + + # 閾值標記線 + threshold_x = gauge_x + int(DANGER_THRESHOLD / 200 * gauge_w) + pygame.draw.line(screen, (255, 255, 255), (threshold_x, gauge_y - 5), (threshold_x, gauge_y + gauge_h + 5), 2) + threshold_label = tiny_font.render(f"DANGER: {DANGER_THRESHOLD}", True, (200, 200, 200)) + screen.blit(threshold_label, (threshold_x - threshold_label.get_width() // 2, gauge_y + gauge_h + 8)) + + # 距離數值 + dist_text = small_font.render(f"Eye Distance: {eye_distance:.0f} px", True, (200, 200, 220)) + screen.blit(dist_text, (gauge_x, gauge_y - 28)) + + # --- 眼睛位置標記 --- + pygame.draw.circle(screen, (59, 130, 246), (lx, ly), 8) + pygame.draw.circle(screen, (59, 130, 246), (rx, ry), 8) + pygame.draw.line(screen, (59, 130, 246, 100), (lx, ly), (rx, ry), 2) + + # --- 主要狀態顯示 --- + if is_too_close: + warn_text = big_font.render("TOO CLOSE!", True, (255, 80, 80)) + screen.blit(warn_text, (WIDTH // 2 - warn_text.get_width() // 2, HEIGHT // 2 - 60)) + + sub_text = font.render("Please lean back!", True, (255, 150, 150)) + screen.blit(sub_text, (WIDTH // 2 - sub_text.get_width() // 2, HEIGHT // 2 + 20)) + else: + safe_text = font.render("Safe Distance", True, (80, 220, 100)) + screen.blit(safe_text, (WIDTH // 2 - safe_text.get_width() // 2, HEIGHT // 2 - 30)) + + # 連續安全時間 + if consecutive_safe_seconds > 3: + streak_text = small_font.render( + f"Good posture for {consecutive_safe_seconds:.0f}s! Keep it up!", + True, (100, 200, 150) + ) + screen.blit(streak_text, (WIDTH // 2 - streak_text.get_width() // 2, HEIGHT // 2 + 20)) + + # --- 統計資訊 --- + stats_y = HEIGHT - 80 + stats1 = small_font.render(f"Total Warnings: {total_warnings}", True, (150, 150, 180)) + screen.blit(stats1, (20, stats_y)) + + stats2 = small_font.render(f"Frame: {frame_counter} | FPS: {clock.get_fps():.0f}", True, (100, 100, 130)) + screen.blit(stats2, (20, stats_y + 28)) + + else: + # 沒偵測到臉 + no_face = font.render("No face detected", True, (255, 200, 0)) + screen.blit(no_face, (WIDTH // 2 - no_face.get_width() // 2, HEIGHT // 2 - 20)) + + hint = small_font.render("Please face the camera", True, (180, 180, 100)) + screen.blit(hint, (WIDTH // 2 - hint.get_width() // 2, HEIGHT // 2 + 30)) + + pygame.display.update() + clock.tick(30) + +cap.release() +pygame.quit() +sys.exit() diff --git "a/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/background.jpg" "b/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/background.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..7b98b815bc295e37f93ad72b68f7990170aa05d4 --- /dev/null +++ "b/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/background.jpg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b4550b2073dfd8267a1d2fc94620e82aada100ab95114d44a62eef8e40e3e7e +size 220630 diff --git "a/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/hit.wav" "b/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/hit.wav" new file mode 100644 index 0000000000000000000000000000000000000000..d138a2494493124e0200999b18a4c4315b860a72 --- /dev/null +++ "b/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/hit.wav" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55440a99e00bb9de3a53bcaa709c83d412001f78dcf7103036ec02a5057c2fdd +size 13274 diff --git "a/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/main.py" "b/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..80e6097aab3d94e230731468e3783047206c2b06 --- /dev/null +++ "b/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/main.py" @@ -0,0 +1,232 @@ +import pygame +import socket +import json +import sys + +pygame.init() +pygame.mixer.init() +screen = pygame.display.set_mode((800, 600)) +clock = pygame.time.Clock() + +# --- 1. 網路設定(自動判斷 P1 / P2)--- +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.setblocking(False) + +# 嘗試綁定 port 5000,若被佔用則綁定 5001 +# 先啟動的人 = P1 (左側),後啟動的人 = P2 (右側) +try: + sock.bind(("0.0.0.0", 5000)) + MY_PORT = 5000 + TARGET_PORT = 5001 + player_role = "P1" +except OSError: + sock.bind(("0.0.0.0", 5001)) + MY_PORT = 5001 + TARGET_PORT = 5000 + player_role = "P2" + +# 請修改為隔壁同學的 IP 準備互連! +TARGET_IP = "127.0.0.1" + +# --- 2. 遊戲參數與各種變數(根據角色自動設定位置與方向)--- +if player_role == "P1": + my_pos = [100, 300] # P1 在左邊 + enemy_pos = [600, 300] # 對手在右邊 + my_bullet_dir = 10 # P1 子彈朝右飛 + enemy_bullet_dir = -10 # 對手子彈朝左飛 +else: + my_pos = [600, 300] # P2 在右邊 + enemy_pos = [100, 300] # 對手在左邊 + my_bullet_dir = -10 # P2 子彈朝左飛 + enemy_bullet_dir = 10 # 對手子彈朝右飛 + +my_hp = 10 +enemy_hp = 10 +my_score = 0 +enemy_score = 0 +bullets = [] +bullet_id_counter = 0 # 子彈編號計數器,用來同步雙方畫面 +game_over = False +opponent_connected = False # 追蹤對手是否已連線 + +pygame.display.set_caption(f"Week 8 - 網路競技場最終戰 [{player_role} | Port {MY_PORT}]") +print(f"遊戲啟動!你是 {player_role}(監聽 port {MY_PORT},發送至 port {TARGET_PORT})") +print(f"位置:{'左側' if player_role == 'P1' else '右側'}") +print("等待連線中...") + +# 字體與素材載入 +font = pygame.font.SysFont(None, 36) +big_font = pygame.font.SysFont(None, 72) + +try: + bg_img = pygame.image.load("background.jpg") + bg_img = pygame.transform.scale(bg_img, (800, 600)) +except FileNotFoundError: + bg_img = None + +try: + shoot_sound = pygame.mixer.Sound("shoot.wav") +except FileNotFoundError: + shoot_sound = None + +try: + hit_sound = pygame.mixer.Sound("hit.wav") +except FileNotFoundError: + hit_sound = None + +# --- 3. 遊戲主迴圈 --- +while True: + if bg_img: + screen.blit(bg_img, (0, 0)) + else: + screen.fill((20, 20, 30)) + + # ======= [網路區間:接收最新消息] ======= + # 【Bug修復】: 加入 while True 迴圈,一次清空所有堆積的網路封包,避免子彈穿透延遲! + while True: + try: + data, addr = sock.recvfrom(1024) + opponent_connected = True # 只要收到任何封包,就代表對手來了 + pkg = json.loads(data.decode()) + + if pkg['type'] == 'pos': + enemy_pos[0] = pkg['x'] + enemy_pos[1] = pkg['y'] + + elif pkg['type'] == 'shoot': + # 對手的子彈:用 enemy_bullet_dir 決定飛行方向,帶上對方給的 bullet_id + bullets.append({'x': pkg['x'], 'y': pkg['y'], 'dir': enemy_bullet_dir, 'owner': 'enemy', 'id': pkg['id']}) + if shoot_sound: + shoot_sound.play() + + elif pkg['type'] == 'hit': + enemy_hp = pkg.get('hp', 10) + # 對方被我的子彈打中了 → 從我的畫面上移除那顆子彈 + hit_bullet_id = pkg.get('bullet_id') + if hit_bullet_id is not None: + bullets[:] = [b for b in bullets if not (b['owner'] == 'me' and b['id'] == hit_bullet_id)] + + elif pkg['type'] == 'im_dead': + # 對方宣告死亡 + enemy_hp = pkg.get('hp', 0) + my_score += 1 + hit_bullet_id = pkg.get('bullet_id') + if hit_bullet_id is not None: + bullets[:] = [b for b in bullets if not (b['owner'] == 'me' and b['id'] == hit_bullet_id)] + + except (BlockingIOError, ConnectionResetError): + # 非阻塞模式下,Linux 拋 BlockingIOError,Windows 拋 ConnectionResetError + break + except json.JSONDecodeError: + # 防止有人亂傳不是 JSON 格式的垃圾訊息導致閃退 + pass + + # ======= [事件區間:玩家輸入] ======= + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit(); sys.exit() + + if event.type == pygame.KEYDOWN: + # 只有活著且對手連線時才能開槍 + if event.key == pygame.K_SPACE and not game_over and opponent_connected: + bullet_id_counter += 1 + bullets.append({'x': my_pos[0], 'y': my_pos[1], 'dir': my_bullet_dir, 'owner': 'me', 'id': bullet_id_counter}) + shoot_pkg = {'type': 'shoot', 'x': my_pos[0], 'y': my_pos[1], 'id': bullet_id_counter} + sock.sendto(json.dumps(shoot_pkg).encode(), (TARGET_IP, TARGET_PORT)) + if shoot_sound: + shoot_sound.play() + + # 死亡時可以按 R 鍵重生 + if event.key == pygame.K_r and game_over: + game_over = False + my_hp = 10 + my_pos = [100, 300] if player_role == "P1" else [600, 300] + respawn_pkg = {'type': 'hit', 'hp': my_hp} + sock.sendto(json.dumps(respawn_pkg).encode(), (TARGET_IP, TARGET_PORT)) + + # 處理 WASD 或方向鍵移動 (活著才能移動) + if not game_over: + keys = pygame.key.get_pressed() + if keys[pygame.K_w]: my_pos[1] -= 5 + if keys[pygame.K_s]: my_pos[1] += 5 + if keys[pygame.K_a]: my_pos[0] -= 5 + if keys[pygame.K_d]: my_pos[0] += 5 + + # ======= [運算與繪圖區間] ======= + my_rect = pygame.Rect(my_pos[0], my_pos[1], 40, 40) + enemy_rect = pygame.Rect(enemy_pos[0], enemy_pos[1], 40, 40) + + # 畫出自己與對手的人物實體 + if not game_over: + pygame.draw.rect(screen, (59, 130, 246), my_rect) + + # 假設對手 > 0 血才畫對手 (而且必須已連線) + if opponent_connected and enemy_hp > 0: + pygame.draw.rect(screen, (16, 185, 129), enemy_rect) + + # 畫計分板與血條 + score_text = font.render(f"Score - You: {my_score} | Enemy: {enemy_score}", True, (255, 255, 255)) + screen.blit(score_text, (800//2 - score_text.get_width()//2, 15)) + + if not game_over: + my_hp_text = font.render(f"HP: {my_hp}", True, (255, 255, 255)) + screen.blit(my_hp_text, (my_pos[0], my_pos[1] - 30)) + + if opponent_connected and enemy_hp > 0: + enemy_hp_text = font.render(f"HP: {enemy_hp}", True, (255, 255, 255)) + screen.blit(enemy_hp_text, (enemy_pos[0], enemy_pos[1] - 30)) + + # 處理子彈移動與繪製 + for b in bullets[:]: + if not game_over: + b['x'] += b['dir'] + + color = (255, 200, 0) if b['owner'] == 'me' else (255, 50, 50) + bullet_rect = pygame.Rect(b['x'], b['y'], 10, 10) + pygame.draw.rect(screen, color, bullet_rect) + + # 判斷自己的碰撞框是否被對方的子彈打中 (自己必須活著) + if b['owner'] == 'enemy' and not game_over and my_rect.colliderect(bullet_rect): + my_hp -= 1 + if hit_sound: + hit_sound.play() + + hit_bullet_id = b['id'] + if b in bullets: + bullets.remove(b) + + # 若被打死,進入結算狀態並宣告死亡 + if my_hp <= 0: + game_over = True + enemy_score += 1 + hit_pkg = {'type': 'im_dead', 'hp': 0, 'bullet_id': hit_bullet_id} + print("您已陣亡!等待按下 'R' 鍵重生。") + else: + hit_pkg = {'type': 'hit', 'hp': my_hp, 'bullet_id': hit_bullet_id} + + sock.sendto(json.dumps(hit_pkg).encode(), (TARGET_IP, TARGET_PORT)) + + elif b['x'] < 0 or b['x'] > 800: + if b in bullets: + bullets.remove(b) + + # 如果死亡,蓋上結算文字 + if game_over: + dead_text = big_font.render("YOU DIED", True, (239, 68, 68)) + screen.blit(dead_text, (800//2 - dead_text.get_width()//2, 600//2 - dead_text.get_height()//2 - 20)) + + hint_text = font.render("Press 'R' to Restart", True, (255, 255, 255)) + screen.blit(hint_text, (800//2 - hint_text.get_width()//2, 600//2 + 40)) + + elif not opponent_connected: + # 對手還沒連線的提示畫面 + wait_text = font.render("Waiting for opponent...", True, (255, 200, 0)) + screen.blit(wait_text, (800//2 - wait_text.get_width()//2, 600//2 - wait_text.get_height()//2)) + + pygame.display.flip() + clock.tick(60) + + # ======= [網路區間:發送最新座標] ======= + if not game_over: + pos_pkg = {'type': 'pos', 'x': my_pos[0], 'y': my_pos[1]} + sock.sendto(json.dumps(pos_pkg).encode(), (TARGET_IP, TARGET_PORT)) diff --git "a/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/shoot.wav" "b/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/shoot.wav" new file mode 100644 index 0000000000000000000000000000000000000000..e4b1fe39a70052df01006c8b89a352f6e18b3f3d --- /dev/null +++ "b/projects/DEMO/Student1/\351\233\231\344\272\272\347\253\266\346\212\200\345\240\264/shoot.wav" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:091481354d97c5469a2cd40f16d8fad63904eb4676992c34c1f208fc87a8173c +size 17684 diff --git "a/projects/DEMO/Student2/\347\242\260\346\222\236\347\256\261\346\204\237\346\207\211/main.py" "b/projects/DEMO/Student2/\347\242\260\346\222\236\347\256\261\346\204\237\346\207\211/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..638357d8fd16d14ee6d33834aa3e83dc909fb9af --- /dev/null +++ "b/projects/DEMO/Student2/\347\242\260\346\222\236\347\256\261\346\204\237\346\207\211/main.py" @@ -0,0 +1,141 @@ +import pygame +import json +import socket +import sys + +# === 初始化 === +pygame.init() +screen = pygame.display.set_mode((800, 600)) +clock = pygame.time.Clock() + +# ==== 【TODO 1】網路 Socket 設定(自動判斷 P1 / P2)==== +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.setblocking(False) + +# 嘗試綁定 port 5000,若被佔用則綁定 5001 +# 先啟動的人 = P1 (左側),後啟動的人 = P2 (右側) +try: + sock.bind(("0.0.0.0", 5000)) + MY_PORT = 5000 + TARGET_PORT = 5001 + player_role = "P1" +except OSError: + sock.bind(("0.0.0.0", 5001)) + MY_PORT = 5001 + TARGET_PORT = 5000 + player_role = "P2" + +TARGET_IP = "127.0.0.1" # 測試用 + +# 根據角色自動設定位置與子彈方向 +if player_role == "P1": + my_x, my_y = 100, 300 # P1 在左邊 + enemy_x, enemy_y = 600, 300 + my_bullet_dir = 10 # P1 子彈朝右飛 + enemy_bullet_dir = -10 +else: + my_x, my_y = 600, 300 # P2 在右邊 + enemy_x, enemy_y = 100, 300 + my_bullet_dir = -10 # P2 子彈朝左飛 + enemy_bullet_dir = 10 + +my_hp = 10 +bullets = [] # 儲存畫面上所有的子彈 +bullet_id_counter = 0 # 子彈編號計數器,用來同步雙方畫面 + +pygame.display.set_caption(f"Week 7 - 碰撞判定與血條爭奪戰 [{player_role} | Port {MY_PORT}]") +print(f"遊戲啟動!你是 {player_role}(監聽 port {MY_PORT},發送至 port {TARGET_PORT})") + +while True: + screen.fill((30, 30, 50)) + + # ==== 【TODO 2】接收對手網路封包 ==== + try: + data, addr = sock.recvfrom(1024) + package = json.loads(data.decode()) + if package['type'] == 'pos': + # 這是座標信:就蓋過對方的舊照片座標 + enemy_x = package['x'] + enemy_y = package['y'] + elif package['type'] == 'shoot': + # 這是射擊事件信:馬上在畫面上憑空「生成」一顆對方開火的子彈 + bullets.append({'x': package['x'], 'y': package['y'], 'dir': enemy_bullet_dir, 'owner': 'enemy', 'id': package['id']}) + elif package['type'] == 'hit': + # 這是對方被擊中的通知 + print(f"對手受傷了!對方血量剩下: {package.get('hp', 0)}") + # 對方被我的子彈打中了 → 從我的畫面上移除那顆子彈 + hit_bullet_id = package.get('bullet_id') + if hit_bullet_id is not None: + bullets[:] = [b for b in bullets if not (b['owner'] == 'me' and b['id'] == hit_bullet_id)] + except (BlockingIOError, ConnectionResetError): + # 非阻塞模式下,Linux 拋 BlockingIOError,Windows 拋 ConnectionResetError + pass + except json.JSONDecodeError: + # 防止有人亂傳不是 JSON 格式的垃圾訊息導致閃退 + pass + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + + # 按下空白鍵發射子彈 + if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: + bullet_id_counter += 1 + new_bullet = {'x': my_x, 'y': my_y, 'dir': my_bullet_dir, 'owner': 'me', 'id': bullet_id_counter} + bullets.append(new_bullet) + + # ==== 【TODO 3】通知對方我開槍了 ==== + shoot_package = {'type': 'shoot', 'x': my_x, 'y': my_y, 'id': bullet_id_counter} + # 將這個 package 用 dumps 打包並送出 + sock.sendto(json.dumps(shoot_package).encode(), (TARGET_IP, TARGET_PORT)) + + # 控制移動 + keys = pygame.key.get_pressed() + if keys[pygame.K_w]: my_y -= 5 + if keys[pygame.K_s]: my_y += 5 + + # 繪製自己與對手 (使用 Rect 作為碰撞箱) + my_rect = pygame.Rect(my_x, my_y, 40, 40) + pygame.draw.rect(screen, (59, 130, 246), my_rect) + + enemy_rect = pygame.Rect(enemy_x, enemy_y, 40, 40) + pygame.draw.rect(screen, (16, 185, 129), enemy_rect) + + # 處理並繪製每一顆子彈 + for b in bullets[:]: + # 使子彈移動 + b['x'] += b['dir'] + + # 繪製子彈 + bullet_color = (255, 200, 0) if b['owner'] == 'me' else (255, 50, 50) + bullet_rect = pygame.Rect(b['x'], b['y'], 10, 10) + pygame.draw.rect(screen, bullet_color, bullet_rect) + + # ==== 【TODO 4: P2P 碰撞判定】 ==== + # 若這是一顆「對方」射過來的子彈,且「打中我了」 + if b['owner'] == 'enemy' and my_rect.colliderect(bullet_rect): + # 1. my_hp -= 1 + my_hp -= 1 + # 2. bullets.remove(b) + hit_bullet_id = b['id'] + if b in bullets: + bullets.remove(b) + # 3. 傳送 {'type': 'hit', 'hp': my_hp, 'bullet_id': ...} 告訴對方我受傷了 + # 帶上 bullet_id 讓對方也能從他的畫面移除這顆子彈 + hit_msg = {'type': 'hit', 'hp': my_hp, 'bullet_id': hit_bullet_id} + sock.sendto(json.dumps(hit_msg).encode(), (TARGET_IP, TARGET_PORT)) + print(f"我被擊中了!剩餘血量: {my_hp}") + + # 飛出邊界則刪除 + elif b['x'] < 0 or b['x'] > 800: + if b in bullets: + bullets.remove(b) + + pygame.display.flip() + clock.tick(60) + + # ==== 【TODO 5】傳送座標 ==== + # 每回合結束,把自己的新座標透過封包 {'type': 'pos', 'x': xxx, 'y': xxx} 送過去 + pos_package = {'type': 'pos', 'x': my_x, 'y': my_y} + sock.sendto(json.dumps(pos_package).encode(), (TARGET_IP, TARGET_PORT)) diff --git "a/projects/DEMO/Student2/\347\265\202\347\253\257\346\251\237\346\211\223\345\255\227\346\270\254\350\251\246/main.py" "b/projects/DEMO/Student2/\347\265\202\347\253\257\346\251\237\346\211\223\345\255\227\346\270\254\350\251\246/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..3515087db2d4255df47335fde2a64ac880146fd7 --- /dev/null +++ "b/projects/DEMO/Student2/\347\265\202\347\253\257\346\251\237\346\211\223\345\255\227\346\270\254\350\251\246/main.py" @@ -0,0 +1,16 @@ +import sys +import time + +print("[測試] 互動式終端機輸入測試開始。") +print("系統正在等待輸入...") + +try: + name = input("請輸入你的名字 (Name): ") + print(f"你好,{name}!") + + age = input("請輸入你的年齡 (Age): ") + print(f"確認資訊:{name} 今年 {age} 歲。") +except Exception as e: + print(f"執行時發生錯誤: {e}") + +print("[測試] 測試成功結束。") diff --git "a/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/background.jpg" "b/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/background.jpg" new file mode 100644 index 0000000000000000000000000000000000000000..7b98b815bc295e37f93ad72b68f7990170aa05d4 --- /dev/null +++ "b/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/background.jpg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b4550b2073dfd8267a1d2fc94620e82aada100ab95114d44a62eef8e40e3e7e +size 220630 diff --git "a/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/hit.wav" "b/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/hit.wav" new file mode 100644 index 0000000000000000000000000000000000000000..d138a2494493124e0200999b18a4c4315b860a72 --- /dev/null +++ "b/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/hit.wav" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55440a99e00bb9de3a53bcaa709c83d412001f78dcf7103036ec02a5057c2fdd +size 13274 diff --git "a/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/main.py" "b/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..fb5ab98cceb6d5ed5bb09cd894de67737e0e9b48 --- /dev/null +++ "b/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/main.py" @@ -0,0 +1,523 @@ +""" +Week 8 + 9 + 10 - 串接 AI 視覺控制的網路競技場 (Complete 完整版) +============================================================= +本腳本為獨立測試版,將 Week 8 的 UDP 雙人對戰競技場與 Week 9-10 的 MediaPipe 手勢偵測進行完美結合: + 1. 使用食指尖 (Landmark 8) 移動方塊 (支援 WASD 鍵盤作為備用 fallback)。 + 2. 使用大拇指 (Landmark 4) 與食指尖的 Pinch (捏合) 來發射子彈 (支援空白鍵作為備用)。 + 3. 右下角顯示「AI 攝影機全息投影」預覽視窗。 + 4. 死亡時,食指移至畫面中央的「Pinch to Respawn」虛擬按鈕並捏合,即可重生。 + +操作方式: + - 同一台電腦測試:開兩個終端機分別執行此程式,先開的為 P1 (左),後開的為 P2 (右)。 + - 將食指伸到鏡頭前即可控制方塊;捏合大拇指與食指即可射擊。 + - 按鍵盤 Q 或點擊視窗關閉鈕即可結束。 +""" +import pygame +import cv2 +import mediapipe as mp +from mediapipe.tasks import python +from mediapipe.tasks.python import vision +import socket +import json +import sys +import os +import math + +# ============================================ +# 1. 初始化 Pygame & 視窗設定 +# ============================================ +pygame.init() +pygame.mixer.init() +WIDTH, HEIGHT = 800, 600 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +clock = pygame.time.Clock() + +# ============================================ +# 2. 網路設定 (自動判斷 P1 / P2) +# ============================================ +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.setblocking(False) + +try: + sock.bind(("0.0.0.0", 5000)) + MY_PORT = 5000 + TARGET_PORT = 5001 + player_role = "P1" +except OSError: + sock.bind(("0.0.0.0", 5001)) + MY_PORT = 5001 + TARGET_PORT = 5000 + player_role = "P2" + +TARGET_IP = "127.0.0.1" # 若要跨電腦連線,請修改為對方的 IP +pygame.display.set_caption(f"AI 雙人對戰競技場 [{player_role} | Port {MY_PORT}]") + +# ============================================ +# 3. 初始化 OpenCV 攝影機與 MediaPipe 手勢偵測 +# ============================================ +print("正在啟動攝影機並載入 MediaPipe 模型...") +cap = cv2.VideoCapture(0) +has_camera = True +if not cap.isOpened(): + print("⚠️ 警告:無法開啟攝影機!將自動切換為鍵盤控制模式 (WASD + Space)。") + has_camera = False + +# 載入 MediaPipe 模型 +detector = None +if has_camera: + try: + # 尋找與此程式同資料夾的 hand_landmarker.task + model_path = os.path.join(os.path.dirname(__file__), 'hand_landmarker.task') + if not os.path.exists(model_path): + # 若不在同資料夾,嘗試在上一層找 (或相對路徑) + model_path = 'hand_landmarker.task' + + base_options = python.BaseOptions(model_asset_path=model_path) + options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) + detector = vision.HandLandmarker.create_from_options(options) + print("✅ MediaPipe 模型載入成功!") + except Exception as e: + print(f"⚠️ 警告:MediaPipe 模型載入失敗 ({e})。將切換為鍵盤控制模式。") + has_camera = False + +# ============================================ +# 4. 遊戲變數 & 視覺設計系統 +# ============================================ +# 科幻感霓虹色彩系統 +COLOR_BG = (11, 15, 25) # 深藍太空底色 +COLOR_P1 = (0, 240, 255) # 霓虹青 (P1) +COLOR_P2 = (57, 255, 20) # 霓虹綠 (P2) +COLOR_BULLET_ME = (255, 110, 0) # 烈焰橘 (我方子彈) +COLOR_BULLET_ENEMY = (255, 0, 128) # 魅惑紅 (對方子彈) +COLOR_WHITE = (255, 255, 255) +COLOR_MUTED = (100, 110, 130) + +# 字型 +font_small = pygame.font.SysFont("monospace", 18) +font_med = pygame.font.SysFont("monospace", 28) +font_large = pygame.font.SysFont("monospace", 64) + +# 載入 Week 8 素材與音效 +try: + bg_img = pygame.image.load("background.jpg") + bg_img = pygame.transform.scale(bg_img, (WIDTH, HEIGHT)) +except FileNotFoundError: + bg_img = None + +try: + shoot_sound = pygame.mixer.Sound("shoot.wav") +except FileNotFoundError: + shoot_sound = None + +try: + hit_sound = pygame.mixer.Sound("hit.wav") +except FileNotFoundError: + hit_sound = None + +# 玩家狀態初始化 +if player_role == "P1": + my_pos = [100, 300] + enemy_pos = [600, 300] + my_bullet_dir = 10 + enemy_bullet_dir = -10 + my_color = COLOR_P1 + enemy_color = COLOR_P2 +else: + my_pos = [600, 300] + enemy_pos = [100, 300] + my_bullet_dir = -10 + enemy_bullet_dir = 10 + my_color = COLOR_P2 + enemy_color = COLOR_P1 + +my_hp = 10 +enemy_hp = 10 +my_score = 0 +enemy_score = 0 +bullets = [] +bullet_id_counter = 0 +game_over = False +opponent_connected = False + +# AI 視覺手勢控制變數 +finger_x, finger_y = my_pos[0] + 20, my_pos[1] + 20 +thumb_x, thumb_y = my_pos[0] + 20, my_pos[1] + 20 +is_pinching = False +was_pinching = False +PINCH_THRESHOLD = 35 # 兩指距離小於 35px 判定為 Pinch +hand_detected = False +last_valid_dist = 999 + +# 虛擬按鈕設定 (用於遊戲結束時手勢點擊重生) +restart_btn_rect = pygame.Rect(300, 380, 200, 50) + +# ============================================ +# 5. 遊戲主迴圈 +# ============================================ +print("\n--- 遊戲已啟動 ---") +print("提示:若偵測到手掌,方塊會自動跟隨食指尖移動。") +print("提示:捏合大拇指與食指即可射擊。") +print("提示:鍵盤 WASD / 鍵盤 Space 依然可做為緊急手動控制。") +print("----------------") + +running = True +while running: + # 畫背景 + if bg_img: + screen.blit(bg_img, (0, 0)) + else: + screen.fill(COLOR_BG) + # 繪製裝飾性格線 (Grid),提升科幻感 + for x in range(0, WIDTH, 40): + pygame.draw.line(screen, (20, 30, 50), (x, 0), (x, HEIGHT), 1) + for y in range(0, HEIGHT, 40): + pygame.draw.line(screen, (20, 30, 50), (0, y), (WIDTH, y), 1) + + # ============================================ + # 5A. 讀取攝影機並進行 MediaPipe 手勢分析 + # ============================================ + cv_preview_surf = None + if has_camera and detector: + success, img = cap.read() + if success: + img = cv2.flip(img, 1) # 鏡像翻轉,控制更直覺 + img_h, img_w, _ = img.shape + + # 轉換為 RGB 格式供 MediaPipe 偵測 + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + if results.hand_landmarks: + hand_detected = True + for hand_lms in results.hand_landmarks: + # 取得食指尖 (Landmark 8) + lm8 = hand_lms[8] + finger_x = int(lm8.x * WIDTH) + finger_y = int(lm8.y * HEIGHT) + + # 取得大拇指尖 (Landmark 4) + lm4 = hand_lms[4] + thumb_x = int(lm4.x * WIDTH) + thumb_y = int(lm4.y * HEIGHT) + + # 計算食指與大拇指之距離 + last_valid_dist = math.hypot(finger_x - thumb_x, finger_y - thumb_y) + is_pinching = last_valid_dist < PINCH_THRESHOLD + + # 將方塊中心移動至食指尖位置 (只有在活著時才移動) + if not game_over: + my_pos[0] = max(0, min(WIDTH - 40, finger_x - 20)) + my_pos[1] = max(0, min(HEIGHT - 40, finger_y - 20)) + + # 在相機影格上畫出關鍵點 (供角落 HUD 顯示) + # 食指尖 + cv2.circle(img, (int(lm8.x * img_w), int(lm8.y * img_h)), 8, (255, 0, 0), -1) + # 大拇指 + cv2.circle(img, (int(lm4.x * img_w), int(lm4.y * img_h)), 8, (0, 0, 255), -1) + # 連線 + line_color = (0, 255, 0) if is_pinching else (0, 0, 255) + cv2.line(img, (int(lm4.x * img_w), int(lm4.y * img_h)), + (int(lm8.x * img_w), int(lm8.y * img_h)), line_color, 2) + else: + hand_detected = False + is_pinching = False + + # 製作 HUD 迷你鏡頭預覽影像 (縮小至 160x120) + small_img = cv2.resize(img, (160, 120)) + small_img_rgb = cv2.cvtColor(small_img, cv2.COLOR_BGR2RGB) + cv_preview_surf = pygame.image.frombuffer(small_img_rgb.tobytes(), (160, 120), "RGB") + + # ============================================ + # 5B. 接收 UDP 封包 (處理網路連線數據) + # ============================================ + while True: + try: + data, addr = sock.recvfrom(1024) + opponent_connected = True + pkg = json.loads(data.decode()) + + if pkg['type'] == 'pos': + enemy_pos[0] = pkg['x'] + enemy_pos[1] = pkg['y'] + + elif pkg['type'] == 'shoot': + bullets.append({ + 'x': pkg['x'], 'y': pkg['y'], + 'dir': enemy_bullet_dir, 'owner': 'enemy', 'id': pkg['id'] + }) + if shoot_sound: + shoot_sound.play() + + elif pkg['type'] == 'hit': + enemy_hp = pkg.get('hp', 10) + hit_bullet_id = pkg.get('bullet_id') + if hit_bullet_id is not None: + bullets[:] = [b for b in bullets if not (b['owner'] == 'me' and b['id'] == hit_bullet_id)] + + elif pkg['type'] == 'im_dead': + enemy_hp = pkg.get('hp', 0) + my_score += 1 + hit_bullet_id = pkg.get('bullet_id') + if hit_bullet_id is not None: + bullets[:] = [b for b in bullets if not (b['owner'] == 'me' and b['id'] == hit_bullet_id)] + + except (BlockingIOError, ConnectionResetError): + break + except json.JSONDecodeError: + pass + + # ============================================ + # 5C. 處理事件與鍵盤輸入 (手鍵雙軌備用) + # ============================================ + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_q: + running = False + + # 空白鍵手動射擊 + if event.key == pygame.K_SPACE and not game_over and opponent_connected: + bullet_id_counter += 1 + bullets.append({ + 'x': my_pos[0] + 20, 'y': my_pos[1] + 20, + 'dir': my_bullet_dir, 'owner': 'me', 'id': bullet_id_counter + }) + shoot_pkg = {'type': 'shoot', 'x': my_pos[0] + 20, 'y': my_pos[1] + 20, 'id': bullet_id_counter} + sock.sendto(json.dumps(shoot_pkg).encode(), (TARGET_IP, TARGET_PORT)) + if shoot_sound: + shoot_sound.play() + + # 按 R 鍵重生 + if event.key == pygame.K_r and game_over: + game_over = False + my_hp = 10 + my_pos = [100, 300] if player_role == "P1" else [600, 300] + respawn_pkg = {'type': 'hit', 'hp': my_hp} + sock.sendto(json.dumps(respawn_pkg).encode(), (TARGET_IP, TARGET_PORT)) + + # Fallback 鍵盤 WASD 移動 + if not game_over: + keys = pygame.key.get_pressed() + if keys[pygame.K_w]: my_pos[1] -= 5 + if keys[pygame.K_s]: my_pos[1] += 5 + if keys[pygame.K_a]: my_pos[0] -= 5 + if keys[pygame.K_d]: my_pos[0] += 5 + + # 邊界限制 + my_pos[0] = max(0, min(WIDTH - 40, my_pos[0])) + my_pos[1] = max(0, min(HEIGHT - 40, my_pos[1])) + + # ============================================ + # 5D. 邊緣觸發 (手勢動作判定) + # ============================================ + if is_pinching and not was_pinching: + # 狀態1:活著 + 對手已連線 = 捏合射擊 + if not game_over and opponent_connected: + bullet_id_counter += 1 + bullets.append({ + 'x': my_pos[0] + 20, 'y': my_pos[1] + 20, + 'dir': my_bullet_dir, 'owner': 'me', 'id': bullet_id_counter + }) + shoot_pkg = {'type': 'shoot', 'x': my_pos[0] + 20, 'y': my_pos[1] + 20, 'id': bullet_id_counter} + sock.sendto(json.dumps(shoot_pkg).encode(), (TARGET_IP, TARGET_PORT)) + if shoot_sound: + shoot_sound.play() + print("💥 AI 手勢射擊!") + + # 狀態2:已陣亡 = 捏合點擊畫面中央的重生按鈕 + elif game_over: + finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10) + if finger_rect.colliderect(restart_btn_rect): + game_over = False + my_hp = 10 + my_pos = [100, 300] if player_role == "P1" else [600, 300] + respawn_pkg = {'type': 'hit', 'hp': my_hp} + sock.sendto(json.dumps(respawn_pkg).encode(), (TARGET_IP, TARGET_PORT)) + print("♻️ AI 虛擬按鈕重生成功!") + + was_pinching = is_pinching + + # ============================================ + # 5E. 繪圖區間 (Pygame 繪圖與 HUD 設計) + # ============================================ + my_rect = pygame.Rect(my_pos[0], my_pos[1], 40, 40) + enemy_rect = pygame.Rect(enemy_pos[0], enemy_pos[1], 40, 40) + + # 1. 繪製雷射引導線 (科技感效果) + if hand_detected and not game_over: + # 雷射瞄準虛線 (玩家指向射擊前方) + laser_color = (my_color[0], my_color[1], my_color[2], 80) + laser_surface = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) + start_pt = (my_pos[0] + 20, my_pos[1] + 20) + end_pt = (WIDTH if my_bullet_dir > 0 else 0, my_pos[1] + 20) + + # 畫虛線 + distance_line = abs(end_pt[0] - start_pt[0]) + step = 15 + for offset in range(0, int(distance_line), step * 2): + curr_x = start_pt[0] + (offset if my_bullet_dir > 0 else -offset) + pygame.draw.line(laser_surface, (my_color[0], my_color[1], my_color[2], 120), + (curr_x, start_pt[1]), + (curr_x + (step if my_bullet_dir > 0 else -step), start_pt[1]), 2) + screen.blit(laser_surface, (0, 0)) + + # 2. 畫自己的人物 (加外發光邊框) + if not game_over: + pygame.draw.rect(screen, my_color, my_rect) + pygame.draw.rect(screen, COLOR_WHITE, my_rect, 2) # 白色框 + + # 3. 畫對方的人物 (對手血量大於 0 且已連線) + if opponent_connected and enemy_hp > 0: + pygame.draw.rect(screen, enemy_color, enemy_rect) + pygame.draw.rect(screen, COLOR_WHITE, enemy_rect, 2) + + # 4. 畫計分板 + score_text = font_med.render(f"P1 (BLUE): {my_score if player_role == 'P1' else enemy_score} | P2 (GREEN): {enemy_score if player_role == 'P1' else my_score}", True, COLOR_WHITE) + screen.blit(score_text, (WIDTH // 2 - score_text.get_width() // 2, 20)) + + # 5. 畫血條 + if not game_over: + # 我的血條 (綠色/紅色比例) + pygame.draw.rect(screen, (100, 0, 0), (my_pos[0] - 10, my_pos[1] - 15, 60, 6)) + pygame.draw.rect(screen, (0, 255, 100), (my_pos[0] - 10, my_pos[1] - 15, 6 * my_hp, 6)) + hp_lbl = font_small.render(f"HP {my_hp}", True, COLOR_WHITE) + screen.blit(hp_lbl, (my_pos[0] - 10, my_pos[1] - 32)) + + if opponent_connected and enemy_hp > 0: + # 對方的血條 + pygame.draw.rect(screen, (100, 0, 0), (enemy_pos[0] - 10, enemy_pos[1] - 15, 60, 6)) + pygame.draw.rect(screen, (0, 255, 100), (enemy_pos[0] - 10, enemy_pos[1] - 15, 6 * enemy_hp, 6)) + ehp_lbl = font_small.render(f"HP {enemy_hp}", True, COLOR_WHITE) + screen.blit(ehp_lbl, (enemy_pos[0] - 10, enemy_pos[1] - 32)) + + # 6. 處理子彈移動、邊界與碰撞 + for b in bullets[:]: + if not game_over: + b['x'] += b['dir'] + + # 繪製子彈 + bullet_rect = pygame.Rect(b['x'] - 5, b['y'] - 5, 10, 10) + bullet_color = COLOR_BULLET_ME if b['owner'] == 'me' else COLOR_BULLET_ENEMY + + # 繪製亮色核心 + pygame.draw.circle(screen, bullet_color, (b['x'], b['y']), 5) + # 外層微光效果 + glow_surf = pygame.Surface((16, 16), pygame.SRCALPHA) + pygame.draw.circle(glow_surf, (bullet_color[0], bullet_color[1], bullet_color[2], 80), (8, 8), 8) + screen.blit(glow_surf, (b['x'] - 8, b['y'] - 8)) + + # 偵測自己被對方的子彈打中 + if b['owner'] == 'enemy' and not game_over and my_rect.colliderect(bullet_rect): + my_hp -= 1 + if hit_sound: + hit_sound.play() + + hit_bullet_id = b['id'] + if b in bullets: + bullets.remove(b) + + if my_hp <= 0: + game_over = True + enemy_score += 1 + hit_pkg = {'type': 'im_dead', 'hp': 0, 'bullet_id': hit_bullet_id} + print("💥 您已陣亡!等待手勢或鍵盤 R 鍵重生。") + else: + hit_pkg = {'type': 'hit', 'hp': my_hp, 'bullet_id': hit_bullet_id} + + sock.sendto(json.dumps(hit_pkg).encode(), (TARGET_IP, TARGET_PORT)) + + # 超出邊界刪除 + elif b['x'] < 0 or b['x'] > WIDTH: + if b in bullets: + bullets.remove(b) + + # 7. 繪製手勢雷射游標與連接線 (當偵測到手掌且未死亡時) + if hand_detected and not game_over: + # 大拇指紫色點 + pygame.draw.circle(screen, (139, 92, 246), (thumb_x, thumb_y), 6) + + # 連接線 (捏合變綠,放開變紅) + line_color = (57, 255, 20) if is_pinching else (255, 50, 50) + pygame.draw.line(screen, line_color, (thumb_x, thumb_y), (finger_x, finger_y), 2) + + # 食指游標 + pygame.draw.circle(screen, line_color, (finger_x, finger_y), 10, 2) + pygame.draw.circle(screen, COLOR_WHITE, (finger_x, finger_y), 3) + + # 8. 繪製角落全息相機 HUD (Holographic Camera Feed) + if cv_preview_surf: + hud_x, hud_y = WIDTH - 180, HEIGHT - 140 + # 繪製科技感發光外框 + pygame.draw.rect(screen, (30, 80, 150), (hud_x - 4, hud_y - 4, 168, 128), 2) + pygame.draw.rect(screen, COLOR_BG, (hud_x - 2, hud_y - 2, 164, 124)) + # 貼上影像 + screen.blit(cv_preview_surf, (hud_x, hud_y)) + + # 相機狀態標籤 + status_color = (57, 255, 20) if hand_detected else (255, 200, 0) + status_text = "AI FEED [ACTIVE]" if hand_detected else "NO HAND DETECTED" + hud_label = font_small.render(status_text, True, status_color) + screen.blit(hud_label, (hud_x, hud_y - 22)) + + # 9. 顯示死亡畫面與虛擬重生按鈕 + if game_over: + # 遮罩半透明層 + overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) + overlay.fill((0, 0, 0, 180)) + screen.blit(overlay, (0, 0)) + + dead_text = font_large.render("YOU DIED", True, (239, 68, 68)) + screen.blit(dead_text, (WIDTH // 2 - dead_text.get_width() // 2, HEIGHT // 2 - 100)) + + # 繪製虛擬按鈕 + btn_border_color = (0, 255, 100) if is_pinching else COLOR_WHITE + # 若食指滑過按鈕變色 (Hover效果) + finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10) + is_hover = finger_rect.colliderect(restart_btn_rect) + btn_bg_color = (30, 60, 40) if is_hover else (30, 30, 40) + + pygame.draw.rect(screen, btn_bg_color, restart_btn_rect, border_radius=8) + pygame.draw.rect(screen, btn_border_color, restart_btn_rect, 2, border_radius=8) + + btn_text = font_small.render("Pinch to Respawn", True, COLOR_WHITE) + screen.blit(btn_text, (restart_btn_rect.centerx - btn_text.get_width() // 2, + restart_btn_rect.centery - btn_text.get_height() // 2)) + + hint_text = font_small.render("or press keyboard 'R' to Respawn", True, COLOR_MUTED) + screen.blit(hint_text, (WIDTH // 2 - hint_text.get_width() // 2, HEIGHT // 2 + 100)) + + # 死亡時依然繪製手勢指引,讓玩家看得到自己能不能點擊按鈕 + if hand_detected: + pygame.draw.circle(screen, (139, 92, 246), (thumb_x, thumb_y), 6) + line_color = (57, 255, 20) if is_pinching else (255, 50, 50) + pygame.draw.line(screen, line_color, (thumb_x, thumb_y), (finger_x, finger_y), 2) + pygame.draw.circle(screen, line_color, (finger_x, finger_y), 10, 2) + pygame.draw.circle(screen, COLOR_WHITE, (finger_x, finger_y), 3) + + elif not opponent_connected: + # 等待對手連線提示 + wait_text = font_med.render("WAITING FOR OPPONENT...", True, (255, 200, 0)) + screen.blit(wait_text, (WIDTH // 2 - wait_text.get_width() // 2, HEIGHT // 2 - 20)) + + ip_info = font_small.render(f"My Port: {MY_PORT} | Targets: {TARGET_IP}:{TARGET_PORT}", True, COLOR_MUTED) + screen.blit(ip_info, (WIDTH // 2 - ip_info.get_width() // 2, HEIGHT // 2 + 20)) + + pygame.display.flip() + clock.tick(60) + + # ============================================ + # 5F. 傳送座標給對手 (每一影格同步) + # ============================================ + if not game_over: + pos_pkg = {'type': 'pos', 'x': my_pos[0], 'y': my_pos[1]} + sock.sendto(json.dumps(pos_pkg).encode(), (TARGET_IP, TARGET_PORT)) + +# 清理資源 +if has_camera: + cap.release() +cv2.destroyAllWindows() +pygame.quit() +sys.exit() diff --git "a/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/shoot.wav" "b/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/shoot.wav" new file mode 100644 index 0000000000000000000000000000000000000000..e4b1fe39a70052df01006c8b89a352f6e18b3f3d --- /dev/null +++ "b/projects/DEMO/Student2/\350\246\226\350\250\212\351\217\241\351\240\255\347\253\266\346\212\200\345\240\264/shoot.wav" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:091481354d97c5469a2cd40f16d8fad63904eb4676992c34c1f208fc87a8173c +size 17684 diff --git "a/projects/DEMO/Student3/\346\211\213\345\213\242\347\211\271\345\276\265\345\210\206\351\241\236/hand_landmarker.task" "b/projects/DEMO/Student3/\346\211\213\345\213\242\347\211\271\345\276\265\345\210\206\351\241\236/hand_landmarker.task" new file mode 100644 index 0000000000000000000000000000000000000000..5ecab741879892d97c2f90bbf03bf55d7213db7c --- /dev/null +++ "b/projects/DEMO/Student3/\346\211\213\345\213\242\347\211\271\345\276\265\345\210\206\351\241\236/hand_landmarker.task" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbc2a30080c3c557093b5ddfc334698132eb341044ccee322ccf8bcf3607cde1 +size 7819105 diff --git "a/projects/DEMO/Student3/\346\211\213\345\213\242\347\211\271\345\276\265\345\210\206\351\241\236/labels.txt" "b/projects/DEMO/Student3/\346\211\213\345\213\242\347\211\271\345\276\265\345\210\206\351\241\236/labels.txt" new file mode 100644 index 0000000000000000000000000000000000000000..1bfbfd2815550865cdac20d9f5781d908f532c4d --- /dev/null +++ "b/projects/DEMO/Student3/\346\211\213\345\213\242\347\211\271\345\276\265\345\210\206\351\241\236/labels.txt" @@ -0,0 +1,3 @@ +0 背景 +1 喝水中 +2 其他動作 diff --git "a/projects/DEMO/Student3/\346\211\213\345\213\242\347\211\271\345\276\265\345\210\206\351\241\236/main.py" "b/projects/DEMO/Student3/\346\211\213\345\213\242\347\211\271\345\276\265\345\210\206\351\241\236/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..169e69414c0c63a7d4f3dd68f63ab354229096f5 --- /dev/null +++ "b/projects/DEMO/Student3/\346\211\213\345\213\242\347\211\271\345\276\265\345\210\206\351\241\236/main.py" @@ -0,0 +1,212 @@ +""" +Week 11 - Teachable Machine 影像分類 + AI 音效觸發 (Complete 完整版) +===================================================================== +本週目標: + 1. 載入 Teachable Machine 訓練好的 Keras 模型 (.h5) + 2. 使用攝影機即時推論,辨識畫面屬於哪一個類別 + 3. 當偵測到特定類別 (如「喝水中」) 時觸發 Pygame 音效 + 4. 使用 Boolean Flag 防止音效重複播放(邊緣觸發) + +前置準備: + - 從 Teachable Machine 匯出 keras_model.h5 和 labels.txt + - 把兩個檔案放在跟這個 .py 檔案同一個資料夾裡 + - pip install tensorflow opencv-python numpy pygame +""" +import pygame +import cv2 +import numpy as np +import sys + +from keras.models import load_model + +# ============================================ +# 1. 初始化 Pygame +# ============================================ +pygame.init() +pygame.mixer.init() +WIDTH, HEIGHT = 800, 600 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Week 11 - AI 影像分類器 🧠🔮") +clock = pygame.time.Clock() + +font = pygame.font.SysFont(None, 48) +small_font = pygame.font.SysFont(None, 28) +bar_font = pygame.font.SysFont(None, 22) + +# ============================================ +# 2. 載入模型與標籤 +# ============================================ +try: + model = load_model("keras_model.h5", compile=False) + print("✅ 模型載入成功!") +except Exception as e: + print(f"❌ 模型載入失敗:{e}") + print(" 請確認 keras_model.h5 是否在同一個資料夾中") + pygame.quit() + sys.exit() + +try: + labels = open("labels.txt", "r").readlines() + print(f"✅ 載入 {len(labels)} 個分類標籤:{[l.strip() for l in labels]}") +except FileNotFoundError: + print("❌ 找不到 labels.txt!請確認檔案是否在同一個資料夾中") + pygame.quit() + sys.exit() + +# ============================================ +# 3. 攝影機 +# ============================================ +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("❌ 無法開啟攝影機,請改用影片檔案") + pygame.quit() + sys.exit() + +# ============================================ +# 4. 遊戲變數 +# ============================================ +current_class = "等待分析..." +confidence = 0.0 +prediction = None +frame_counter = 0 + +# 邊緣觸發:紀錄上一個狀態 +is_drinking = False # 根據你的類別修改 + +# 載入音效 +try: + trigger_sound = pygame.mixer.Sound("bubble.wav") + print("✅ 音效載入成功") +except FileNotFoundError: + trigger_sound = None + print("⚠️ 找不到音效檔案 bubble.wav,將不播放音效") + +# 背景顏色(根據辨識結果切換) +bg_color = (20, 20, 35) +trigger_flash = 0 # 觸發時的閃爍計數器 + +# 信心度條形圖的顏色 +bar_colors = [ + (34, 197, 94), # 綠色 + (59, 130, 246), # 藍色 + (245, 158, 11), # 黃色 + (239, 68, 68), # 紅色 + (139, 92, 246), # 紫色 +] + +# ============================================ +# 5. 主迴圈 +# ============================================ +running = True +while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # --- 讀取攝影機 --- + success, img = cap.read() + if not success: + cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + continue + + img = cv2.flip(img, 1) + + # --- 每 5 幀做一次推論(避免畫面大卡)--- + frame_counter += 1 + if frame_counter % 5 == 0: + try: + # Keras 要求的影像預處理(這三行請死背!) + img_resized = cv2.resize(img, (224, 224)) + img_array = np.asarray(img_resized, dtype=np.float32).reshape(1, 224, 224, 3) + img_array = (img_array / 127.5) - 1 + + # 前饋推論:取得每個類別的機率 + prediction = model.predict(img_array, verbose=0) + index = np.argmax(prediction) + current_class = labels[index].strip() + confidence = prediction[0][index] + except Exception as e: + print(f"推論出錯:{e}") + + # --- 邊緣觸發 + 動作回饋 --- + # 這裡假設第一個非背景的類別叫做「喝水中」 + # 請根據你自己的 labels.txt 修改判斷條件! + if current_class != labels[0].strip() and confidence > 0.90: + if not is_drinking: + # 狀態剛從「背景」切換到「動作」的瞬間 + if trigger_sound: + trigger_sound.play() + trigger_flash = 15 # 觸發閃爍 15 幀 + print(f"🥤 偵測到動作:{current_class}!信心度: {confidence:.1%}") + is_drinking = True + bg_color = (20, 50, 35) # 深綠背景 + else: + is_drinking = False + bg_color = (20, 20, 35) # 回到預設 + + # 閃爍效果倒數 + if trigger_flash > 0: + trigger_flash -= 1 + + # --- 繪製畫面 --- + # 背景(觸發時閃爍) + if trigger_flash > 0 and trigger_flash % 2 == 0: + screen.fill((40, 80, 50)) + else: + screen.fill(bg_color) + + # 辨識結果大字 + result_color = (50, 255, 50) if is_drinking else (255, 255, 255) + class_text = font.render(f"{current_class}", True, result_color) + screen.blit(class_text, (WIDTH // 2 - class_text.get_width() // 2, 60)) + + conf_text = small_font.render(f"Confidence: {confidence:.1%}", True, (100, 200, 255)) + screen.blit(conf_text, (WIDTH // 2 - conf_text.get_width() // 2, 120)) + + # --- 信心度條形圖 --- + if prediction is not None: + bar_y_start = 180 + bar_height = 30 + bar_gap = 12 + bar_max_width = 500 + bar_x = 150 + + for i, prob in enumerate(prediction[0]): + y = bar_y_start + i * (bar_height + bar_gap) + label_name = labels[i].strip() if i < len(labels) else f"Class {i}" + color = bar_colors[i % len(bar_colors)] + + # 標籤名稱 + label_text = bar_font.render(label_name, True, (200, 200, 200)) + screen.blit(label_text, (bar_x - label_text.get_width() - 10, y + 5)) + + # 背景軌道 + pygame.draw.rect(screen, (40, 40, 55), (bar_x, y, bar_max_width, bar_height), border_radius=6) + + # 填充條 + fill_width = int(prob * bar_max_width) + if fill_width > 0: + pygame.draw.rect(screen, color, (bar_x, y, fill_width, bar_height), border_radius=6) + + # 百分比文字 + pct_text = bar_font.render(f"{prob:.1%}", True, (255, 255, 255)) + text_x = bar_x + fill_width + 8 if fill_width < bar_max_width - 60 else bar_x + fill_width - 55 + screen.blit(pct_text, (text_x, y + 5)) + + # 標記最高的那一條 + if i == np.argmax(prediction): + pygame.draw.rect(screen, color, (bar_x - 4, y, 4, bar_height), border_radius=2) + + # 底部狀態列 + status_text = small_font.render( + f"Frame: {frame_counter} | Predict every 5 frames | FPS: {clock.get_fps():.0f}", + True, (80, 80, 100) + ) + screen.blit(status_text, (WIDTH // 2 - status_text.get_width() // 2, HEIGHT - 35)) + + pygame.display.update() + clock.tick(30) + +cap.release() +pygame.quit() +sys.exit() diff --git "a/projects/DEMO/Student3/\346\211\213\351\203\250\350\276\250\350\255\230/hand_landmarker.task" "b/projects/DEMO/Student3/\346\211\213\351\203\250\350\276\250\350\255\230/hand_landmarker.task" new file mode 100644 index 0000000000000000000000000000000000000000..5ecab741879892d97c2f90bbf03bf55d7213db7c --- /dev/null +++ "b/projects/DEMO/Student3/\346\211\213\351\203\250\350\276\250\350\255\230/hand_landmarker.task" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbc2a30080c3c557093b5ddfc334698132eb341044ccee322ccf8bcf3607cde1 +size 7819105 diff --git "a/projects/DEMO/Student3/\346\211\213\351\203\250\350\276\250\350\255\230/main.py" "b/projects/DEMO/Student3/\346\211\213\351\203\250\350\276\250\350\255\230/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..8846faa5292a8e43f82da685adf36938d138796a --- /dev/null +++ "b/projects/DEMO/Student3/\346\211\213\351\203\250\350\276\250\350\255\230/main.py" @@ -0,0 +1,58 @@ +import cv2 +import mediapipe as mp +from mediapipe.tasks import python +from mediapipe.tasks.python import vision +import os + +# ============================================ +# Week 9 - Step 2: MediaPipe 手部追蹤 (Tasks API) +# ============================================ + +# 初始化 MediaPipe Hand Landmarker +model_path = os.path.join(os.path.dirname(__file__), 'hand_landmarker.task') +base_options = python.BaseOptions(model_asset_path=model_path) +options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) +detector = vision.HandLandmarker.create_from_options(options) + +cap = cv2.VideoCapture(0) + +while True: + success, img = cap.read() + if not success: + break + + img = cv2.flip(img, 1) + + # 1. OpenCV 的 BGR 轉為 RGB + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + # 2. 將 Numpy 陣列轉為 MediaPipe Image 物件 + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + + # 3. 進行偵測 + results = detector.detect(mp_image) + + # 4. 如果有偵測到手,把 21 個關鍵點都畫出來 + if results.hand_landmarks: + for hand_lms in results.hand_landmarks: + h, w, c = img.shape + + # 迴圈畫出 21 個節點 + for i, lm in enumerate(hand_lms): + cx, cy = int(lm.x * w), int(lm.y * h) + + # 畫出小綠點標示每個關節 + cv2.circle(img, (cx, cy), 5, (0, 255, 0), -1) + + # 特別把食指尖 (Landmark 8) 標示為大紅點,並印出座標 + if i == 8: + cv2.circle(img, (cx, cy), 12, (0, 0, 255), -1) + print(f"食指(8) 座標:({cx}, {cy})") + + cv2.imshow("Hand Tracking", img) + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + +cap.release() +cv2.destroyAllWindows() diff --git "a/projects/DEMO/Student4/\351\200\243\347\267\232\345\220\214\346\255\245\347\216\251\345\256\266/main.py" "b/projects/DEMO/Student4/\351\200\243\347\267\232\345\220\214\346\255\245\347\216\251\345\256\266/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..995492160e7abf19e5b1abb0ec3755c8affb6183 --- /dev/null +++ "b/projects/DEMO/Student4/\351\200\243\347\267\232\345\220\214\346\255\245\347\216\251\345\256\266/main.py" @@ -0,0 +1,86 @@ +import socket +import json +import pygame +import sys + +# === 1. 面板與網路初始化 === +pygame.init() +screen = pygame.display.set_mode((800, 600)) +pygame.display.set_caption("Week 6 - 雙人座標同步") +clock = pygame.time.Clock() + +my_x, my_y = 100, 100 +enemy_x, enemy_y = -100, -100 # 一開始對方在畫面外 + +# 建立 UDP 連線 +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +sock.bind(("0.0.0.0", 5000)) + +# 【關鍵】:設定為非阻塞模式 Non-blocking !!! +# 這讓 recvfrom 就算沒收到信,也不會卡住整個遊戲的畫面更新! +sock.setblocking(False) + +# 測試用:請輸入同學的 IP,如果沒有同學可改為 "127.0.0.1" 測試自己傳給自己 +TARGET_IP = input("請輸入對方 IP (Enter 預設 127.0.0.1): ") +if not TARGET_IP: + TARGET_IP = "127.0.0.1" + +print(f"連線成功!準備將座標同步給 {TARGET_IP}") + +# === 2. 遊戲主迴圈 === +while True: + screen.fill((30, 30, 50)) # 深色背景 + + # --- A. 接收對方的資料 --- + try: + # 因為設了 setblocking(False),沒有信的時候會引發 BlockingIOError + data, addr = sock.recvfrom(1024) + + # 拆開看對方傳了什麼 + enemy_package = json.loads(data.decode()) + enemy_x = enemy_package['x'] + enemy_y = enemy_package['y'] + + except (BlockingIOError, ConnectionResetError): + # 非阻塞模式下,Linux 拋 BlockingIOError,Windows 拋 ConnectionResetError + # 沒有信?當作沒發生,程式繼續往下走! + pass + except json.JSONDecodeError: + # 防止有人亂傳不是 JSON 格式的垃圾訊息導致閃退 + pass + + # --- B. 處理遊戲事件與玩家輸入 --- + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + + # 鍵盤控制自己移動 + keys = pygame.key.get_pressed() + if keys[pygame.K_LEFT]: my_x -= 5 + if keys[pygame.K_RIGHT]: my_x += 5 + if keys[pygame.K_UP]: my_y -= 5 + if keys[pygame.K_DOWN]: my_y += 5 + + # --- C. 繪圖階段 --- + # 畫自己 (藍色) + pygame.draw.rect(screen, (59, 130, 246), (my_x, my_y, 40, 40)) + # 畫對手 (綠色) + pygame.draw.rect(screen, (16, 185, 129), (enemy_x, enemy_y, 40, 40)) + + pygame.display.flip() + clock.tick(60) + + # --- D. 送出自己的資料給對方 --- + my_package = { + "x": my_x, + "y": my_y + } + + # 打包 -> 轉 bytes -> 送出 + json_str = json.dumps(my_package) + try: + sock.sendto(json_str.encode(), (TARGET_IP, 5000)) + except OSError: + # 若發生網路短暫斷線引發的 OS 錯誤,我們也能忽略不處理以避免閃退 + pass diff --git a/server.py b/server.py index 9c503cec439c6822e34b41a0746ab51b3fb03c3d..158597742dbca74c2661cacbacca5fa3cc47c7cf 100644 --- a/server.py +++ b/server.py @@ -84,6 +84,46 @@ async def get_logo(): """Serves the official Telearn logo.""" return FileResponse(os.path.join(BASE_DIR, "templates", "logo-light.png")) +# ----------------- Hashing & Decoding Utilities ----------------- +import hashlib + +def get_id(name: str) -> str: + """Generates a stable 10-char hex hash ID for the given name.""" + return hashlib.sha256(name.encode('utf-8')).hexdigest()[:10] + +def decode_class_student(class_name_or_id: str, student_name_or_id: str = None): + """ + Decodes ID back to real folder names for class and student. + Supports transparent fallback if real names are passed. + """ + all_projects = scan_projects() + + # 1. Decode Class + real_class = None + for c in all_projects: + if get_id(c) == class_name_or_id: + real_class = c + break + if not real_class: + if class_name_or_id in all_projects: + real_class = class_name_or_id + + if not student_name_or_id: + return real_class, None + + # 2. Decode Student (Student ID is hashed from class_name + "/" + student_name) + real_student = None + if real_class and real_class in all_projects: + for s in all_projects[real_class]: + if get_id(f"{real_class}/{s}") == student_name_or_id: + real_student = s + break + if not real_student: + if student_name_or_id in all_projects[real_class]: + real_student = student_name_or_id + + return real_class, real_student + # ----------------- HTTP Routes ----------------- @app.get("/", response_class=HTMLResponse) async def read_index(request: Request): @@ -96,43 +136,69 @@ async def read_index(request: Request): @app.get("/class/{class_name}", response_class=HTMLResponse) async def read_class(request: Request, class_name: str): - """Renders the catalog restricted to a specific class.""" + """Renders the catalog restricted to a specific class (supports ID or Name).""" + real_class, _ = decode_class_student(class_name) + if not real_class: + return HTMLResponse("Class not found", status_code=404) return templates.TemplateResponse("index.html", { "request": request, - "restrict_class": class_name, + "restrict_class": real_class, "restrict_student": "" }) @app.get("/student/{class_name}/{student_name}", response_class=HTMLResponse) async def read_student(request: Request, class_name: str, student_name: str): - """Renders the catalog restricted to a specific student.""" + """Renders the catalog restricted to a specific student (supports IDs or Names).""" + real_class, real_student = decode_class_student(class_name, student_name) + if not real_class or not real_student: + return HTMLResponse("Student not found", status_code=404) return templates.TemplateResponse("index.html", { "request": request, - "restrict_class": class_name, - "restrict_student": student_name + "restrict_class": real_class, + "restrict_student": real_student }) @app.get("/api/projects", response_class=JSONResponse) async def get_projects(class_name: str = None, student_name: str = None): - """API endpoint to get the class-student-project structure (with filtering).""" + """ + API endpoint to get the class-student-project structure. + Also returns class_ids and student_ids maps. + """ all_projects = scan_projects() + + # Pre-generate ID maps + class_ids = {c: get_id(c) for c in all_projects} + student_ids = {f"{c}/{s}": get_id(f"{c}/{s}") for c in all_projects for s in all_projects[c]} + if class_name: - if class_name in all_projects: - if student_name: - if student_name in all_projects[class_name]: - return {class_name: {student_name: all_projects[class_name][student_name]}} + real_class, real_student = decode_class_student(class_name, student_name) + if real_class in all_projects: + if real_student: + if real_student in all_projects[real_class]: + filtered_projects = {real_class: {real_student: all_projects[real_class][real_student]}} else: - return {class_name: {}} + filtered_projects = {real_class: {}} else: - return {class_name: all_projects[class_name]} + filtered_projects = {real_class: all_projects[real_class]} else: - return {} - return all_projects + filtered_projects = {} + else: + filtered_projects = all_projects + + return { + "projects": filtered_projects, + "class_ids": class_ids, + "student_ids": student_ids + } @app.get("/run/{class_name}/{student_name}/{project_name}", response_class=HTMLResponse) async def run_project_page(request: Request, class_name: str, student_name: str, project_name: str): - """Renders the player viewport wrapper.""" - proj_dir = os.path.join(PROJECTS_DIR, class_name, student_name, project_name) + """Renders the player viewport wrapper (supports IDs or Names for class/student).""" + real_class, real_student = decode_class_student(class_name, student_name) + if not real_class or not real_student: + return HTMLResponse("Project not found", status_code=404) + + proj_dir = os.path.join(PROJECTS_DIR, real_class, real_student, project_name) proj_type = "unknown" external_url = "" @@ -150,8 +216,8 @@ async def run_project_page(request: Request, class_name: str, student_name: str, return templates.TemplateResponse("player.html", { "request": request, - "class_name": class_name, - "student_name": student_name, + "class_name": real_class, + "student_name": real_student, "project_name": project_name, "project_type": proj_type, "external_url": external_url @@ -160,9 +226,16 @@ async def run_project_page(request: Request, class_name: str, student_name: str, # ----------------- WebSocket Communication Proxy ----------------- @app.websocket("/ws/play/{class_name}/{student_name}/{project_name}") async def play_project_ws(websocket: WebSocket, class_name: str, student_name: str, project_name: str): + real_class, real_student = decode_class_student(class_name, student_name) + if not real_class or not real_student: + await websocket.accept() + await websocket.send_json({"type": "log", "data": "錯誤: 找不到該專案的執行路徑!\n"}) + await websocket.close() + return + await websocket.accept() - project_path = os.path.join(PROJECTS_DIR, class_name, student_name, project_name, "main.py") + project_path = os.path.join(PROJECTS_DIR, real_class, real_student, project_name, "main.py") if not os.path.exists(project_path): await websocket.send_json({"type": "log", "data": "錯誤: 找不到該專案的 main.py 檔案!\n"}) await websocket.close() diff --git a/templates/index.html b/templates/index.html index fbd0f5df7fcbb62de35f42c503da8d3f44c03572..73920d53bebf5254ffe8b8b49c2db731a6b408d8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,20 +5,16 @@