diff --git a/.gitignore b/.gitignore index 5c00752..bb7447d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ /bin /.idea *.iml +/.vscode +/core +/*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..38f5d98 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM openjdk:8-slim-buster + +COPY . . +RUN apt update && \ + apt install -y vim git && \ + apt install -y libxext6 libxrender1 libxtst6 libfreetype6 libfontconfig1 && \ + echo "Done!" + +# Dev environment: +# docker run -d --name bom-ai -e DISPLAY="host.docker.internal:0" -v /tmp/.X11-unix:/tmp/.X11-unix -v $HOME:/root openjdk:8-slim-buster sleep 999999999 \ No newline at end of file diff --git a/models/sac/logs.csv b/models/sac/logs.csv new file mode 100644 index 0000000..2dc7793 --- /dev/null +++ b/models/sac/logs.csv @@ -0,0 +1,152 @@ +episode,steps,totalReward,avgLoss,avgQ1Loss,avgQ2Loss,avgPolicyLoss +0,210,-523.0,118.12451,57.803856,59.82003,0.50062275 +1,242,-749.0,119.76689,57.689888,60.6614,1.4156002 +2,925,-433.0,29.094067,11.821329,13.617163,3.6555743 +3,350,-1450.0,97.75449,49.923336,44.656353,3.1748052 +4,316,-983.0,181.35384,90.335,86.61,4.408834 +5,315,-1663.0,195.4226,92.854515,96.12815,6.4399476 +6,200,-563.0,127.81987,56.41343,63.062725,8.343729 +7,492,-1969.0,118.48268,46.97105,61.066925,10.444702 +8,1639,-1933.0,71.446945,17.357225,37.748768,16.340952 +9,611,-3869.0,172.67046,75.226616,84.87112,12.572725 +10,1129,-236.0,52.390003,13.913859,20.7331,17.743044 +11,2586,-894.0,32.774223,6.2971,6.447057,20.030064 +12,1705,-1958.0,58.806236,18.043636,19.590504,21.1721 +13,1087,-1929.0,67.4272,20.155167,24.935963,22.336071 +14,2240,-123.0,42.15841,9.641373,10.174707,22.342333 +15,1128,-1046.0,45.677597,10.252888,11.928723,23.495987 +16,869,-427.0,42.60451,8.979891,10.330642,23.29398 +17,1469,-1247.0,50.361095,11.086599,13.231975,26.042522 +18,1843,-2226.0,58.08662,13.345929,17.555159,27.185535 +19,209,-502.0,98.05267,39.028477,40.900356,18.123844 +20,582,-2491.0,129.24535,50.306473,56.399944,22.538923 +21,3053,-1365.0,59.419476,9.592533,13.747647,36.079296 +22,1574,-1598.0,59.90373,10.037819,13.51472,36.35119 +23,1941,-5878.0,100.58356,26.006247,34.26289,40.314415 +24,1656,-2202.0,73.19973,11.98628,18.913006,42.300446 +25,1735,-2155.0,79.18924,12.2470455,21.48121,45.460983 +26,1164,-1313.0,79.21062,12.396383,19.153912,47.660324 +27,610,-2019.0,105.52742,33.878315,39.571068,32.078037 +28,362,-1719.0,154.11963,53.11065,64.010025,36.998947 +29,353,-2500.0,223.22144,75.26022,108.108505,39.852715 +30,1101,-808.0,78.17186,7.5052843,12.557988,58.10859 +31,691,-2222.0,109.035255,30.359722,32.32763,46.347897 +32,1310,-1650.0,96.08864,11.255519,19.656374,65.17674 +33,1713,-2007.0,97.9173,11.116342,21.450443,65.35051 +34,493,-1859.0,126.57732,37.349167,41.03942,48.18874 +35,199,-583.0,149.29532,47.74101,53.956993,47.597305 +36,602,-2139.0,122.94492,30.306122,40.448242,52.19056 +37,415,-1235.0,118.02385,24.727316,34.640484,58.65606 +38,1498,-855.0,107.44676,6.473565,13.841501,87.1317 +39,1018,-465.0,92.36423,5.727498,7.5124907,79.12424 +40,1508,-1074.0,116.3741,8.731051,15.619425,92.02363 +41,2783,-7718.0,147.32169,24.090548,34.481503,88.749626 +42,2817,-1685.0,116.387314,8.810155,12.208402,95.36876 +43,900,-4465.0,166.33464,45.115116,53.52423,67.695305 +44,2576,-815.0,120.480225,7.7298965,10.9017315,101.848595 +45,2138,-61.0,104.30084,4.9779444,6.128467,93.19443 +46,201,-563.0,123.48415,26.819494,32.71556,63.949104 +47,325,-1343.0,156.92891,35.9489,44.947186,76.03282 +48,265,-788.0,126.70134,22.144688,32.233784,72.32287 +49,1480,-856.0,130.70566,7.4623823,9.821857,113.421425 +50,2814,415.0,125.44133,7.1611204,7.6502137,110.62999 +51,2773,-2229.0,130.94748,12.18892,16.88979,101.86876 +52,439,-2963.0,192.62236,51.537384,60.648438,80.436554 +53,3379,-2698.0,151.15074,13.95962,18.150515,119.04061 +54,406,-1656.0,154.1638,32.234413,33.465935,88.463455 +55,928,-532.0,139.04916,13.678808,15.582199,109.78816 +56,1807,-4969.0,169.50269,23.175442,29.379389,116.94785 +57,3567,-2742.0,158.13736,11.868324,15.63386,130.63516 +58,1595,-1537.0,157.65791,13.312529,18.453812,125.89158 +59,1738,-2335.0,165.72011,16.73418,22.532606,126.45332 +60,2110,-144.0,145.10225,9.112715,10.281461,125.70807 +61,3584,-3811.0,171.11647,14.337924,21.871695,134.90685 +62,350,-1140.0,144.29398,21.862326,27.185719,95.24593 +63,569,-1742.0,151.54155,21.045376,29.28093,101.215256 +64,2064,2.0,152.65018,8.205217,9.342428,135.10254 +65,542,-1074.0,128.08012,14.894574,17.12357,96.06197 +66,1529,-1512.0,174.40132,15.257792,19.353363,139.79016 +67,499,-2228.0,179.6372,30.673548,43.322502,105.64116 +68,210,-472.0,130.74551,15.163992,20.842999,94.738525 +69,932,-322.0,160.4552,10.627113,12.091006,137.73709 +70,1661,-2031.0,179.20987,15.501996,21.56076,142.14711 +71,1471,-3317.0,204.40268,25.035603,32.167492,147.1996 +72,3693,-3352.0,193.87155,18.341549,22.932495,152.5975 +73,620,-1838.0,171.4963,23.471308,29.276295,118.7487 +74,334,-2312.0,224.80649,41.612465,51.95589,131.23814 +75,2807,-2826.0,217.40385,19.104603,25.11099,173.18826 +76,211,-452.0,149.61783,17.018948,24.468262,108.130615 +77,1131,-1665.0,227.66077,22.852886,34.55725,170.25063 +78,405,-1526.0,200.46663,25.985239,38.89547,135.5859 +79,196,-613.0,166.63863,19.178562,30.738901,116.72117 +80,2775,-1298.0,225.39261,16.317951,20.474087,188.60056 +81,1246,-1496.0,260.51868,33.96737,38.915424,187.6359 +82,1795,-3210.0,241.34492,20.28743,25.996094,195.06139 +83,1003,-336.0,247.02841,33.084476,34.42362,179.52034 +84,1236,-4137.0,239.60161,29.686676,39.337677,170.57724 +85,201,-653.0,191.40996,27.361547,37.84957,126.19884 +86,852,-479.0,234.84122,25.684011,27.419458,181.73773 +87,584,-2971.0,374.7953,72.8714,91.26561,210.65828 +88,855,-628.0,264.15375,37.259468,39.90641,186.98785 +89,1063,-1221.0,315.79663,56.050713,60.30932,199.4366 +90,419,-2205.0,261.1889,47.547623,55.48989,158.15141 +91,851,-299.0,265.70084,36.8254,38.38253,190.49289 +92,496,-1628.0,218.82877,27.804276,35.946114,155.07835 +93,497,-2068.0,252.80205,36.05374,50.92767,165.82065 +94,2901,-1598.0,297.61774,31.636084,35.86326,230.11841 +95,196,-613.0,202.14757,23.986948,34.356026,143.8046 +96,1466,-877.0,338.9154,56.342846,58.485302,224.08725 +97,204,-513.0,208.65662,30.679163,40.03849,137.93896 +98,1348,-1347.0,245.89726,18.769289,22.457476,204.6705 +99,414,-1465.0,250.0127,36.8858,49.284737,163.84218 +100,913,-3274.0,257.47235,38.609352,48.72204,170.14096 +101,1658,-1821.0,328.081,43.973297,49.319313,234.7884 +102,724,-3099.0,316.6719,51.85784,60.527943,204.2861 +103,745,-3158.0,348.49344,55.69146,75.36152,217.44046 +104,644,-3196.0,444.84766,95.25508,115.02041,234.57217 +105,351,-1260.0,242.30518,32.09302,48.578606,161.63356 +106,707,-3451.0,381.35712,75.92653,96.38502,209.04558 +107,1549,-1301.0,329.9795,31.153934,35.562172,263.26337 +108,3388,-677.0,327.24673,31.528233,34.822388,260.89612 +109,450,-1632.0,299.1242,46.470955,60.099213,192.55403 +110,428,-2324.0,356.9698,61.959625,90.28121,204.72891 +111,341,-1581.0,293.56952,38.33511,60.230827,195.0036 +112,442,-2703.0,393.09177,74.03804,101.332405,217.72134 +113,1281,-2123.0,455.85272,82.362755,92.875946,280.61407 +114,459,-3011.0,399.64087,76.27206,92.15181,231.217 +115,196,-613.0,357.22076,71.38127,89.26785,196.57166 +116,2543,-1378.0,359.75684,41.0498,43.394787,275.31226 +117,670,-2274.0,586.66644,158.49991,167.63762,260.5289 +118,2205,-1336.0,359.61612,34.254612,37.6472,287.7143 +119,203,-533.0,266.6281,31.90745,46.645325,188.07533 +120,1204,-1809.0,438.8253,80.831406,83.91501,274.07886 +121,708,-281.0,346.15836,43.003242,45.87547,257.27963 +122,453,-1332.0,305.78186,49.403618,56.70229,199.67595 +123,3004,-1429.0,434.58945,69.05426,71.033325,294.5019 +124,2527,-5269.0,397.82635,61.075844,71.52562,265.22488 +125,488,-1809.0,307.19785,44.869057,58.27709,204.05173 +126,402,-2586.0,452.73065,94.45423,120.5411,237.73532 +127,1165,-1563.0,480.82822,88.38582,96.86128,295.58112 +128,233,-940.0,317.5232,40.83218,53.27243,223.41858 +129,3453,-742.0,399.51688,45.095848,48.12706,306.29398 +130,176,-1305.0,556.099,151.86472,163.12949,241.10483 +131,867,-407.0,396.10928,52.28838,56.332428,287.48846 +132,684,-1903.0,387.1917,65.513824,73.55791,248.11996 +133,328,-1842.0,470.78085,99.382645,109.06551,262.3327 +134,342,-1291.0,443.00543,86.902466,103.6072,252.49576 +135,3716,-4250.0,538.96893,104.41012,106.38905,328.16974 +136,414,-1955.0,520.40485,126.093544,141.60693,252.70435 +137,376,-2108.0,412.96344,74.881485,83.98923,254.09273 +138,2683,-2106.0,480.47736,58.03288,62.00152,360.44296 +139,351,-1190.0,450.6452,91.1224,108.19773,251.32507 +140,364,-2039.0,505.68207,118.09823,130.81314,256.7707 +141,1466,-1217.0,451.6564,57.135326,60.923744,333.59732 +142,2319,-1826.0,499.6158,84.22354,87.16097,328.23132 +143,2393,-6380.0,557.36884,113.30038,119.39022,324.67822 +144,305,-1064.0,487.2184,97.52559,117.25138,272.44147 +145,3443,-613.0,637.1561,139.21585,144.62766,353.31265 +146,1099,-1798.0,611.7533,129.92844,138.07776,343.74713 +147,381,-1798.0,463.65878,92.3239,98.37209,272.96283 +148,1323,-2269.0,749.7801,196.73126,201.69229,351.35654 +149,1510,-1024.0,511.31818,80.332405,83.52703,347.4587 +150,196,-673.0,426.88153,91.29005,102.27358,233.31792 diff --git a/models/sac/metadata.json b/models/sac/metadata.json new file mode 100644 index 0000000..23da0ce --- /dev/null +++ b/models/sac/metadata.json @@ -0,0 +1 @@ +{"episode":151} \ No newline at end of file diff --git a/models/sac/policy/model-0001.params b/models/sac/policy/model-0001.params new file mode 100644 index 0000000..71051c7 Binary files /dev/null and b/models/sac/policy/model-0001.params differ diff --git a/models/sac/q1/model-0001.params b/models/sac/q1/model-0001.params new file mode 100644 index 0000000..44efa58 Binary files /dev/null and b/models/sac/q1/model-0001.params differ diff --git a/models/sac/q2/model-0001.params b/models/sac/q2/model-0001.params new file mode 100644 index 0000000..f7fbe47 Binary files /dev/null and b/models/sac/q2/model-0001.params differ diff --git a/pom.xml b/pom.xml index c6173a6..c87a7a2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 uet.oop bomberman @@ -6,6 +7,48 @@ Bomberman + + UTF-8 + 0.28.0 + 2.2.2 + 2.11.2 + + + + + ai.djl + api + ${djl.version} + + + ai.djl.pytorch + pytorch-engine + ${djl.version} + + + ai.djl.pytorch + pytorch-native-cpu + ${pytorch.version} + runtime + + + ai.djl.pytorch + pytorch-jni + ${pytorch.version}-${djl.version} + runtime + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + target target/classes @@ -21,17 +64,18 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.0 + 3.8.1 1.8 1.8 ${project.build.sourceEncoding} + org.apache.maven.plugins maven-jar-plugin - 3.1.0 + 3.2.0 @@ -40,17 +84,15 @@ + org.apache.maven.plugins maven-resources-plugin - 2.4.3 + 3.2.0 ${project.build.sourceEncoding} - - UTF-8 - \ No newline at end of file diff --git a/res/font/Minecraft.ttf b/res/font/Minecraft.ttf new file mode 100644 index 0000000..85c1472 Binary files /dev/null and b/res/font/Minecraft.ttf differ diff --git a/res/font/SuperPixel-m2L8j.ttf b/res/font/SuperPixel-m2L8j.ttf new file mode 100644 index 0000000..f7691c8 Binary files /dev/null and b/res/font/SuperPixel-m2L8j.ttf differ diff --git a/res/levels/Level1.txt b/res/levels/Level1P_1.txt similarity index 82% rename from res/levels/Level1.txt rename to res/levels/Level1P_1.txt index 6f00589..38171c0 100644 --- a/res/levels/Level1.txt +++ b/res/levels/Level1P_1.txt @@ -1,12 +1,12 @@ 1 13 31 ############################### -#p s* * 1 * * * * * # +#p * s* * 1 * * * * * # # # # #*# # #*#*# # # #*#*#*# # -# x* b** * * * * # -# # # # # #*# # #*#*# # # # #*# -#f x ** * * # +# f* b** * * * * # +#f# # # # #*# # #*#*# # # # #*# +# x ** * *1 # # # # # # # # # # #*# #*# # # # -#* * * * * # +#* b * * * # # # # # #*# # # #*#*# # # # # # #* ** * * # # #*# # # # # # #*# # # # # # # diff --git a/res/levels/Level2.txt b/res/levels/Level1P_2.txt similarity index 62% rename from res/levels/Level2.txt rename to res/levels/Level1P_2.txt index f76a558..fe5728b 100644 --- a/res/levels/Level2.txt +++ b/res/levels/Level1P_2.txt @@ -1,16 +1,16 @@ 2 13 31 ############################### #p * *2 * ** * * *# -# # # # #*# # #*# # # # # #*# # +# # # # #b# # #*# # # # # #*# # # * * *** ** * ** # # #*# # # # #1# #b#1# # # # #*# # * ** * **# -# # #*# # # # # #*#*#*#*# # # # +# # #*# #s# # # #*#*#*#*# # # # # * * * *x* * # -# # # # #*# # # # # # #*# #*# # -# **** *1 # +# #*# # #*# # # # # # #*# #*# # +# * *f *1 # # # # # # # # # # #*# # # # #*# -# ** * * 3 # +# * * * 3 # ############################### diff --git a/res/levels/Level1P_3.txt b/res/levels/Level1P_3.txt new file mode 100644 index 0000000..7aef208 --- /dev/null +++ b/res/levels/Level1P_3.txt @@ -0,0 +1,14 @@ +3 13 31 +############################### +#p * *3 * ** * * *# +# # # # #*# # #*# # # # # #*# # +# f* * *** ** # * ** # +# #*# # # # #4# #b#1# # # # #*# +# * * ** * **# +# # #*# 3 # # # #*#*#*#*# # # # +# * * * *x* * 4 # +# # # # #*#4# # # # # #*# #*# # +# ***3 *2 # +# # # # # # # # # #*# # # # #*# +# ** ** * 5 # +############################### diff --git a/res/levels/Level2P_1.txt b/res/levels/Level2P_1.txt new file mode 100644 index 0000000..4553efc --- /dev/null +++ b/res/levels/Level2P_1.txt @@ -0,0 +1,14 @@ +1 13 31 +############################### +#p * s* * 1 * * * * * # +# # # #*# # #*#*# # # #*#*#*# # +# f* b** * * * * # +#f# # # # #*# # #*#*# # # # #*# +# x ** * *1 # +# # # # # # # # # #*# #*# # # # +#* b * * * # +# # # # #*# # # #*#*# # # # # # +#* ** * * # +# #*# # # # # # #*# # # # # # # +#a * * * # +############################### \ No newline at end of file diff --git a/res/levels/Level2P_2.txt b/res/levels/Level2P_2.txt new file mode 100644 index 0000000..f71da92 --- /dev/null +++ b/res/levels/Level2P_2.txt @@ -0,0 +1,15 @@ +2 13 31 +############################### +#p * *2 * ** * * *# +# # # # #b# # #*# # # # # #*# # +# * * *** ** * ** # +# #*# # # # #1# #b#1# # # # #*# +# * ** * **# +# # #*# #s# # # #*#*#*#*# # # # +# * * * *x* * # +# #*# # #*# # # # # # #*# #*# # +# * *f *1 # +# # # # # # # # # #*# # # # #*# +#a * * * 3 # +############################### + diff --git a/res/levels/Level2P_3.txt b/res/levels/Level2P_3.txt new file mode 100644 index 0000000..5cd7ad5 --- /dev/null +++ b/res/levels/Level2P_3.txt @@ -0,0 +1,14 @@ +3 13 31 +############################### +#p * *3 * ** * * *# +# # # # #*# # #*# # # # # #*# # +# f* * *** ** # * ** # +# #*# # # # #4# #b#1# # # # #*# +# * * ** * **# +# # #*# 3 # # # #*#*#*#*# # # # +# * * * *x* * 4 # +# # # # #*#4# # # # # #*# #*# # +# ***3 *2 # +# # # # # # # # # #*# # # # #*# +#a** ** * 5 # +############################### diff --git a/res/menu/7729559.svg b/res/menu/7729559.svg new file mode 100644 index 0000000..32a2e57 --- /dev/null +++ b/res/menu/7729559.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/menu/9165683_home_house_icon.png b/res/menu/9165683_home_house_icon.png new file mode 100644 index 0000000..df9e3c5 Binary files /dev/null and b/res/menu/9165683_home_house_icon.png differ diff --git a/res/menu/bgBombman.png b/res/menu/bgBombman.png new file mode 100644 index 0000000..8c19720 Binary files /dev/null and b/res/menu/bgBombman.png differ diff --git a/res/menu/f15591389f71a57ec5e54ba0f48ed385.jpg b/res/menu/f15591389f71a57ec5e54ba0f48ed385.jpg new file mode 100644 index 0000000..49189ad Binary files /dev/null and b/res/menu/f15591389f71a57ec5e54ba0f48ed385.jpg differ diff --git a/res/menu/forest_by_forheksed_d9q4k94-fullview 1.png b/res/menu/forest_by_forheksed_d9q4k94-fullview 1.png new file mode 100644 index 0000000..b7fe19d Binary files /dev/null and b/res/menu/forest_by_forheksed_d9q4k94-fullview 1.png differ diff --git a/res/menu/gameover.png b/res/menu/gameover.png new file mode 100644 index 0000000..1020913 Binary files /dev/null and b/res/menu/gameover.png differ diff --git a/res/menu/icons8-menu-50.png b/res/menu/icons8-menu-50.png new file mode 100644 index 0000000..5a897f7 Binary files /dev/null and b/res/menu/icons8-menu-50.png differ diff --git a/res/menu/icons8-restart-50.png b/res/menu/icons8-restart-50.png new file mode 100644 index 0000000..a206a9b Binary files /dev/null and b/res/menu/icons8-restart-50.png differ diff --git a/res/menu/pointer.png b/res/menu/pointer.png new file mode 100644 index 0000000..eb18034 Binary files /dev/null and b/res/menu/pointer.png differ diff --git a/res/menu/restart.png b/res/menu/restart.png new file mode 100644 index 0000000..d4b6a8c Binary files /dev/null and b/res/menu/restart.png differ diff --git a/res/sprites/fireIcon.png b/res/sprites/fireIcon.png new file mode 100644 index 0000000..2b2f529 Binary files /dev/null and b/res/sprites/fireIcon.png differ diff --git a/res/sprites/player2_down.png b/res/sprites/player2_down.png new file mode 100644 index 0000000..2735443 Binary files /dev/null and b/res/sprites/player2_down.png differ diff --git a/res/sprites/player2_down_1.png b/res/sprites/player2_down_1.png new file mode 100644 index 0000000..8eddb58 Binary files /dev/null and b/res/sprites/player2_down_1.png differ diff --git a/res/sprites/player2_down_2.png b/res/sprites/player2_down_2.png new file mode 100644 index 0000000..46092ac Binary files /dev/null and b/res/sprites/player2_down_2.png differ diff --git a/res/sprites/player2_left.png b/res/sprites/player2_left.png new file mode 100644 index 0000000..e0b74d4 Binary files /dev/null and b/res/sprites/player2_left.png differ diff --git a/res/sprites/player2_left_1.png b/res/sprites/player2_left_1.png new file mode 100644 index 0000000..e8c99b2 Binary files /dev/null and b/res/sprites/player2_left_1.png differ diff --git a/res/sprites/player2_left_2.png b/res/sprites/player2_left_2.png new file mode 100644 index 0000000..8d68f29 Binary files /dev/null and b/res/sprites/player2_left_2.png differ diff --git a/res/sprites/player2_right.png b/res/sprites/player2_right.png new file mode 100644 index 0000000..6589405 Binary files /dev/null and b/res/sprites/player2_right.png differ diff --git a/res/sprites/player2_right_1.png b/res/sprites/player2_right_1.png new file mode 100644 index 0000000..bb03279 Binary files /dev/null and b/res/sprites/player2_right_1.png differ diff --git a/res/sprites/player2_right_2.png b/res/sprites/player2_right_2.png new file mode 100644 index 0000000..da73c79 Binary files /dev/null and b/res/sprites/player2_right_2.png differ diff --git a/res/sprites/player2_up.png b/res/sprites/player2_up.png new file mode 100644 index 0000000..38dd3ab Binary files /dev/null and b/res/sprites/player2_up.png differ diff --git a/res/sprites/player2_up_1.png b/res/sprites/player2_up_1.png new file mode 100644 index 0000000..a1f927a Binary files /dev/null and b/res/sprites/player2_up_1.png differ diff --git a/res/sprites/player2_up_2.png b/res/sprites/player2_up_2.png new file mode 100644 index 0000000..df6d5d0 Binary files /dev/null and b/res/sprites/player2_up_2.png differ diff --git a/res/textures/classic.png b/res/textures/classic.png index 33db1e3..e93685c 100644 Binary files a/res/textures/classic.png and b/res/textures/classic.png differ diff --git a/src/uet/oop/bomberman/Board.java b/src/uet/oop/bomberman/Board.java index 4130228..de59abd 100644 --- a/src/uet/oop/bomberman/Board.java +++ b/src/uet/oop/bomberman/Board.java @@ -1,358 +1,170 @@ package uet.oop.bomberman; -import uet.oop.bomberman.entities.Entity; -import uet.oop.bomberman.entities.Message; -import uet.oop.bomberman.entities.bomb.Bomb; -import uet.oop.bomberman.entities.bomb.FlameSegment; -import uet.oop.bomberman.entities.character.Bomber; -import uet.oop.bomberman.entities.character.Character; -import uet.oop.bomberman.exceptions.LoadLevelException; +import uet.oop.bomberman.agent.base.Agent; +import uet.oop.bomberman.agent.base.RewardBasedAgent; +import uet.oop.bomberman.base.Copyable; +import uet.oop.bomberman.base.IBombManager; +import uet.oop.bomberman.base.IEntityManager; +import uet.oop.bomberman.base.IGameInfoManager; +import uet.oop.bomberman.base.ILevelManager; +import uet.oop.bomberman.entities.character.action.Action; +import uet.oop.bomberman.entities.character.action.ActionConstants; +import uet.oop.bomberman.entities.character.exceptions.ActionOnCooldownException; +import uet.oop.bomberman.entities.character.exceptions.CharacterActionException; import uet.oop.bomberman.graphics.IRender; import uet.oop.bomberman.graphics.Screen; -import uet.oop.bomberman.input.Keyboard; -import uet.oop.bomberman.level.FileLevelLoader; -import uet.oop.bomberman.level.LevelLoader; +import uet.oop.bomberman.manager.EntityManager; +import uet.oop.bomberman.manager.GameInfoManager; +import uet.oop.bomberman.manager.LevelManager; -import java.awt.*; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; /** * Quản lý thao tác điều khiển, load level, render các màn hình của game */ -public class Board implements IRender { - protected LevelLoader _levelLoader; +public class Board implements Copyable, IRender { protected Game _game; - protected Keyboard _input; - protected Screen _screen; - - public Entity[] _entities; - public List _characters = new ArrayList<>(); - protected List _bombs = new ArrayList<>(); - private List _messages = new ArrayList<>(); - - private int _screenToShow = -1; //1:endgame, 2:changelevel, 3:paused - - private int _time = Game.TIME; - private int _points = Game.POINTS; - - public Board(Game game, Keyboard input, Screen screen) { + + private List agents = new ArrayList<>(); + + private IEntityManager entityManager; + private IGameInfoManager gameInfoManager; + private ILevelManager levelManager; + + private IBombManager bombManager; + + public Board(Game game, Screen screen) { _game = game; - _input = input; - _screen = screen; - - loadLevel(1); //start in level 1 - } - - @Override - public void update() { - if( _game.isPaused() ) return; - - updateEntities(); - updateCharacters(); - updateBombs(); - updateMessages(); - detectEndGame(); - - for (int i = 0; i < _characters.size(); i++) { - Character a = _characters.get(i); - if(a.isRemoved()) _characters.remove(i); - } + levelManager = new LevelManager(this); + levelManager.loadGlobalLevel(); } @Override - public void render(Screen screen) { - if( _game.isPaused() ) return; - - //only render the visible part of screen - int x0 = Screen.xOffset >> 4; //tile precision, -> left X - int x1 = (Screen.xOffset + screen.getWidth() + Game.TILES_SIZE) / Game.TILES_SIZE; // -> right X - int y0 = Screen.yOffset >> 4; - int y1 = (Screen.yOffset + screen.getHeight()) / Game.TILES_SIZE; //render one tile plus to fix black margins - - for (int y = y0; y < y1; y++) { - for (int x = x0; x < x1; x++) { - _entities[x + y * _levelLoader.getWidth()].render(screen); + public synchronized void update() { + if (gameInfoManager.isPaused()) + return; + + entityManager.update(); + gameInfoManager.update(); + + processAgentAction(); + } + + public void setLevelManager(ILevelManager levelManager) { + this.levelManager = levelManager; + levelManager.loadGlobalLevel(); + } + + private void clearAgents() { + agents.clear(); + } + + public void addAgent(Agent agent) { + agents.add(agent); + } + + private void processAgentAction() { + for (Agent agent : agents) { + List actions = agent.getNextActions(); + for (Action action : actions) { + try { + agent.getCharacter().performAction(action); + if (action == ActionConstants.DO_NOTHING) { + // Penalize the agent for doing nothing while they can perform something else + // for (Action performableAction: agent.getCharacter().getPerformableActions()) { + // if (performableAction != ActionConstants.DO_NOTHING) { + // if (agent instanceof RewardBasedAgent) { + // ((RewardBasedAgent)agent).addReward(-10); + // } + // break; + // } + // } + } + } catch (ActionOnCooldownException ex) { + // Penalize the agent for spamming actions + if (agent instanceof RewardBasedAgent) { + System.out.println( + "Character " + + agent.getCharacter().getClass().getSimpleName() + + " action on cooldown: " + + action.getClass().getSimpleName() + ); + ((RewardBasedAgent)agent).addReward(-10); + } + } catch (CharacterActionException ignored) { + // Penalize the agent for doing invalid actions + if (agent instanceof RewardBasedAgent) { + ((RewardBasedAgent)agent).addReward(-10); + } + } } } - - renderBombs(screen); - renderCharacter(screen); - - } - - public void nextLevel() { - Game.setBombRadius(1); - Game.setBombRate(1); - Game.setBomberSpeed(1.0); - loadLevel(_levelLoader.getLevel() + 1); - } - - public void loadLevel(int level) { - _time = Game.TIME; - _screenToShow = 2; - _game.resetScreenDelay(); - _game.pause(); - _characters.clear(); - _bombs.clear(); - _messages.clear(); - - try { - _levelLoader = new FileLevelLoader(this, level); - _entities = new Entity[_levelLoader.getHeight() * _levelLoader.getWidth()]; - - _levelLoader.createEntities(); - } catch (LoadLevelException e) { - endGame(); - } - } - - protected void detectEndGame() { - if(_time <= 0) - endGame(); - } - - public void endGame() { - _screenToShow = 1; - _game.resetScreenDelay(); - _game.pause(); - } - - public boolean detectNoEnemies() {// phat hien enemies - int total = 0; - for (int i = 0; i < _characters.size(); i++) { - if(_characters.get(i) instanceof Bomber == false) - ++total; - } - - return total == 0; - } - - public void drawScreen(Graphics g) { - switch (_screenToShow) { - case 1: - _screen.drawEndGame(g, _points); - break; - case 2: - _screen.drawChangeLevel(g, _levelLoader.getLevel()); - break; - case 3: - _screen.drawPaused(g); - break; - } - } - - public Entity getEntity(double x, double y, Character m) { - - Entity res = null; - - res = getFlameSegmentAt((int)x, (int)y); - if( res != null) return res; - - res = getBombAt(x, y); - if( res != null) return res; - - res = getCharacterAtExcluding((int)x, (int)y, m); - if( res != null) return res; - - res = getEntityAt((int)x, (int)y); - - return res; - } - - public List getBombs() { - return _bombs; - } - - public Bomb getBombAt(double x, double y) { - Iterator bs = _bombs.iterator(); - Bomb b; - while(bs.hasNext()) { - b = bs.next(); - if(b.getX() == (int)x && b.getY() == (int)y) - return b; - } - - return null; } - public Bomber getBomber() { - Iterator itr = _characters.iterator(); - - Character cur; - while(itr.hasNext()) { - cur = itr.next(); - - if(cur instanceof Bomber) - return (Bomber) cur; - } - - return null; - } - - public Character getCharacterAtExcluding(int x, int y, Character a) { - Iterator itr = _characters.iterator(); - - Character cur; - while(itr.hasNext()) { - cur = itr.next(); - if(cur == a) { - continue; - } - - if(cur.getXTile() == x && cur.getYTile() == y) { - return cur; + public void handleWinLevel() { + for (Agent agent : agents) { + if (agent instanceof RewardBasedAgent) { + ((RewardBasedAgent)agent).handleWinLevel(); } - } - - return null; } - - public FlameSegment getFlameSegmentAt(int x, int y) { - Iterator bs = _bombs.iterator(); - Bomb b; - while(bs.hasNext()) { - b = bs.next(); - - FlameSegment e = b.flameAt(x, y); - if(e != null) { - return e; + + public void handleLoseLevel() { + for (Agent agent : agents) { + if (agent instanceof RewardBasedAgent) { + ((RewardBasedAgent)agent).handleLoseLevel(); } } - - return null; - } - - public Entity getEntityAt(double x, double y) { - return _entities[(int)x + (int)y * _levelLoader.getWidth()]; - } - - public void addEntity(int pos, Entity e) { - _entities[pos] = e; - } - - public void addCharacter(Character e) { - _characters.add(e); - } - - public void addBomb(Bomb e) { - _bombs.add(e); - } - - public void addMessage(Message e) { - _messages.add(e); } - protected void renderCharacter(Screen screen) { - Iterator itr = _characters.iterator(); - - while(itr.hasNext()) - itr.next().render(screen); - } - - protected void renderBombs(Screen screen) { - Iterator itr = _bombs.iterator(); - - while(itr.hasNext()) - itr.next().render(screen); - } - - public void renderMessages(Graphics g) { - Message m; - for (int i = 0; i < _messages.size(); i++) { - m = _messages.get(i); - - g.setFont(new Font("Arial", Font.PLAIN, m.getSize())); - g.setColor(m.getColor()); - g.drawString(m.getMessage(), (int)m.getX() - Screen.xOffset * Game.SCALE, (int)m.getY()); - } - } - - protected void updateEntities() { - if( _game.isPaused() ) return; - for (int i = 0; i < _entities.length; i++) { - _entities[i].update(); - } - } - - protected void updateCharacters() { - if( _game.isPaused() ) return; - Iterator itr = _characters.iterator(); - - while(itr.hasNext() && !_game.isPaused()) - itr.next().update(); - } - - protected void updateBombs() { - if( _game.isPaused() ) return; - Iterator itr = _bombs.iterator(); - - while(itr.hasNext()) - itr.next().update(); - } - - protected void updateMessages() { - if( _game.isPaused() ) return; - Message m; - int left; - for (int i = 0; i < _messages.size(); i++) { - m = _messages.get(i); - left = m.getDuration(); - - if(left > 0) - m.setDuration(--left); - else - _messages.remove(i); + @Override + public synchronized void render(Screen screen) { + if (gameInfoManager.isPaused()) + return; + if (gameInfoManager.getTime() <= 0) { + levelManager.endGame(); } + entityManager.render(screen); + } + + public synchronized void init() { + gameInfoManager = new GameInfoManager(); + entityManager = new EntityManager( + levelManager.getBoardWidth(), + levelManager.getBoardHeight(), + gameInfoManager, + levelManager); + gameInfoManager.setEntityManager(entityManager); + gameInfoManager.pause(); + _game.setScreenToShow(2); + _game.resetScreenDelay(); } - public int subtractTime() { - if(_game.isPaused()) - return this._time; - else - return this._time--; - } - - public Keyboard getInput() { - return _input; - } - - public LevelLoader getLevel() { - return _levelLoader; - } - - public Game getGame() { - return _game; + public void clear() { + clearAgents(); } - public int getShow() { - return _screenToShow; + public IEntityManager getEntityManager() { + return entityManager; } - public void setShow(int i) { - _screenToShow = i; + public IGameInfoManager getGameInfoManager() { + return gameInfoManager; } - public int getTime() { - return _time; + public ILevelManager getLevelManager() { + return levelManager; } - public int getPoints() { - return _points; + public IBombManager getBombManager() { + return bombManager; } - public void addPoints(int points) { - this._points += points; - } - - public int getWidth() { - return _levelLoader.getWidth(); + @Override + public Board copy() { + // TODO Auto-generated method stub + return null; } - public int getHeight() { - return _levelLoader.getHeight(); - } - } diff --git a/src/uet/oop/bomberman/BombermanGame.java b/src/uet/oop/bomberman/BombermanGame.java index 0000736..43c5a73 100644 --- a/src/uet/oop/bomberman/BombermanGame.java +++ b/src/uet/oop/bomberman/BombermanGame.java @@ -4,9 +4,11 @@ import uet.oop.bomberman.sound.Sound; public class BombermanGame { - + public static void main(String[] args) { - Sound.play("soundtrack"); - new Frame(); + Sound.play("soundtrack"); + Frame frame = new Frame(); + frame.setVisible(true); + frame.start(); } } diff --git a/src/uet/oop/bomberman/Game.java b/src/uet/oop/bomberman/Game.java index 53e5c53..b40681a 100644 --- a/src/uet/oop/bomberman/Game.java +++ b/src/uet/oop/bomberman/Game.java @@ -1,13 +1,21 @@ package uet.oop.bomberman; +import uet.oop.bomberman.base.IGameInfoManager; +import uet.oop.bomberman.entities.Entity; import uet.oop.bomberman.graphics.Screen; import uet.oop.bomberman.gui.Frame; import uet.oop.bomberman.input.Keyboard; +import uet.oop.bomberman.screen.SelectGameModeScreen; +import uet.oop.bomberman.screen.DeadScreen; +import uet.oop.bomberman.screen.SelectLevelScreen; +import uet.oop.bomberman.utils.EScreenName; +import uet.oop.bomberman.utils.Global; import java.awt.*; import java.awt.image.BufferStrategy; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; +import java.util.Optional; /** * Tạo vòng lặp cho game, lưu trữ một vài tham số cấu hình toàn cục, @@ -15,172 +23,257 @@ */ public class Game extends Canvas { - - public static final int TILES_SIZE = 16, - WIDTH = TILES_SIZE * (31 / 2), - HEIGHT = 13 * TILES_SIZE; + WIDTH = TILES_SIZE * (31 / 2), + HEIGHT = 13 * TILES_SIZE; public static int SCALE = 3; - + public static final String TITLE = "BombermanGame"; - - private static final int BOMBRATE = 1; - private static final int BOMBRADIUS = 1; - private static final double BOMBERSPEED = 1.0;//toc do bomber - - public static final int TIME = 200; + public static final int TICKS_PER_SECOND = 60; + + public static final int BOMBRATE = 1; + public static final int BOMBRADIUS = 1; + public static final double BOMBERSPEED = 3.0;// toc do bomber + + public static final int TIME = 200 * TICKS_PER_SECOND; public static final int POINTS = 0; - - protected static int SCREENDELAY = 3; - protected static int bombRate = BOMBRATE; - protected static int bombRadius = BOMBRADIUS; - protected static double bomberSpeed = BOMBERSPEED; - - + protected static int SCREENDELAY = 3 * TICKS_PER_SECOND; + protected int _screenDelay = SCREENDELAY; - - private Keyboard _input; + private boolean _running = false; - private boolean _paused = true; - private Board _board; private Screen screen; private Frame _frame; - + private BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); - private int[] pixels = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); - + private int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + + // game variable + private int frames; + private int updates; + private long timer; + + // game screens + public SelectLevelScreen selectLevelScreen; + private SelectGameModeScreen selectGameModeScreen; + public DeadScreen deadScreen; + + public boolean headless = false; + + private int _screenToShow = -1; // 1:endgame, 2:changelevel, 3:paused + public Game(Frame frame) { _frame = frame; _frame.setTitle(TITLE); - + screen = new Screen(WIDTH, HEIGHT); - _input = new Keyboard(); - - _board = new Board(this, _input, screen); - addKeyListener(_input); + + _board = new Board(this, screen); + addKeyListener(Keyboard.i()); + + initScreen(); + } - - - private void renderGame() { - BufferStrategy bs = getBufferStrategy(); - if(bs == null) { - createBufferStrategy(3); - return; - } - + + private void renderGame(Graphics g) { screen.clear(); - + _board.render(screen); - + for (int i = 0; i < pixels.length; i++) { pixels[i] = screen._pixels[i]; } - - Graphics g = bs.getDrawGraphics(); - + g.drawImage(image, 0, 0, getWidth(), getHeight(), null); - _board.renderMessages(g); - - g.dispose(); - bs.show(); + _board.getGameInfoManager().render(screen, g); } - - private void renderScreen() { + + private void renderScreen(Graphics g) { + screen.clear(); + drawScreen(g); + } + + private void drawScreen(Graphics g) { + switch (getScreenToShow()) { + case 1: + screen.drawEndGame(g, _board.getGameInfoManager().getPoints()); + break; + case 2: + screen.drawChangeLevel(g, Global.gameLevel); + break; + case 3: + screen.drawPaused(g); + break; + } + } + + private void initScreen() { + Global.currentScreen = EScreenName.SELECT_GAME_MODE; + this.selectGameModeScreen = new SelectGameModeScreen(); + this.selectLevelScreen = new SelectLevelScreen(_board, _frame); + this.deadScreen = new DeadScreen(this); + } + + private void update() { + Keyboard.i().update(); + switch (Global.currentScreen) { + case GAME_PLAY_SCREEN: + _board.update(); + if (_screenToShow == -1 && Keyboard.i().pause) { // Kiểm tra nếu phím "p" được nhấn + _screenToShow = 3; // Hiển thị màn hình tạm dừng + _board.getGameInfoManager().pause(); // Đặt trạng thái game là tạm dừng + return; + } + if (_screenToShow == 3 && Keyboard.i().resume) { + _board.getGameInfoManager().unpause(); + _screenToShow = -1; + _screenDelay = 0; + } + snapCameraToPlayer(); + break; + case SELECT_LEVEL_SCREEN: + selectLevelScreen.update(); + break; + case SELECT_GAME_MODE: + selectGameModeScreen.update(); + break; + case END_GAME_SCREEN: + deadScreen.update(); + break; + } + + } + + private void showScreen() { BufferStrategy bs = getBufferStrategy(); - if(bs == null) { + if (bs == null) { createBufferStrategy(3); return; } - - screen.clear(); - Graphics g = bs.getDrawGraphics(); - - _board.drawScreen(g); + + IGameInfoManager gameInfoManager = _board.getGameInfoManager(); + + switch (Global.currentScreen) { + case GAME_PLAY_SCREEN: + Keyboard.i().keyboardInputCallback = Optional.empty(); + if (gameInfoManager.isPaused()) { + if (_screenToShow == 2 && _screenDelay <= 0) { + _screenToShow = -1; + gameInfoManager.unpause(); + } + + renderScreen(g); + } else { + renderGame(g); + } + + if (Keyboard.i().resume) { + gameInfoManager.unpause(); + _screenToShow = -1; + _screenDelay = 0; + } + + if (_screenToShow == 2) --_screenDelay; + + frames++; + if (System.currentTimeMillis() - timer > 1000) { + _frame.setTime(gameInfoManager.getTime()); + _frame.setPoints(gameInfoManager.getPoints()); + _frame.setLevel(Global.gameLevel); + _frame.setEnemy(Global.enemies); + _frame.renderItemTime(); + + timer += 1000; + _frame.setTitle(TITLE + " | " + updates + " rate, " + frames + " fps"); + updates = 0; + frames = 0; + } + break; + case SELECT_LEVEL_SCREEN: + // TODO: render select level screen + selectLevelScreen.setInput(Keyboard.i()); + selectLevelScreen.drawScreen(g); + break; + case SELECT_GAME_MODE: + selectGameModeScreen.setInput(Keyboard.i()); + selectGameModeScreen.drawScreen(g); + break; + case END_GAME_SCREEN: + deadScreen.setInput(); + deadScreen.drawScreen(g); + break; + } g.dispose(); bs.show(); } - private void update() { - _input.update(); - _board.update(); + private void initGame() { + this.timer = System.currentTimeMillis(); + this.frames = 0; + this.updates = 0; } - + public void start() { _running = true; - - long lastTime = System.nanoTime(); - long timer = System.currentTimeMillis(); - final double ns = 1000000000.0 / 60.0; //nanosecond, 60 frames per second + + initGame(); + + long lastTime = System.nanoTime(); + final double ns = 1000000000.0 / 60.0; // nanosecond, 60 frames per second double delta = 0; - int frames = 0; - int updates = 0; - requestFocus(); - while(_running) { + // requestFocus(); + while (_running) { long now = System.nanoTime(); delta += (now - lastTime) / ns; lastTime = now; - while(delta >= 1) { + + if (headless) { + // Headless mode: update as fast as possible, without rendering update(); - updates++; - delta--; - } - - if(_paused) { - if(_screenDelay <= 0) { - _board.setShow(-1); - _paused = false; - } - - renderScreen(); + showScreen(); } else { - renderGame(); - } - - - frames++; - if(System.currentTimeMillis() - timer > 1000) { - _frame.setTime(_board.subtractTime()); - _frame.setPoints(_board.getPoints()); - timer += 1000; - _frame.setTitle(TITLE + " | " + updates + " rate, " + frames + " fps"); - updates = 0; - frames = 0; - - if(_board.getShow() == 2) - --_screenDelay; + // Keep updating to catch up with 60 frames per second + while (delta >= 1) { + update(); + updates++; + delta--; + } + + showScreen(); } } } - - public static double getBomberSpeed() { - return bomberSpeed; - } - - public static int getBombRate() { - return bombRate; - } - - public static int getBombRadius() { - return bombRadius; - } - - public static void addBomberSpeed(double i) { - bomberSpeed += i; - } - - public static void addBombRadius(int i) { - bombRadius += i; - } - - public static void addBombRate(int i) { - bombRate += i; + + public void stop() { + _running = false; } + private void snapCameraToPlayer() { + int xScroll = calculateXOffset(_board.getEntityManager().getPlayer()); + Screen.setOffset(xScroll, 0); + } + + private int calculateXOffset(Entity entity) { + if(entity == null) return 0; + int temp = Screen.xOffset; + + double x = entity.getX() / 16; + double complement = 0.5; + int firstBreakpoint = _board.getLevelManager().getBoardWidth() / 4; + int lastBreakpoint = _board.getLevelManager().getBoardWidth() - firstBreakpoint; + + if( x > firstBreakpoint + complement && x < lastBreakpoint - complement) { + temp = (int)entity.getX() - (Game.WIDTH / 2); + } + + return temp; + } + public void resetScreenDelay() { _screenDelay = SCREENDELAY; } @@ -189,22 +282,21 @@ public Board getBoard() { return _board; } - public boolean isPaused() { - return _paused; + public void restartGame() { + Global.currentScreen = EScreenName.GAME_PLAY_SCREEN; + _board.getLevelManager().loadGlobalLevel(); } - - public void pause() { - _paused = true; + + public void startNewGame() { + Global.currentScreen = EScreenName.SELECT_LEVEL_SCREEN; } - public static void setBombRate(int bombRate) { - Game.bombRate = bombRate; - } - public static void setBombRadius(int bombRadius) { - Game.bombRadius = bombRadius; - } + public int getScreenToShow() { + return _screenToShow; + } + + public void setScreenToShow(int screenToShow) { + this._screenToShow = screenToShow; + } - public static void setBomberSpeed(double bomberSpeed) { - Game.bomberSpeed = bomberSpeed; - } } diff --git a/src/uet/oop/bomberman/TrainAgent.java b/src/uet/oop/bomberman/TrainAgent.java new file mode 100644 index 0000000..2b4d867 --- /dev/null +++ b/src/uet/oop/bomberman/TrainAgent.java @@ -0,0 +1,47 @@ +package uet.oop.bomberman; + +import uet.oop.bomberman.gui.Frame; +import uet.oop.bomberman.manager.LevelManager; +import uet.oop.bomberman.utils.EScreenName; +import uet.oop.bomberman.utils.Global; + +public class TrainAgent { + + public static void main(String[] args) { + new TrainAgent(); + } + + private TrainAgent() { + Global.isAIPlayer = true; + Frame frame = new Frame(); + Game game = frame._gamepane.getGame(); + Global.currentScreen = EScreenName.GAME_PLAY_SCREEN; + game.getBoard().setLevelManager(new LoopingLevelManager(game.getBoard())); + game.headless = true; + frame.setVisible(true); + frame.start(); + } + + private class LoopingLevelManager extends LevelManager { + public LoopingLevelManager(Board board) { + super(board); + } + + @Override + public void endGame() { + board.handleLoseLevel(); + // Restart level + loadGlobalLevel(); + } + + @Override + public void nextLevel() { + board.handleWinLevel(); + // Restart level + loadGlobalLevel(); + } + + + } + +} diff --git a/src/uet/oop/bomberman/agent/KeyboardAgent.java b/src/uet/oop/bomberman/agent/KeyboardAgent.java new file mode 100644 index 0000000..d8abd5b --- /dev/null +++ b/src/uet/oop/bomberman/agent/KeyboardAgent.java @@ -0,0 +1,51 @@ +package uet.oop.bomberman.agent; + +import java.util.ArrayList; +import java.util.List; + +import uet.oop.bomberman.agent.base.Agent; +import uet.oop.bomberman.entities.character.Bomber; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.character.action.Action; +import uet.oop.bomberman.entities.character.action.ActionConstants; +import uet.oop.bomberman.entities.character.action.ActionMove; +import uet.oop.bomberman.input.Keyboard; + +public class KeyboardAgent extends Agent { + + public KeyboardAgent(Character character) { + super(character); + } + + @Override + public List getNextActions() { + List actions = getMoveActions(); + + if (character instanceof Bomber) { + if (Keyboard.i().space) { + actions.add(ActionConstants.PLACE_BOMB); + } + } + return actions; + } + + private List getMoveActions() { + int xa = 0, ya = 0; + if (Keyboard.i().up) + ya--; + if (Keyboard.i().down) + ya++; + if (Keyboard.i().left) + xa--; + if (Keyboard.i().right) + xa++; + + List actions = new ArrayList<>(); + if (xa != 0 || ya != 0) { + ActionMove actionMove = new ActionMove(xa, ya); + actions.add(actionMove); + } + return actions; + } + +} diff --git a/src/uet/oop/bomberman/agent/KeyboardAgentPlayer1.java b/src/uet/oop/bomberman/agent/KeyboardAgentPlayer1.java new file mode 100644 index 0000000..5e95761 --- /dev/null +++ b/src/uet/oop/bomberman/agent/KeyboardAgentPlayer1.java @@ -0,0 +1,51 @@ +package uet.oop.bomberman.agent; + +import java.util.ArrayList; +import java.util.List; + +import uet.oop.bomberman.entities.character.Bomber; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.character.action.Action; +import uet.oop.bomberman.entities.character.action.ActionConstants; +import uet.oop.bomberman.entities.character.action.ActionMove; +import uet.oop.bomberman.input.Keyboard; + +public class KeyboardAgentPlayer1 extends KeyboardAgent { + // biến + + public KeyboardAgentPlayer1(Character character) { + super(character); + } + + @Override + public List getNextActions() { + List actions = getMoveActions(); + + if (character instanceof Bomber) { + if (Keyboard.i().player1_bomb) { + actions.add(ActionConstants.PLACE_BOMB); + } + } + return actions; + } + + private List getMoveActions() { + int xa = 0, ya = 0; + if (Keyboard.i().player1_up) + ya--; + if (Keyboard.i().player1_down) + ya++; + if (Keyboard.i().player1_left) + xa--; + if (Keyboard.i().player1_right) + xa++; + + List actions = new ArrayList<>(); + if (xa != 0 || ya != 0) { + ActionMove actionMove = new ActionMove(xa, ya); + actions.add(actionMove); + } + return actions; + } + +} diff --git a/src/uet/oop/bomberman/agent/KeyboardAgentPlayer2.java b/src/uet/oop/bomberman/agent/KeyboardAgentPlayer2.java new file mode 100644 index 0000000..1724ced --- /dev/null +++ b/src/uet/oop/bomberman/agent/KeyboardAgentPlayer2.java @@ -0,0 +1,51 @@ +package uet.oop.bomberman.agent; + +import java.util.ArrayList; +import java.util.List; + +import uet.oop.bomberman.entities.character.Bomber; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.character.action.Action; +import uet.oop.bomberman.entities.character.action.ActionConstants; +import uet.oop.bomberman.entities.character.action.ActionMove; +import uet.oop.bomberman.input.Keyboard; + +public class KeyboardAgentPlayer2 extends KeyboardAgent { + + public KeyboardAgentPlayer2(Character character) { + super(character); + // TODO Auto-generated constructor stub + } + + @Override + public List getNextActions() { + List actions = getMoveActions(); + + if (character instanceof Bomber) { + if (Keyboard.i().player2_bomb) { + actions.add(ActionConstants.PLACE_BOMB); + } + } + return actions; + } + + private List getMoveActions() { + int xa = 0, ya = 0; + if (Keyboard.i().player2_up) + ya--; + if (Keyboard.i().player2_down) + ya++; + if (Keyboard.i().player2_left) + xa--; + if (Keyboard.i().player2_right) + xa++; + + List actions = new ArrayList<>(); + if (xa != 0 || ya != 0) { + ActionMove actionMove = new ActionMove(xa, ya); + actions.add(actionMove); + } + return actions; + } + +} diff --git a/src/uet/oop/bomberman/agent/MovingAgent.java b/src/uet/oop/bomberman/agent/MovingAgent.java new file mode 100644 index 0000000..6b93e7b --- /dev/null +++ b/src/uet/oop/bomberman/agent/MovingAgent.java @@ -0,0 +1,44 @@ +package uet.oop.bomberman.agent; + +import java.util.ArrayList; +import java.util.List; + +import uet.oop.bomberman.agent.base.Agent; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.character.action.Action; +import uet.oop.bomberman.entities.character.action.ActionConstants; +import uet.oop.bomberman.entities.character.enemy.ai.AI; + +public class MovingAgent extends Agent { + + private AI ai; + + public MovingAgent(Character character, AI ai) { + super(character); + this.ai = ai; + } + + public Action getNextAction() { + int direction = ai.calculateDirection(); + switch (direction) { + case 0: + return ActionConstants.MOVE_UP; + case 1: + return ActionConstants.MOVE_RIGHT; + case 2: + return ActionConstants.MOVE_DOWN; + case 3: + return ActionConstants.MOVE_LEFT; + default: + return ActionConstants.DO_NOTHING; + } + } + + @Override + public List getNextActions() { + List actions = new ArrayList<>(); + actions.add(getNextAction()); + return actions; + } + +} diff --git a/src/uet/oop/bomberman/agent/base/Agent.java b/src/uet/oop/bomberman/agent/base/Agent.java new file mode 100644 index 0000000..5c22291 --- /dev/null +++ b/src/uet/oop/bomberman/agent/base/Agent.java @@ -0,0 +1,17 @@ +package uet.oop.bomberman.agent.base; + +import uet.oop.bomberman.entities.character.Character; + +public abstract class Agent implements IAgent { + + protected Character character; + + public Agent(Character character) { + this.character = character; + } + + public Character getCharacter() { + return character; + } + +} diff --git a/src/uet/oop/bomberman/agent/base/IAgent.java b/src/uet/oop/bomberman/agent/base/IAgent.java new file mode 100644 index 0000000..d358ab9 --- /dev/null +++ b/src/uet/oop/bomberman/agent/base/IAgent.java @@ -0,0 +1,11 @@ +package uet.oop.bomberman.agent.base; + +import java.util.List; + +import uet.oop.bomberman.entities.character.action.Action; + +public interface IAgent { + + public List getNextActions(); + +} diff --git a/src/uet/oop/bomberman/agent/base/RewardBasedAgent.java b/src/uet/oop/bomberman/agent/base/RewardBasedAgent.java new file mode 100644 index 0000000..e5d3a1c --- /dev/null +++ b/src/uet/oop/bomberman/agent/base/RewardBasedAgent.java @@ -0,0 +1,10 @@ +package uet.oop.bomberman.agent.base; + +public interface RewardBasedAgent { + + public void handleWinLevel(); + public void handleLoseLevel(); + + public void addReward(float reward); + +} diff --git a/src/uet/oop/bomberman/agent/base/SerializableAgent.java b/src/uet/oop/bomberman/agent/base/SerializableAgent.java new file mode 100644 index 0000000..677fd43 --- /dev/null +++ b/src/uet/oop/bomberman/agent/base/SerializableAgent.java @@ -0,0 +1,8 @@ +package uet.oop.bomberman.agent.base; + +public interface SerializableAgent { + + public void load(String path); + public void save(String path); + +} diff --git a/src/uet/oop/bomberman/agent/ppo/NaivePPOAgent.java b/src/uet/oop/bomberman/agent/ppo/NaivePPOAgent.java new file mode 100644 index 0000000..d41d332 --- /dev/null +++ b/src/uet/oop/bomberman/agent/ppo/NaivePPOAgent.java @@ -0,0 +1,13 @@ +package uet.oop.bomberman.agent.ppo; + +import uet.oop.bomberman.Board; +import uet.oop.bomberman.agent.state.NaivePlayerStateExtractor; +import uet.oop.bomberman.entities.character.Character; + +public class NaivePPOAgent extends PPOAgent { + + public NaivePPOAgent(Character character, Board board) { + super(character, board, new NaivePlayerStateExtractor(character)); + } + +} diff --git a/src/uet/oop/bomberman/agent/ppo/PPOAgent.java b/src/uet/oop/bomberman/agent/ppo/PPOAgent.java new file mode 100644 index 0000000..9465ccb --- /dev/null +++ b/src/uet/oop/bomberman/agent/ppo/PPOAgent.java @@ -0,0 +1,104 @@ +package uet.oop.bomberman.agent.ppo; + +import java.util.ArrayList; +import java.util.List; + +import uet.oop.bomberman.Board; +import uet.oop.bomberman.agent.base.Agent; +import uet.oop.bomberman.agent.base.RewardBasedAgent; +import uet.oop.bomberman.agent.base.SerializableAgent; +import uet.oop.bomberman.agent.rl.PPO; +import uet.oop.bomberman.agent.state.base.IStateExtractor; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.character.action.Action; + +public class PPOAgent extends Agent implements SerializableAgent, RewardBasedAgent { + + private IStateExtractor stateExtractor; + private Board board; + private PPO ppo; + private List validActions; + + private float prevValue; + private float bonusReward = 0; + + public PPOAgent(Character character, Board board, IStateExtractor stateExtractor) { + super(character); + this.board = board; + this.prevValue = stateExtractor.getValue(board); + this.stateExtractor = stateExtractor; + this.validActions = character.getValidActions(); + this.ppo = new PPO( + stateExtractor.getDimension(), + validActions.size(), + 64, + 0.97f, + 0.95f, + 3e-4f, + 8, + 64, + 0.05f + ); + this.ppo.setActionMaskGetter(() -> { + List performableActions = character.getPerformableActions(); + Boolean[] mask = new Boolean[validActions.size()]; + for (int i = 0; i < validActions.size(); i++) { + mask[i] = performableActions.contains(validActions.get(i)); + } + return mask; + }); + } + + @Override + public List getNextActions() { + collectReward(); + Action action = getAction(); + List actions = new ArrayList<>(); + actions.add(action); + prevValue = stateExtractor.getValue(board); + return actions; + } + + private void collectReward() { + float currentValue = stateExtractor.getValue(board); + try { + ppo.collect(currentValue - prevValue + bonusReward, false); + bonusReward = 0; + } catch (IllegalStateException ignored) {} + } + + private Action getAction() { + float[] state = stateExtractor.getEmbedding(board); + int actionIndex = ppo.react(state); + Action action = validActions.get(actionIndex); + return action; + } + + @Override + public void load(String path) { + ppo.load(path); + } + + @Override + public void save(String path) { + ppo.save(path); + } + + @Override + public void handleWinLevel() { + ppo.collect(100, true); + save(null); + } + + @Override + public void handleLoseLevel() { + ppo.collect(-100, true); + save(null); + } + + @Override + public void addReward(float reward) { + bonusReward += reward; + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/ExpertGuidedAgent.java b/src/uet/oop/bomberman/agent/rl/ExpertGuidedAgent.java new file mode 100644 index 0000000..8d88b35 --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/ExpertGuidedAgent.java @@ -0,0 +1,121 @@ +package uet.oop.bomberman.agent.rl; + +import java.util.List; +import java.util.Random; + +import ai.djl.ndarray.NDManager; +import ai.djl.translate.TranslateException; +import uet.oop.bomberman.Game; +import uet.oop.bomberman.agent.base.IAgent; +import uet.oop.bomberman.agent.rl.base.BaseAgentImpl; +import uet.oop.bomberman.entities.character.action.Action; + +public class ExpertGuidedAgent extends BaseAgentImpl { + + private static final float EXPERT_RATE = 0.5f; + private static final int SWITCHING_DURATION = 10 * Game.TICKS_PER_SECOND; + + + private BaseAgentImpl originalAgent; + private IAgent expertAgent; + private List validActions; + private final Random RANDOM = new Random(); + + private boolean isExpertGuided = false; + private int switchingDuration = 0; + + protected ExpertGuidedAgent(Builder builder) { + super(builder); + this.originalAgent = builder.originalAgent; + this.expertAgent = builder.expertAgent; + this.validActions = builder.validActions; + } + + @Override + public int react(float[] state) { + switchingDuration -= 1; + if (switchingDuration <= 0) { + float chance = RANDOM.nextFloat(); + if (chance < EXPERT_RATE) { + isExpertGuided = true; + } else { + isExpertGuided = false; + } + switchingDuration = SWITCHING_DURATION; + } + if (isExpertGuided) { + Action action = expertAgent.getNextActions().get(0); + int actionId = validActions.indexOf(action); + return react(state, actionId); + } else { + return originalAgent.react(state); + } + } + + @Override + public int react(float[] state, int action) { + return originalAgent.react(state, action); + } + + @Override + public void collect(float reward, boolean done) { + originalAgent.collect(reward, done); + } + + @Override + public void reset() { + originalAgent.reset(); + } + + @Override + public int sampleAction(NDManager submanager, float[] state) { + return originalAgent.sampleAction(submanager, state); + } + + @Override + public void load(String path) { + originalAgent.load(path); + } + + @Override + public void save(String path) { + originalAgent.save(path); + } + + @Override + public void updateModel(NDManager submanager) throws TranslateException { + originalAgent.updateModel(submanager); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends BaseBuilder { + + private BaseAgentImpl originalAgent; + private IAgent expertAgent; + private List validActions; + + public Builder setOriginalAgent(BaseAgentImpl originalAgent) { + this.originalAgent = originalAgent; + return this; + } + + public Builder setExpertAgent(IAgent expertAgent) { + this.expertAgent = expertAgent; + return this; + } + + public Builder setValidActions(List validActions) { + this.validActions = validActions; + return this; + } + + public ExpertGuidedAgent build() { + return new ExpertGuidedAgent(this); + } + + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/PPO.java b/src/uet/oop/bomberman/agent/rl/PPO.java new file mode 100644 index 0000000..340cc1b --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/PPO.java @@ -0,0 +1,153 @@ +package uet.oop.bomberman.agent.rl; + +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.NDManager; +import ai.djl.ndarray.index.NDIndex; +import ai.djl.ndarray.types.DataType; +import ai.djl.ndarray.types.Shape; +import ai.djl.training.DefaultTrainingConfig; +import ai.djl.training.GradientCollector; +import ai.djl.training.Trainer; +import ai.djl.training.listener.TrainingListener; +import ai.djl.training.loss.Loss; +import ai.djl.translate.TranslateException; +import uet.oop.bomberman.agent.rl.base.BaseGAE; +import uet.oop.bomberman.agent.rl.dtypes.MemoryBatch; +import uet.oop.bomberman.agent.rl.utils.ModelHelper; + +public class PPO extends BaseGAE { + private final int inner_updates; + private final int inner_batch_size; + private final float ratio_lower_bound; + private final float ratio_upper_bound; + + private NDArray last_gradient_01 = null; + + public PPO(int dim_of_state_space, int num_of_action, int hidden_size, float gamma, float gae_lambda, + float learning_rate, int inner_updates, int inner_batch_size, float ratio_clip) { + super(dim_of_state_space, num_of_action, hidden_size, gamma, gae_lambda, learning_rate); + this.inner_updates = inner_updates; + this.inner_batch_size = inner_batch_size; + this.ratio_lower_bound = 1.0f - ratio_clip; + this.ratio_upper_bound = 1.0f + ratio_clip; + } + + @Override + public void updateModel(NDManager submanager) throws TranslateException { + MemoryBatch batch = memory.getOrderedBatch(submanager); + NDArray states = batch.getStates(); + NDArray actions = batch.getActions(); + + try (Trainer trainer = model.newTrainer(getTrainingConfig())) { + + trainer.initialize(new Shape(1, dim_of_state_space)); + trainer.notifyListeners(listener -> listener.onTrainingBegin(trainer)); + + NDList net_output = trainer.evaluate(new NDList(states)); + + NDArray distribution = ModelHelper.gather( + net_output.get(0).duplicate(), + actions.toIntArray() + ); + NDArray values = net_output.get(1).duplicate(); + + NDArray rewards = batch.getRewards(); + NDList estimates = estimateAdvantage(values.duplicate(), rewards); + NDArray expected_returns = estimates.get(0); + NDArray advantages = estimates.get(1); + + float rewardsValue = rewards.sum().getFloat(); + float lossValue = 0.0f; + + long iters = inner_updates * (1 + actions.size() / inner_batch_size); + for (int i = 0; i < iters; i++) { + + NDArray index = manager.randomInteger(0, actions.size(), new Shape(inner_batch_size), DataType.INT32); + + NDArray states_subset = states.get(index); + NDArray actions_subset = actions.get(index); + NDArray distribution_subset = distribution.get(index); + NDArray expected_returns_subset = expected_returns.get(index); + NDArray advantages_subset = advantages.get(index); + + try (GradientCollector collector = trainer.newGradientCollector()) { + + NDList net_output_updated = trainer.forward(new NDList(states_subset)); + NDArray distribution_updated = ModelHelper.gather( + net_output_updated.get(0), + actions_subset.toIntArray() + ); + NDArray values_updated = net_output_updated.get(1); + + NDArray loss_critic = (expected_returns_subset.sub(values_updated.squeeze())).square().mean(); + + NDArray ratios = distribution_updated.sub(distribution_subset).exp(); + NDArray td_objective = ratios.mul(advantages_subset); + NDArray clipped_td_objective = ratios.clip(ratio_lower_bound, ratio_upper_bound).mul(advantages_subset); + NDArray loss_actor = td_objective.minimum(clipped_td_objective).mean(); + + NDArray distribution_entropy = net_output_updated.get(0).mul( + net_output_updated.get(0).add(1e-2).log() + ).sum().neg(); + + NDArray loss = loss_critic; + loss = loss.sub(loss_actor); + loss = loss.sub(distribution_entropy.mul(0.05)); + + collector.backward(loss); + + float _loss = 0.0f; + try { + _loss = (float) loss.getDouble(); + } catch (IllegalStateException ex) { + ex.printStackTrace(); + _loss = loss.getFloat(); + } + lossValue += _loss; + + NDArray gradient = model.getBlock() + .getChildren().get(1).getValue() + .getParameters().get(0).getValue() + .getArray() + .getGradient() + .duplicate(); + if (gradient.isNaN().any().getBoolean() || gradient.isInfinite().any().getBoolean()) { + throw new IllegalStateException(); + } + last_gradient_01 = gradient; + + trainer.step(); + } + } + + trainer.notifyListeners(listener -> listener.onEpoch(trainer)); + trainer.notifyListeners(listener -> listener.onTrainingEnd(trainer)); + + System.out.println("Iters: " + iters); + System.out.println("Avg loss: " + lossValue / iters); + System.out.println("Total rewards: " + rewardsValue); + + System.out.println(); + + } + } + + private DefaultTrainingConfig getTrainingConfig() { + DefaultTrainingConfig trainingConfig = new DefaultTrainingConfig(Loss.l2Loss()) + .addTrainingListeners(TrainingListener.Defaults.basic()) + .optOptimizer(optimizer); + return trainingConfig; + } + + private NDArray getSample(NDManager submanager, NDArray array, int[] index) { + + Shape shape = Shape.update(array.getShape(), 0, inner_batch_size); + NDArray sample = submanager.zeros(shape, array.getDataType()); + for (int i = 0; i < index.length; i++) { + sample.set(new NDIndex(i), array.get(index[i])); + } + return sample; + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/SAC.java b/src/uet/oop/bomberman/agent/rl/SAC.java new file mode 100644 index 0000000..60f1c05 --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/SAC.java @@ -0,0 +1,421 @@ +package uet.oop.bomberman.agent.rl; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import ai.djl.MalformedModelException; +import ai.djl.Model; +import ai.djl.inference.Predictor; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.NDManager; +import ai.djl.ndarray.index.NDIndex; +import ai.djl.ndarray.types.DataType; +import ai.djl.ndarray.types.Shape; +import ai.djl.training.DefaultTrainingConfig; +import ai.djl.training.GradientCollector; +import ai.djl.training.Trainer; +import ai.djl.training.listener.TrainingListener; +import ai.djl.training.loss.Loss; +import ai.djl.translate.NoopTranslator; +import ai.djl.translate.TranslateException; +import uet.oop.bomberman.agent.rl.base.BaseAgentImpl; +import uet.oop.bomberman.agent.rl.base.BaseGAE; +import uet.oop.bomberman.agent.rl.dtypes.MemoryBatch; +import uet.oop.bomberman.agent.rl.model.sac.DeterministicPolicyNetwork; +import uet.oop.bomberman.agent.rl.model.sac.PolicyNetwork; +import uet.oop.bomberman.agent.rl.model.sac.QNetwork; + +public class SAC extends BaseAgentImpl { + + private static final String FILE_NAME_Q1 = "q1"; + private static final String FILE_NAME_Q2 = "q2"; + private static final String FILE_NAME_POLICY = "policy"; + private static final String FILE_NAME_METADATA = "metadata.json"; + private static final String FILE_NAME_LOGS = "logs.csv"; + private static final float EPSILON = 1e-6f; + + private final int batchSize; + private final int trainingIters; + private final int policyUpdateInterval; + private final float gamma; + private final float alpha; + + private QNetwork q1Network; + private QNetwork q2Network; + private PolicyNetwork policyNetwork; + + private Model q1Model; + private Model q2Model; + private Model policyModel; + private Predictor policyPredictor; + private int policyUpdateCounter = 0; + + private List logs = new ArrayList<>(); + + public static class AgentLog { + public int episode; + public int steps; + public float totalReward; + + public float avgLoss; + public float avgQ1Loss; + public float avgQ2Loss; + public float avgPolicyLoss; + } + + public SAC(Builder builder) { + super(builder); + this.batchSize = builder.batchSize; + this.trainingIters = builder.trainingIters; + this.policyUpdateInterval = builder.policyUpdateInterval; + this.policyUpdateCounter = policyUpdateInterval; + this.gamma = builder.gamma; + this.alpha = builder.alpha; + this.reset(); + } + + @Override + public int sampleAction(NDManager submanager, float[] state) { + try { + NDList output = policyPredictor.predict(new NDList(submanager.create(state))); + int action = policyNetwork.sampleAction(submanager, output) + .toType(DataType.INT32, true) + .getInt(); + return action; + } catch (TranslateException e) { + throw new RuntimeException(e); + } + } + + protected NDArray calculateExpectedReturns(NDArray rewards) { + NDArray expectedReturns = rewards.duplicate(); + for (long i = expectedReturns.getShape().get(0) - 2; i >= 0; i--) { + NDIndex index = new NDIndex(i); + expectedReturns.set(index, expectedReturns.get(i).add(expectedReturns.get(i + 1).mul(gamma))); + } + + return expectedReturns; + } + + @Override + public void updateModel(NDManager submanager) throws TranslateException { + MemoryBatch memoryBatch = memory.getOrderedBatch(submanager); + NDArray states = memoryBatch.getStates(); + NDArray actions = memoryBatch.getActions(); + NDArray nextStates = memoryBatch.getNextStates(); + NDArray rewards = memoryBatch.getRewards(); + NDArray masks = memoryBatch.getMasks(); + NDArray expectedReturns = calculateExpectedReturns(rewards); + + final int[] lastDimension = new int[] { states.getShape().dimension() - 1 }; + + try ( + Trainer q1Trainer = q1Model.newTrainer(getTrainingConfig()); + Trainer q2Trainer = q2Model.newTrainer(getTrainingConfig()); + Trainer policyTrainer = policyModel.newTrainer(getTrainingConfig()) + ) { + q1Trainer.notifyListeners(listener -> listener.onTrainingBegin(q1Trainer)); + q2Trainer.notifyListeners(listener -> listener.onTrainingBegin(q2Trainer)); + policyTrainer.notifyListeners(listener -> listener.onTrainingBegin(policyTrainer)); + + float totalLossQ1 = 0.0f; + float totalLossQ2 = 0.0f; + float totalLossPolicy = 0.0f; + + for (int iter = 0; iter < trainingIters; iter++) { + + NDArray randomIndex = submanager.randomInteger( + 0, + actions.size(), + new Shape(batchSize), + DataType.INT32 + ); + + NDArray batchStates = states.get(randomIndex); + NDArray batchActions = actions.get(randomIndex); + NDArray batchNextStates = nextStates.get(randomIndex); + NDArray batchRewards = rewards.get(randomIndex); + NDArray batchExpectedReturns = expectedReturns.get(randomIndex); + NDArray batchMasks = masks.get(randomIndex); + + // After each line, the shape of the NDArray is shown in the comment + // where N is the batch size, e.g. (N, 1), (N, actionSize) + + NDArray indexAction = batchActions.expandDims(1); // (N, 1) + + NDArray nextPi = policyTrainer.evaluate(new NDList(batchNextStates)).singletonOrThrow(); // (N, actionSize) + NDArray nextQ1 = q1Trainer.evaluate(new NDList(batchNextStates, nextPi)).singletonOrThrow(); // (N) + NDArray nextQ2 = q2Trainer.evaluate(new NDList(batchNextStates, nextPi)).singletonOrThrow(); // (N) + NDArray nextPiSingle = nextPi.max(lastDimension); // (N) + NDArray nextEntropy = nextPiSingle.maximum(EPSILON).log().neg(); // (N) + NDArray nextMinQ = nextQ1.minimum(nextQ2).add(nextEntropy.mul(alpha)); // (N) + NDArray maskNotDone = batchMasks.sub(1).neg(); + NDArray estimatedQ = batchRewards.add(nextMinQ.mul(gamma).mul(maskNotDone)); // (N) + + NDArray actualPiZeros = submanager.zeros(new Shape(batchSize, actionSize)); // (N, actionSize) + NDArray actualPiOnes = submanager.ones(new Shape(batchSize, actionSize)); // (N, actionSize) + NDArray actualPi = actualPiZeros.scatter(indexAction, actualPiOnes, 1); // (N, actionSize) + + try ( + GradientCollector collector = policyTrainer.newGradientCollector(); + ) { + NDArray q1 = q1Trainer.forward(new NDList(batchStates, actualPi)).singletonOrThrow(); + NDArray q2 = q2Trainer.forward(new NDList(batchStates, actualPi)).singletonOrThrow(); + + NDArray q1Loss = q1.sub(estimatedQ).square().mean(); + NDArray q2Loss = q2.sub(estimatedQ).square().mean(); + + totalLossQ1 += q1Loss.getFloat(); + totalLossQ2 += q2Loss.getFloat(); + + NDArray pi = policyTrainer.forward(new NDList(batchStates)).singletonOrThrow(); // (N, actionSize) + NDArray piSingle = pi.max(lastDimension); // (N) + NDArray entropy = piSingle.maximum(EPSILON).log().neg(); // (N) + NDArray minQ = q1.minimum(q2).add(entropy.mul(alpha)); // (N) + NDArray policyLoss = minQ.neg().mean(); + + totalLossPolicy += policyLoss.getFloat(); + + NDArray gradient = q1Model.getBlock() + .getParameters() + .get(2).getValue() + .getArray() + .getGradient() + .duplicate(); + if (gradient.isNaN().any().getBoolean() || gradient.isInfinite().any().getBoolean()) { + throw new IllegalStateException(); + } + + NDArray loss = q1Loss.add(q2Loss); + + collector.backward(loss); + q1Trainer.step(); + q2Trainer.step(); + + policyUpdateCounter -= 1; + if (policyUpdateCounter <= 0) { + policyUpdateCounter = policyUpdateInterval; + collector.zeroGradients(); + collector.backward(policyLoss); + policyTrainer.step(); + } + } + } + + q1Trainer.notifyListeners(listener -> listener.onEpoch(q1Trainer)); + q2Trainer.notifyListeners(listener -> listener.onEpoch(q2Trainer)); + policyTrainer.notifyListeners(listener -> listener.onEpoch(policyTrainer)); + + q1Trainer.notifyListeners(listener -> listener.onTrainingEnd(q1Trainer)); + q2Trainer.notifyListeners(listener -> listener.onTrainingEnd(q2Trainer)); + policyTrainer.notifyListeners(listener -> listener.onTrainingEnd(policyTrainer)); + + float totalLoss = totalLossQ1 + totalLossQ2 + totalLossPolicy; + + System.out.println("Iters: " + trainingIters); + System.out.println("Avg loss: " + totalLoss / trainingIters); + float totalRewards = rewards.sum().getFloat(); + System.out.println("Total rewards: " + totalRewards); + + AgentLog log = new AgentLog(); + log.episode = metadata.episode; + log.steps = (int) states.getShape().get(0); + log.avgLoss = totalLoss / trainingIters; + log.avgQ1Loss = totalLossQ1 / trainingIters; + log.avgQ2Loss = totalLossQ2 / trainingIters; + log.avgPolicyLoss = totalLossPolicy / trainingIters; + log.totalReward = totalRewards; + logs.add(log); + + System.out.println(); + + } + } + + @Override + public void load(String path) { + try { + Path dir = Paths.get(path); + if (!Files.exists(dir)) { + System.out.println("No pre-trained model found"); + return; + } + q1Model = loadParams(path, FILE_NAME_Q1, q1Model); + q2Model = loadParams(path, FILE_NAME_Q2, q2Model); + policyModel = loadParams(path, FILE_NAME_POLICY, policyModel); + + Path pathMetadata = Paths.get(path, FILE_NAME_METADATA); + if (Files.exists(pathMetadata)) { + metadata = new ObjectMapper().readValue(pathMetadata.toFile(), AgentMetadata.class); + } else { + System.out.println("No metadata found"); + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private Model loadParams(String dir, String fileName, Model model) throws IOException, MalformedModelException { + Path path = Paths.get(dir, fileName); + model.load(path, "model"); + // try (DataInputStream is = new DataInputStream(Files.newInputStream(path))) { + // model.getBlock().loadParameters(manager, is); + // } + return model; + } + + @Override + public void save(String path) { + try { + Path dir = Paths.get(path); + Files.createDirectories(dir); + saveParams(path, FILE_NAME_Q1, q1Model); + saveParams(path, FILE_NAME_Q2, q2Model); + saveParams(path, FILE_NAME_POLICY, policyModel); + + Path pathMetadata = Paths.get(path, FILE_NAME_METADATA); + try (BufferedWriter bw = new BufferedWriter(new FileWriter(pathMetadata.toFile()))) { + new ObjectMapper().writeValue(bw, metadata); + } + + Path pathCsv = Paths.get(path, FILE_NAME_LOGS); + if (!Files.exists(pathCsv)) { + pathCsv.toFile().createNewFile(); + try (BufferedWriter bw = new BufferedWriter(new FileWriter(pathCsv.toFile(), false))) { + String header = Arrays.stream(AgentLog.class.getFields()) + .filter(field -> Modifier.isPublic(field.getModifiers())) + .map(Field::getName) + .collect(Collectors.joining(",")); + bw.write(header); + bw.newLine(); + } catch (SecurityException e) { + e.printStackTrace(); + } + } + try (BufferedWriter bw = new BufferedWriter(new FileWriter(pathCsv.toFile(), true))) { + for (AgentLog log : logs) { + String line = Arrays.stream(AgentLog.class.getFields()) + .filter(field -> Modifier.isPublic(field.getModifiers())) + .map(field -> { + try { + return field.get(log).toString(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + return ""; + } + }) + .collect(Collectors.joining(",")); + bw.write(line); + bw.newLine(); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void saveParams(String dir, String fileName, Model model) throws IOException { + Path path = Paths.get(dir, fileName); + + Files.createDirectories(path); + model.save(path, "model"); + } + + @Override + public void reset() { + if (manager != null) { + manager.close(); + } + manager = NDManager.newBaseManager(); + + q1Network = QNetwork.builder() + .setManager(manager) + .setStateSize(stateSize) + .setActionSize(actionSize) + .build(); + q1Network.initialize(manager, DataType.FLOAT32, new Shape(stateSize), new Shape(actionSize)); + q1Model = q1Network.toModel(); + + q2Network = QNetwork.builder() + .setManager(manager) + .setStateSize(stateSize) + .setActionSize(actionSize) + .build(); + q2Network.initialize(manager, DataType.FLOAT32, new Shape(stateSize), new Shape(actionSize)); + q2Model = q2Network.toModel(); + + policyNetwork = DeterministicPolicyNetwork.builder() + .setManager(manager) + .setStateSize(stateSize) + .setActionSize(actionSize) + .build(); + policyNetwork.initialize(manager, DataType.FLOAT32, new Shape(stateSize)); + policyModel = policyNetwork.toModel(); + policyPredictor = policyModel.newPredictor(new NoopTranslator()); + } + + private DefaultTrainingConfig getTrainingConfig() { + DefaultTrainingConfig trainingConfig = new DefaultTrainingConfig(Loss.l2Loss()) + .addTrainingListeners(TrainingListener.Defaults.basic()) + .optOptimizer(optimizer); + return trainingConfig; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends BaseGAE.BaseBuilder { + + private int batchSize = 128; + private int trainingIters = 10; + private int policyUpdateInterval = 1; + private float gamma = 0.99f; + private float alpha = 0.2f; + + public Builder optBatchSize(int batchSize) { + this.batchSize = batchSize; + return self(); + } + + public Builder optTrainingIters(int trainingIters) { + this.trainingIters = trainingIters; + return self(); + } + + public Builder optPolicyUpdateInterval(int policyUpdateInterval) { + this.policyUpdateInterval = policyUpdateInterval; + return self(); + } + + public Builder optGamma(float gamma) { + this.gamma = gamma; + return self(); + } + + public Builder optAlpha(float alpha) { + this.alpha = alpha; + return self(); + } + + public SAC build() { + return new SAC(this); + } + + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/base/BaseAgent.java b/src/uet/oop/bomberman/agent/rl/base/BaseAgent.java new file mode 100644 index 0000000..d5d1c7e --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/base/BaseAgent.java @@ -0,0 +1,51 @@ +package uet.oop.bomberman.agent.rl.base; + +public abstract class BaseAgent { + private boolean is_eval = false; + + /** + * Calculate the action to the input state + * + * @param state + * @return action + */ + public abstract int react(float[] state); + public abstract int react(float[] state, int action); + + /** + * Collect the result of the previous action + * + * @param reward + * @param done + */ + public abstract void collect(float reward, boolean done); + + /** + * Reset the agent. + */ + public abstract void reset(); + + /** + * Switch to the training mode. + */ + public final void train() { + this.is_eval = false; + } + + /** + * Switch to the inference mode. + */ + public final void eval() { + this.is_eval = true; + } + + /** + * Check if the agent is in the inference mode. + * + * @return true if the agent is in the inference mode. + */ + public final boolean isEval() { + return is_eval; + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/base/BaseAgentImpl.java b/src/uet/oop/bomberman/agent/rl/base/BaseAgentImpl.java new file mode 100644 index 0000000..311af04 --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/base/BaseAgentImpl.java @@ -0,0 +1,155 @@ +package uet.oop.bomberman.agent.rl.base; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.Supplier; + +import ai.djl.ndarray.NDManager; +import ai.djl.training.optimizer.Optimizer; +import ai.djl.translate.TranslateException; +import uet.oop.bomberman.agent.rl.dtypes.Memory; + +public abstract class BaseAgentImpl extends BaseAgent { + + private static final float EXPLORE_RATE = 0.05f; + protected final Random random = new Random(); + + protected Memory memory = new Memory(65535); + protected Optimizer optimizer; + protected int stateSize; + protected int actionSize; + + protected NDManager manager; + + protected Supplier getActionMask; + + protected AgentMetadata metadata = new AgentMetadata(); + + public static class AgentMetadata implements Serializable { + public int episode = 0; + } + + protected BaseAgentImpl(Optimizer optimizer, int stateSize, int actionSize) { + this( + builder() + .setOptimizer(optimizer) + .setStateSize(stateSize) + .setActionSize(actionSize) + ); + } + + protected BaseAgentImpl(BaseBuilder builder) { + this.manager = NDManager.newBaseManager(); + this.optimizer = builder.optimizer; + this.stateSize = builder.stateSize; + this.actionSize = builder.actionSize; + } + + public void setActionMaskGetter(Supplier getActionMask) { + this.getActionMask = getActionMask; + } + + @Override + public int react(float[] state) { + int action = sampleAction(state); + return react(state, action); + } + + @Override + public int react(float[] state, int action) { + if (!isEval()) { + memory.setState(state); + } + if (!isEval()) { + memory.setAction(action); + } + + return action; + + } + + private int sampleAction(float[] state) { + int action; + if (random.nextFloat() < EXPLORE_RATE) { + action = randomAction(); + } else { + try (NDManager submanager = manager.newSubManager()) { + action = sampleAction(submanager, state); + } catch (RuntimeException ex) { + ex.printStackTrace(); + action = randomAction(); + } + System.out.println(" action: " + action); + } + return action; + } + + private int randomAction() { + int action; + Boolean[] actionMask = getActionMask.get(); + List validActions = new ArrayList<>(); + for (int i = 0; i < actionSize; i++) { + if (actionMask[i]) { + validActions.add(i); + } + } + action = validActions.get(random.nextInt(validActions.size())); + return action; + } + + public abstract int sampleAction(NDManager submanager, float[] state); + public abstract void load(String path); + public abstract void save(String path); + + @Override + public void collect(float reward, boolean done) { + if (!isEval()) { + memory.setRewardAndMask(reward, done); + if (done) { + try { + updateModel(manager); + } catch (TranslateException e) { + throw new IllegalStateException(e); + } + metadata.episode += 1; + memory.reset(); + } + } + } + + private static BaseBuilder builder() { + return new BaseBuilder<>(); + } + + public abstract void updateModel(NDManager submanager) throws TranslateException; + + public static class BaseBuilder> { + + private Optimizer optimizer; + private int stateSize; + private int actionSize; + + public T setOptimizer(Optimizer optimizer) { + this.optimizer = optimizer; + return self(); + } + + public T setStateSize(int stateSize) { + this.stateSize = stateSize; + return self(); + } + + public T setActionSize(int actionSize) { + this.actionSize = actionSize; + return self(); + } + + @SuppressWarnings("unchecked") + protected final T self() { + return (T) this; + } + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/base/BaseGAE.java b/src/uet/oop/bomberman/agent/rl/base/BaseGAE.java new file mode 100644 index 0000000..2df74e9 --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/base/BaseGAE.java @@ -0,0 +1,120 @@ +package uet.oop.bomberman.agent.rl.base; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import ai.djl.MalformedModelException; +import ai.djl.Model; +import ai.djl.inference.Predictor; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.NDManager; +import ai.djl.ndarray.index.NDIndex; +import ai.djl.training.optimizer.Optimizer; +import ai.djl.training.tracker.Tracker; +import ai.djl.translate.NoopTranslator; +import ai.djl.translate.TranslateException; +import uet.oop.bomberman.agent.rl.model.DistributionValueModel; +import uet.oop.bomberman.agent.rl.utils.ActionSampler; + +public abstract class BaseGAE extends BaseAgentImpl { + + private final float gae_lambda; + private final float gamma; + protected final int num_of_action; + protected final int dim_of_state_space; + private final int hidden_size; + + protected Model model; + protected Predictor predictor; + + public BaseGAE(int dim_of_state_space, int num_of_action, int hidden_size, float gamma, float gae_lambda, + float learning_rate) { + super( + Optimizer.adam() + .optLearningRateTracker(Tracker.fixed(learning_rate)) + .build(), + dim_of_state_space, + num_of_action + ); + this.gae_lambda = gae_lambda; + this.gamma = gamma; + this.dim_of_state_space = dim_of_state_space; + this.num_of_action = num_of_action; + this.hidden_size = hidden_size; + this.reset(); + } + + @Override + public void load(String path) { + try { + Path dir = Paths.get("models"); + File file = new File("models/PPO.params"); + Files.createDirectories(dir); + DataInputStream is = new DataInputStream(Files.newInputStream(file.toPath())); + model.getBlock().loadParameters(manager, is); + } catch (NoSuchFileException e) { + System.out.println("No pre-trained model found"); + } catch (MalformedModelException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Override + public void save(String path) { + try { + File file = new File("models/PPO.params"); + + DataOutputStream os = new DataOutputStream(Files.newOutputStream(file.toPath())); + model.getBlock().saveParameters(os); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Override + public int sampleAction(NDManager submanager, float[] state) { + try { + NDArray prob = predictor.predict(new NDList(submanager.create(state))).get(0); + int action = ActionSampler.sampleMultinomial(prob, random); + return action; + } catch (TranslateException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public void reset() { + if (manager != null) { + manager.close(); + } + manager = NDManager.newBaseManager(); + model = DistributionValueModel.newModel(manager, dim_of_state_space, hidden_size, num_of_action); + predictor = model.newPredictor(new NoopTranslator()); + } + + protected NDList estimateAdvantage(NDArray values, NDArray rewards) { + NDArray expected_returns = rewards.duplicate(); + NDArray advantages = rewards.sub(values.squeeze()); + for (long i = expected_returns.getShape().get(0) - 2; i >= 0; i--) { + NDIndex index = new NDIndex(i); + expected_returns.set(index, expected_returns.get(i).add(expected_returns.get(i + 1).mul(gamma))); + advantages.set(index, + advantages.get(i).add(values.get(i + 1).add(advantages.get(i + 1).mul(gae_lambda)).mul(gamma))); + } + + return new NDList(expected_returns, advantages); + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/dtypes/Memory.java b/src/uet/oop/bomberman/agent/rl/dtypes/Memory.java new file mode 100644 index 0000000..70d1ccf --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/dtypes/Memory.java @@ -0,0 +1,166 @@ +package uet.oop.bomberman.agent.rl.dtypes; + +import java.util.Arrays; +import java.util.Random; + +import ai.djl.ndarray.NDManager; + +public final class Memory { + private final Random random; + private final int capacity; + private final Transition[] memory; + + private float[] state_prev; + private int action; + private float reward; + private boolean mask; + private int stage; + private int head; + private int size; + + public Memory(int capacity) { + this(capacity, 0); + } + + public Memory(int capacity, int seed) { + this.capacity = capacity; + this.memory = new Transition[capacity]; + this.random = new Random(seed); + + reset(); + } + + public void setState(float[] state) { + assertStage(0); + if (state_prev != null) { + add(new Transition(state_prev, state, action, reward, mask)); + } + state_prev = state; + + } + + public void setAction(int action) { + assertStage(1); + this.action = action; + } + + public void setRewardAndMask(float reward, boolean mask) { + assertStage(2); + this.reward = reward; + this.mask = mask; + + if (mask) { + add(new Transition(state_prev, null, action, reward, mask)); + state_prev = null; + action = -1; + } + + } + + public Transition[] sample(int sample_size) { + Transition[] chunk = new Transition[sample_size]; + for (int i = 0; i < sample_size; i++) { + chunk[i] = memory[random.nextInt(size)]; + } + + return chunk; + } + + public MemoryBatch sampleBatch(int sample_size, NDManager manager) { + return getBatch(sample(sample_size), manager, sample_size); + } + + public MemoryBatch getOrderedBatch(NDManager manager) { + return getBatch(memory, manager, size()); + } + + public Transition get(int index) { + if (index < 0 || index >= size) { + throw new ArrayIndexOutOfBoundsException("Index out of bound " + index); + } + return memory[index]; + } + + public int size() { + return size; + } + + public void reset() { + state_prev = null; + action = -1; + reward = 0.0F; + mask = false; + stage = 0; + head = -1; + size = 0; + } + + @Override + public String toString() { + return Arrays.toString(memory); + } + + private void add(Transition transition) { + head += 1; + if (head >= capacity) { + head = 0; + } + + memory[head] = transition; + if (size < capacity) { + size++; + } + } + + private void assertStage(int i) { + if (i != stage) { + String info_name; + switch (stage) { + case 0: + info_name = "State"; + break; + case 1: + info_name = "Action"; + break; + case 2: + info_name = "Reward and Mask"; + break; + default: + info_name = null; + } + throw new IllegalStateException("Expected information: " + info_name); + } else { + stage++; + if (stage > 2) { + stage = 0; + } + } + } + + private MemoryBatch getBatch(Transition[] transitions, NDManager manager, int batch_size) { + + float[][] states = new float[batch_size][]; + float[][] next_states = new float[batch_size][]; + int[] actions = new int[batch_size]; + float[] rewards = new float[batch_size]; + int[] masks = new int[batch_size]; + + int index = head; + for (int i = 0; i < batch_size; i++) { + index++; + if (index >= batch_size) { + index = 0; + } + states[i] = transitions[index].getState(); + float[] next_state = transitions[index].getNextState(); + next_states[i] = next_state != null ? next_state : new float[states[i].length]; + actions[i] = transitions[index].getAction(); + rewards[i] = transitions[index].getReward(); + masks[i] = transitions[index].isMasked() ? 1 : 0; + } + + return new MemoryBatch(manager.create(states), manager.create(next_states), manager.create(actions), + manager.create(rewards), manager.create(masks)); + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/dtypes/MemoryBatch.java b/src/uet/oop/bomberman/agent/rl/dtypes/MemoryBatch.java new file mode 100644 index 0000000..ad441fd --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/dtypes/MemoryBatch.java @@ -0,0 +1,32 @@ +package uet.oop.bomberman.agent.rl.dtypes; + +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; + +public final class MemoryBatch extends NDList { + private static final long serialVersionUID = 1L; + + public MemoryBatch(NDArray... arrays) { + super(arrays); + } + + public NDArray getActions() { + return get(2); + } + + public NDArray getRewards() { + return get(3); + } + + public NDArray getMasks() { + return get(4); + } + + public NDArray getStates() { + return get(0); + } + + public NDArray getNextStates() { + return get(1); + } +} diff --git a/src/uet/oop/bomberman/agent/rl/dtypes/Snapshot.java b/src/uet/oop/bomberman/agent/rl/dtypes/Snapshot.java new file mode 100644 index 0000000..84e600b --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/dtypes/Snapshot.java @@ -0,0 +1,44 @@ +package uet.oop.bomberman.agent.rl.dtypes; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class Snapshot { + private final float[] state; + private final float reward; + private final boolean mask; + + public Snapshot(float[] state, float reward, boolean mask) { + this.state = state.clone(); + this.reward = reward; + this.mask = mask; + } + + public final float[] getState() { + return state; + } + + public final float getReward() { + return reward; + } + + public final boolean isMasked() { + return mask; + } + + @Override + public String toString() { + try { + Map map = new HashMap<>(); + map.put("state", state); + map.put("reward", reward); + map.put("mask", mask); + return new ObjectMapper().writeValueAsString(map); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/uet/oop/bomberman/agent/rl/dtypes/Transition.java b/src/uet/oop/bomberman/agent/rl/dtypes/Transition.java new file mode 100644 index 0000000..384b884 --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/dtypes/Transition.java @@ -0,0 +1,43 @@ +package uet.oop.bomberman.agent.rl.dtypes; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public final class Transition extends Snapshot { + + private final float[] state_next; + private final int action; + + public Transition(float[] state, float[] state_next, int action, float reward, boolean mask) { + super(state, reward, mask); + this.state_next = state_next != null ? state_next.clone() : null; + this.action = action; + } + + public final float[] getNextState() { + return state_next; + } + + public final int getAction() { + return action; + } + + @Override + public String toString() { + try { + Map map = new HashMap<>(); + map.put("state", getState()); + map.put("state_next", state_next); + map.put("action", action); + map.put("reward", getReward()); + map.put("mask", isMasked()); + return new ObjectMapper().writeValueAsString(map); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/model/BaseModel.java b/src/uet/oop/bomberman/agent/rl/model/BaseModel.java new file mode 100644 index 0000000..239bf24 --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/model/BaseModel.java @@ -0,0 +1,19 @@ +package uet.oop.bomberman.agent.rl.model; + +import ai.djl.ndarray.NDManager; +import ai.djl.nn.AbstractBlock; + +public abstract class BaseModel extends AbstractBlock { + private static final byte VERSION = 2; + private final NDManager manager; + + public BaseModel(NDManager manager) { + super(VERSION); + this.manager = manager; + } + + public NDManager getManager() { + return manager; + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/model/BaseNetwork.java b/src/uet/oop/bomberman/agent/rl/model/BaseNetwork.java new file mode 100644 index 0000000..713f90b --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/model/BaseNetwork.java @@ -0,0 +1,47 @@ +package uet.oop.bomberman.agent.rl.model; + +import ai.djl.Model; +import ai.djl.ndarray.NDManager; +import ai.djl.ndarray.types.DataType; +import ai.djl.ndarray.types.Shape; +import ai.djl.nn.Parameter; +import ai.djl.training.initializer.ConstantInitializer; +import ai.djl.training.initializer.NormalInitializer; + +public abstract class BaseNetwork extends BaseModel { + + protected BaseNetwork(BaseBuilder builder) { + super(builder.manager); + setInitializer(new NormalInitializer(), Parameter.Type.WEIGHT); + setInitializer(new ConstantInitializer(1f), Parameter.Type.GAMMA); + setInitializer(new ConstantInitializer(0f), Parameter.Type.BETA); + } + + @Override + protected abstract void initializeChildBlocks( + NDManager manager, + DataType dataType, + Shape... inputShapes + ); + + public abstract Model toModel(); + + public static abstract class BaseBuilder>{ + + private NDManager manager; + + public S setManager(NDManager manager) { + this.manager = manager; + return self(); + } + + @SuppressWarnings("unchecked") + protected final S self() { + return (S) this; + } + + public abstract BaseNetwork build(); + + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/model/DistributionValueModel.java b/src/uet/oop/bomberman/agent/rl/model/DistributionValueModel.java new file mode 100644 index 0000000..91243cb --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/model/DistributionValueModel.java @@ -0,0 +1,152 @@ +package uet.oop.bomberman.agent.rl.model; + +import ai.djl.Model; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.NDManager; +import ai.djl.ndarray.types.DataType; +import ai.djl.ndarray.types.Shape; +import ai.djl.nn.Activation; +import ai.djl.nn.Block; +import ai.djl.nn.Blocks; +import ai.djl.nn.Parameter; +import ai.djl.nn.SequentialBlock; +import ai.djl.nn.core.Linear; +import ai.djl.nn.norm.BatchNorm; +import ai.djl.training.ParameterStore; +import ai.djl.training.initializer.ConstantInitializer; +import ai.djl.training.initializer.NormalInitializer; +import ai.djl.training.initializer.XavierInitializer; +import ai.djl.util.PairList; + +public class DistributionValueModel extends BaseModel { + private static final float LAYERNORM_MOMENTUM = 0.9999f; + private static final float LAYERNORM_EPSILON = 1e-5f; + + private final Block linear_input; + private final Block linear_action; + private final Block linear_value; + + public NDArray last_linear_input; + public NDArray last_linear_action; + public NDArray last_linear_action_norm; + public NDArray last_linear_action_dist; + public NDArray last_linear_value; + + private final int hidden_size; + private final int output_size; + private final Parameter gamma; + private final Parameter beta; + + private boolean isFirstUpdate = true; + private float moving_mean = 0.0f; + private float moving_var = 1.0f; + + private DistributionValueModel(NDManager manager, int hidden_size, int output_size) { + super(manager); + Block input_block = new SequentialBlock() + .add(Linear.builder().setUnits(hidden_size).build()) + .add(Activation.reluBlock()) + .add(Linear.builder().setUnits(hidden_size).build()) + .add(Activation.reluBlock()) + ; + Block action_block = new SequentialBlock() + .add(Linear.builder().setUnits(output_size).build()) + ; + this.linear_input = addChildBlock("linear_input", input_block); + this.linear_action = addChildBlock("linear_action", action_block); + this.linear_value = addChildBlock("linear_value", Linear.builder().setUnits(1).build()); + + Parameter pGamma = Parameter.builder() + .setName("gamma") + .setType(Parameter.Type.GAMMA) + .optRequiresGrad(true) + .optShape(new Shape(1)) + .build(); + this.gamma = addParameter(pGamma); + + Parameter pBeta = Parameter.builder() + .setName("beta") + .setType(Parameter.Type.BETA) + .optRequiresGrad(true) + .optShape(new Shape(1)) + .build(); + this.beta = addParameter(pBeta); + + + this.hidden_size = hidden_size; + this.output_size = output_size; + } + + public static Model newModel(NDManager manager, int input_size, int hidden_size, int output_size) { + Model model = Model.newInstance("DistributionValueModel"); + BaseModel net = new DistributionValueModel(manager, hidden_size, output_size); + net.initialize(net.getManager(), DataType.FLOAT32, new Shape(input_size)); + model.setBlock(net); + + return model; + } + + @Override + protected NDList forwardInternal(ParameterStore parameter_store, NDList inputs, boolean training, + PairList params) { + + NDList hidden = linear_input.forward(parameter_store, inputs, training); + last_linear_input = hidden.singletonOrThrow(); + + NDArray output_action = linear_action.forward(parameter_store, hidden, training).singletonOrThrow(); + last_linear_action = output_action; + // output_action = normalize(output_action, training); + // last_linear_action_norm = output_action; + + NDArray distribution = output_action.softmax(output_action.getShape().dimension() - 1); + last_linear_action_dist = distribution; + + NDArray value = linear_value.forward(parameter_store, hidden, training).singletonOrThrow(); + last_linear_value = value; + + return new NDList(distribution, value); + } + + @Override + public Shape[] getOutputShapes(Shape[] inputShapes) { + return new Shape[] { new Shape(output_size), new Shape(1) }; + } + + @Override + public void initializeChildBlocks(NDManager manager, DataType data_type, Shape... input_shapes) { + setInitializer(new NormalInitializer(), Parameter.Type.WEIGHT); + setInitializer(new ConstantInitializer(1f), Parameter.Type.GAMMA); + setInitializer(new ConstantInitializer(0f), Parameter.Type.BETA); + + linear_input.initialize(manager, data_type, input_shapes); + linear_action.initialize(manager, data_type, new Shape(hidden_size)); + linear_value.initialize(manager, data_type, new Shape(hidden_size)); + gamma.initialize(manager, data_type); + beta.initialize(manager, data_type); + } + + private NDArray normalize(NDArray arr, boolean training) { + int last_dimension = arr.getShape().dimension() - 1; + NDArray score_mean = arr.mean(new int[]{last_dimension}).expandDims(last_dimension); + NDArray score_var = arr.sub(score_mean).pow(2) + .mean(new int[]{last_dimension}) + .expandDims(last_dimension); + // float score_mean = arr.mean().getFloat(); + // float score_var = arr.sub(score_mean).pow(2).mean().getFloat(); + // if (isFirstUpdate) { + // moving_mean = score_mean; + // moving_var = score_var; + // isFirstUpdate = false; + // } else { + // moving_mean = moving_mean * LAYERNORM_MOMENTUM + score_mean * (1.0f - LAYERNORM_MOMENTUM); + // moving_var = moving_var * LAYERNORM_MOMENTUM + score_var * (1.0f - LAYERNORM_MOMENTUM); + // } + return arr + .sub(score_mean) + .div(score_var.add(LAYERNORM_EPSILON).sqrt()) + .mul(gamma.getArray()) + .add(beta.getArray()); + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/model/sac/DeterministicPolicyNetwork.java b/src/uet/oop/bomberman/agent/rl/model/sac/DeterministicPolicyNetwork.java new file mode 100644 index 0000000..18ba5f5 --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/model/sac/DeterministicPolicyNetwork.java @@ -0,0 +1,178 @@ +package uet.oop.bomberman.agent.rl.model.sac; + +import java.util.Random; + +import ai.djl.Model; +import ai.djl.inference.Predictor; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDArrays; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.NDManager; +import ai.djl.ndarray.types.DataType; +import ai.djl.ndarray.types.Shape; +import ai.djl.nn.Activation; +import ai.djl.nn.Block; +import ai.djl.nn.SequentialBlock; +import ai.djl.nn.core.Linear; +import ai.djl.training.ParameterStore; +import ai.djl.translate.NoopTranslator; +import ai.djl.translate.TranslateException; +import ai.djl.util.PairList; +import uet.oop.bomberman.agent.rl.utils.ActionSampler; + +public class DeterministicPolicyNetwork extends PolicyNetwork { + + private final Random random = new Random(); + + private final int actionSize; + + private Block block; + + protected DeterministicPolicyNetwork(Builder builder) { + super(builder); + this.actionSize = builder.actionSize; + + SequentialBlock block = new SequentialBlock(); + for (int i = 0; i < builder.numHiddenLayer; i++) { + block.add(Linear.builder().setUnits(builder.hiddenSize).build()); + block.add(Activation.reluBlock()); + } + block.add(Linear.builder().setUnits(builder.actionSize).build()); + this.block = addChildBlock("Network", block); + } + + @Override + public Shape[] getOutputShapes(Shape[] inputShapes) { + final Shape[] OUTPUT_SHAPE_SINGLE = new Shape[] {new Shape(actionSize)}; + long batchSize = -1; + for (Shape shape : inputShapes) { + if (shape.dimension() <= 1) { + return OUTPUT_SHAPE_SINGLE; + } + long _batch_size = shape.get(0); + if (batchSize == -1) { + batchSize = _batch_size; + } else if (batchSize != _batch_size) { + throw new IllegalArgumentException("Inconsistent batch size"); + } + } + return new Shape[] {new Shape(batchSize, actionSize)}; + } + + @Override + protected NDList forwardInternal(ParameterStore parameterStore, NDList inputs, boolean training, + PairList params) { + int lastDimension = inputs.get(0).getShape().dimension() - 1; + NDArray input = NDArrays.concat(inputs, lastDimension); + NDArray output = block.forward(parameterStore, new NDList(input), training, params).singletonOrThrow(); + output = output.softmax(output.getShape().dimension() - 1); + return new NDList(output); + } + + @Override + protected void initializeChildBlocks(NDManager manager, DataType dataType, Shape... inputShapes) { + int dimensions = inputShapes[0].dimension(); + int lastDimensionSize = 0; + for (Shape shape: inputShapes) { + lastDimensionSize += shape.get(dimensions - 1); + } + block.initialize(manager, dataType, new Shape(lastDimensionSize)); + } + + @Override + public NDArray sampleAction(NDManager manager, NDList modelOutput) { + // modelOutput[0].shape should be either (N, actionSize) or (actionSize,) + // after sampling, the shape should be (N,) or () + + // NDArray dist = modelOutput.singletonOrThrow(); + // if (dist.getShape().dimension() == 1) { + // int action = ActionSampler.sampleMultinomial(dist, random); + // return manager.create(action); + // } else { + // int N = (int) dist.getShape().get(0); + // int[] actions = new int[N]; + // for (int i = 0; i < N; i++) { + // actions[i] = ActionSampler.sampleMultinomial(dist.get(i), random); + // } + // return manager.create(actions); + // } + + return modelOutput.singletonOrThrow().argMax(-1); + } + + @Override + public Model toModel() { + Model model = Model.newInstance("DeterministicPolicyNetwork"); + model.setBlock(this); + return model; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends BaseBuilder { + + private int stateSize; + private int actionSize; + private int hiddenSize = 64; + private int numHiddenLayer = 1; + + private Builder() { + } + + public Builder setStateSize(int inputSize) { + this.stateSize = inputSize; + return self(); + } + + public Builder setActionSize(int actionSize) { + this.actionSize = actionSize; + return self(); + } + + public Builder optHiddenSize(int hiddenSize) { + this.hiddenSize = hiddenSize; + return self(); + } + + public Builder optNumLayer(int numLayer) { + this.numHiddenLayer = numLayer; + return self(); + } + + @Override + public DeterministicPolicyNetwork build() { + return new DeterministicPolicyNetwork(this); + } + + } + + public static void main(String[] args) { + NDManager manager = NDManager.newBaseManager(); + DeterministicPolicyNetwork network = DeterministicPolicyNetwork.builder() + .setManager(manager) + .setStateSize(4) + .setActionSize(2) + .optHiddenSize(64) + .optNumLayer(2) + .build(); + Model model = network.toModel(); + Predictor predictor = model.newPredictor(new NoopTranslator()); + long batchSize = 8; + NDArray state = manager.ones(new Shape(batchSize, 4)); + NDArray action = manager.ones(new Shape(batchSize, 2)); + NDList result = null; + try { + result = predictor.predict(new NDList(state, action)); + } catch (TranslateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + NDArray resultArray = result.singletonOrThrow(); + if (!resultArray.getShape().equals(new Shape(batchSize, 1))) { + throw new IllegalStateException("Invalid output shape"); + } + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/model/sac/IPolicyNetwork.java b/src/uet/oop/bomberman/agent/rl/model/sac/IPolicyNetwork.java new file mode 100644 index 0000000..1ca2b67 --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/model/sac/IPolicyNetwork.java @@ -0,0 +1,11 @@ +package uet.oop.bomberman.agent.rl.model.sac; + +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.NDManager; + +public interface IPolicyNetwork { + + public NDArray sampleAction(NDManager manager, NDList modelOutput); + +} diff --git a/src/uet/oop/bomberman/agent/rl/model/sac/PolicyNetwork.java b/src/uet/oop/bomberman/agent/rl/model/sac/PolicyNetwork.java new file mode 100644 index 0000000..f55bb4a --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/model/sac/PolicyNetwork.java @@ -0,0 +1,11 @@ +package uet.oop.bomberman.agent.rl.model.sac; + +import uet.oop.bomberman.agent.rl.model.BaseNetwork; + +public abstract class PolicyNetwork extends BaseNetwork implements IPolicyNetwork { + + public PolicyNetwork(BaseNetwork.BaseBuilder builder) { + super(builder); + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/model/sac/QNetwork.java b/src/uet/oop/bomberman/agent/rl/model/sac/QNetwork.java new file mode 100644 index 0000000..3d91e1b --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/model/sac/QNetwork.java @@ -0,0 +1,149 @@ +package uet.oop.bomberman.agent.rl.model.sac; + +import java.util.Arrays; + +import ai.djl.Model; +import ai.djl.inference.Predictor; +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.NDArrays; +import ai.djl.ndarray.NDList; +import ai.djl.ndarray.NDManager; +import ai.djl.ndarray.types.DataType; +import ai.djl.ndarray.types.Shape; +import ai.djl.nn.Activation; +import ai.djl.nn.Block; +import ai.djl.nn.SequentialBlock; +import ai.djl.nn.core.Linear; +import ai.djl.training.ParameterStore; +import ai.djl.translate.NoopTranslator; +import ai.djl.translate.TranslateException; +import ai.djl.util.PairList; +import uet.oop.bomberman.agent.rl.model.BaseNetwork; + +public class QNetwork extends BaseNetwork { + + private Block block; + + protected QNetwork(Builder builder) { + super(builder); + SequentialBlock block = new SequentialBlock(); + for (int i = 0; i < builder.numHiddenLayer; i++) { + block.add(Linear.builder().setUnits(builder.hiddenSize).build()); + block.add(Activation.reluBlock()); + } + block.add(Linear.builder().setUnits(1).build()); + this.block = addChildBlock("QNetwork", block); + } + + @Override + public Shape[] getOutputShapes(Shape[] inputShapes) { + final Shape[] OUTPUT_SHAPE_SINGLE = new Shape[] {new Shape()}; + long batchSize = -1; + for (Shape shape : inputShapes) { + if (shape.dimension() <= 1) { + return OUTPUT_SHAPE_SINGLE; + } + long _batch_size = shape.get(0); + if (batchSize == -1) { + batchSize = _batch_size; + } else if (batchSize != _batch_size) { + throw new IllegalArgumentException("Inconsistent batch size"); + } + } + return new Shape[] {new Shape(batchSize)}; + } + + @Override + protected NDList forwardInternal(ParameterStore parameterStore, NDList inputs, boolean training, + PairList params) { + int lastDimension = inputs.get(0).getShape().dimension() - 1; + NDArray input = NDArrays.concat(inputs, lastDimension); + NDArray output = block.forward(parameterStore, new NDList(input), training, params).singletonOrThrow(); + return new NDList(output.squeeze(-1)); + } + + @Override + protected void initializeChildBlocks(NDManager manager, DataType dataType, Shape... inputShapes) { + int dimensions = inputShapes[0].dimension(); + int lastDimensionSize = 0; + for (Shape shape: inputShapes) { + lastDimensionSize += shape.get(dimensions - 1); + } + block.initialize(manager, dataType, new Shape(lastDimensionSize)); + } + + public Model toModel() { + Model model = Model.newInstance("QNetwork"); + model.setBlock(this); + return model; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends BaseBuilder { + + private int stateSize; + private int actionSize; + private int hiddenSize = 64; + private int numHiddenLayer = 1; + + private Builder() { + } + + public Builder setStateSize(int inputSize) { + this.stateSize = inputSize; + return self(); + } + + public Builder setActionSize(int actionSize) { + this.actionSize = actionSize; + return self(); + } + + public Builder optHiddenSize(int hiddenSize) { + this.hiddenSize = hiddenSize; + return self(); + } + + public Builder optNumLayer(int numLayer) { + this.numHiddenLayer = numLayer; + return self(); + } + + @Override + public QNetwork build() { + return new QNetwork(this); + } + + } + + public static void main(String[] args) { + NDManager manager = NDManager.newBaseManager(); + QNetwork network = QNetwork.builder() + .setManager(manager) + .setStateSize(4) + .setActionSize(2) + .optHiddenSize(64) + .optNumLayer(2) + .build(); + Model model = network.toModel(); + Predictor predictor = model.newPredictor(new NoopTranslator()); + long batchSize = 8; + NDArray state = manager.ones(new Shape(batchSize, 4)); + NDArray action = manager.ones(new Shape(batchSize, 2)); + NDList result = null; + try { + result = predictor.predict(new NDList(state, action)); + } catch (TranslateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + NDArray resultArray = result.singletonOrThrow(); + if (!resultArray.getShape().equals(new Shape(batchSize, 1))) { + throw new IllegalStateException("Invalid output shape"); + } + } + +} diff --git a/src/uet/oop/bomberman/agent/rl/utils/ActionSampler.java b/src/uet/oop/bomberman/agent/rl/utils/ActionSampler.java new file mode 100644 index 0000000..7ecf501 --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/utils/ActionSampler.java @@ -0,0 +1,36 @@ +package uet.oop.bomberman.agent.rl.utils; + +import java.util.Random; + +import ai.djl.ndarray.NDArray; + +public final class ActionSampler { + public static int epsilonGreedy(NDArray distribution, Random random, float epsilon) { + if (random.nextFloat() < epsilon) { + return random.nextInt((int) distribution.size()); + } else { + return greedy(distribution); + } + } + + public static int greedy(NDArray distribution) { + return (int) distribution.argMax().getLong(); + } + + public static int sampleMultinomial(NDArray distribution, Random random) { + int value = 0; + long size = distribution.size(); + float rnd = random.nextFloat(); + for (int i = 0; i < size; i++) { + float cut = distribution.getFloat(value); + if (rnd > cut) { + value++; + } else { + return value; + } + rnd -= cut; + } + + throw new IllegalArgumentException("Invalid multinomial distribution"); + } +} diff --git a/src/uet/oop/bomberman/agent/rl/utils/ModelHelper.java b/src/uet/oop/bomberman/agent/rl/utils/ModelHelper.java new file mode 100644 index 0000000..a5f7d64 --- /dev/null +++ b/src/uet/oop/bomberman/agent/rl/utils/ModelHelper.java @@ -0,0 +1,27 @@ +package uet.oop.bomberman.agent.rl.utils; + +import ai.djl.ndarray.NDArray; +import ai.djl.ndarray.types.Shape; + +public final class ModelHelper { + public static NDArray gather(NDArray arr, int[] indexes) { + boolean[][] mask = new boolean[(int) arr.size(0)][(int) arr.size(1)]; + for (int i = 0; i < indexes.length; i++) { + mask[i][indexes[i]] = true; + } + NDArray boolean_mask = arr.getManager().create(mask); + for (int i = (int) boolean_mask.getShape().dimension(); i < arr.getShape().dimension(); i++) { + boolean_mask = boolean_mask.expandDims(i); + } + + return arr.get(tile(boolean_mask, arr.getShape())).reshape(Shape.update(arr.getShape(), 1, 1)).squeeze(); + } + + public static NDArray tile(NDArray arr, Shape shape) { + for (int i = (int) arr.getShape().dimension(); i < shape.dimension(); i++) { + arr = arr.expandDims(i); + } + return arr.broadcast(shape); + + } +} diff --git a/src/uet/oop/bomberman/agent/rulebased/RuleBasedBomberAgent.java b/src/uet/oop/bomberman/agent/rulebased/RuleBasedBomberAgent.java new file mode 100644 index 0000000..bc56e86 --- /dev/null +++ b/src/uet/oop/bomberman/agent/rulebased/RuleBasedBomberAgent.java @@ -0,0 +1,201 @@ +package uet.oop.bomberman.agent.rulebased; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import uet.oop.bomberman.Board; +import uet.oop.bomberman.agent.base.Agent; +import uet.oop.bomberman.entities.Entity; +import uet.oop.bomberman.entities.LayeredEntity; +import uet.oop.bomberman.entities.bomb.Bomb; +import uet.oop.bomberman.entities.bomb.Flame; +import uet.oop.bomberman.entities.bomb.FlameSegment; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.character.action.Action; +import uet.oop.bomberman.entities.character.action.ActionConstants; +import uet.oop.bomberman.entities.tile.Tile; + +public class RuleBasedBomberAgent extends Agent { + + private Board board; + + public RuleBasedBomberAgent(Character character, Board board) { + super(character); + this.board = board; + } + + @Override + public List getNextActions() { + int x = character.getXTile(); + int y = character.getYTile(); + boolean canPlaceBomb = character.canPerformAction(ActionConstants.PLACE_BOMB); + boolean canDestroy = getDestroyableNeighbourEntities(x, y) > 0; + if (canPlaceBomb && canDestroy) { + return Arrays.asList(ActionConstants.PLACE_BOMB); + } + if (!character.isMoving()) { + int width = board.getLevelManager().getBoardWidth(); + int height = board.getLevelManager().getBoardHeight(); + + // BFS to find shortest path + Queue queue = new LinkedList<>(); + queue.add(new Coordinate(x, y, 0)); + boolean[][] visited = new boolean[width][height]; + int[][] distance = new int[width][height]; + visited[x][y] = true; + int[][] parentX = new int[width][height]; + int[][] parentY = new int[width][height]; + while (!queue.isEmpty()) { + Coordinate current = queue.poll(); + int[] dx = {-1, 1, 0, 0}; + int[] dy = {0, 0, -1, 1}; + for (int i = 0; i < dx.length; i++) { + int newX = current.x + dx[i]; + int newY = current.y + dy[i]; + if (newX < 0 || newX >= width || newY < 0 || newY >= height) { + continue; + } + if (visited[newX][newY]) { + continue; + } + Tile tile = board.getEntityManager().getTileManager().getTileAt(newX, newY); + if (!tile.canBePassedThroughBy(character)) { + continue; + } + visited[newX][newY] = true; + parentX[newX][newY] = current.x; + parentY[newX][newY] = current.y; + distance[newX][newY] = current.distance + 1; + queue.add(new Coordinate(newX, newY, current.distance + 1)); + } + } + + int[][] tileScore = new int[width][height]; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + Entity entity = board.getEntityManager().getEntityAtExcluding(i, j, character); + if (entity == null) { + tileScore[i][j] = Integer.MIN_VALUE; + continue; + } + if (i == x && j == y) { + tileScore[i][j] = Integer.MIN_VALUE; + continue; + } + if (!entity.canBePassedThroughBy(character)) { + tileScore[i][j] = Integer.MIN_VALUE; + continue; + } + if (!visited[i][j]) { + tileScore[i][j] = Integer.MIN_VALUE; + continue; + } + if (getNeighbourBombs(i, j) > 0) { + tileScore[i][j] = Integer.MIN_VALUE; + continue; + } + if (entity instanceof Flame || entity instanceof FlameSegment) { + tileScore[i][j] = Integer.MIN_VALUE; + continue; + } + if (entity instanceof Character && !((Character) entity).isPlayer()) { + tileScore[i][j] = Integer.MIN_VALUE; + continue; + } + int destroyableEntities = getDestroyableNeighbourEntities(i, j); + tileScore[i][j] += destroyableEntities; + + if (entity instanceof LayeredEntity && ((LayeredEntity) entity).hasItem()) { + tileScore[i][j] += 5; + } + + if (distance[i][j] < 5) { + float bonus = tileScore[i][j] * (5 - distance[i][j]) / 5.0f; + tileScore[i][j] += bonus; + } + } + } + int bestScore = Integer.MIN_VALUE; + int bestX = x; + int bestY = y; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + if (tileScore[i][j] > bestScore) { + bestScore = tileScore[i][j]; + bestX = i; + bestY = j; + } + } + } + if (bestX == x && bestY == y) { + return Arrays.asList(ActionConstants.DO_NOTHING); + } + // trace back to current tile and output the immediate next action that player should perform + int currentX = bestX; + int currentY = bestY; + while (parentX[currentX][currentY] != x || parentY[currentX][currentY] != y) { + int nextX = parentX[currentX][currentY]; + int nextY = parentY[currentX][currentY]; + currentX = nextX; + currentY = nextY; + } + if (currentX == x - 1) { + return Arrays.asList(ActionConstants.MOVE_LEFT); + } + if (currentX == x + 1) { + return Arrays.asList(ActionConstants.MOVE_RIGHT); + } + if (currentY == y - 1) { + return Arrays.asList(ActionConstants.MOVE_UP); + } + if (currentY == y + 1) { + return Arrays.asList(ActionConstants.MOVE_DOWN); + } + } + + return Arrays.asList(ActionConstants.DO_NOTHING); + } + + private class Coordinate { + int x; + int y; + int distance; + Coordinate(int x, int y, int distance) { + this.x = x; + this.y = y; + this.distance = distance; + } + } + + private int getDestroyableNeighbourEntities(int x, int y) { + int destroyableEntities = 0; + int[] dx = {-1, 1, 0, 0}; + int[] dy = {0, 0, -1, 1}; + for (int i = 0; i < dx.length; i++) { + Entity entity = board.getEntityManager().getEntityAtExcluding(x + dx[i], y + dy[i], character); + if (entity == null) continue; + if (entity instanceof Tile && ((Tile) entity).isDestroyable()) { + destroyableEntities += 1; + } + if (entity instanceof Character && !((Character) entity).isPlayer()) { + destroyableEntities += 1; + } + } + return destroyableEntities; + } + + private int getNeighbourBombs(int x, int y) { + int bombs = 0; + int[] dx = {-1, 1, 0, 0}; + int[] dy = {0, 0, -1, 1}; + for (int i = 0; i < dx.length; i++) { + Entity entity = board.getEntityManager().getEntityAtExcluding(x + dx[i], y + dy[i], character); + if (entity == null) continue; + if (entity instanceof Bomb) bombs += 1; + } + return bombs; + } + +} diff --git a/src/uet/oop/bomberman/agent/sac/SacAgent.java b/src/uet/oop/bomberman/agent/sac/SacAgent.java new file mode 100644 index 0000000..b0614f9 --- /dev/null +++ b/src/uet/oop/bomberman/agent/sac/SacAgent.java @@ -0,0 +1,122 @@ +package uet.oop.bomberman.agent.sac; + +import java.util.ArrayList; +import java.util.List; + +import ai.djl.training.optimizer.Optimizer; +import ai.djl.training.tracker.Tracker; +import uet.oop.bomberman.Board; +import uet.oop.bomberman.agent.base.Agent; +import uet.oop.bomberman.agent.base.RewardBasedAgent; +import uet.oop.bomberman.agent.base.SerializableAgent; +import uet.oop.bomberman.agent.rl.ExpertGuidedAgent; +import uet.oop.bomberman.agent.rl.SAC; +import uet.oop.bomberman.agent.rl.base.BaseAgentImpl; +import uet.oop.bomberman.agent.rulebased.RuleBasedBomberAgent; +import uet.oop.bomberman.agent.state.base.IStateExtractor; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.character.action.Action; + +public class SacAgent extends Agent implements SerializableAgent, RewardBasedAgent { + + private IStateExtractor stateExtractor; + private Board board; + private BaseAgentImpl agent; + private List validActions; + + private float prevValue; + private float bonusReward = 0; + + private String dir = "models"; + + public SacAgent(Character character, Board board, IStateExtractor stateExtractor) { + super(character); + this.board = board; + this.prevValue = stateExtractor.getValue(board); + this.stateExtractor = stateExtractor; + this.validActions = character.getValidActions(); + final Optimizer optimizer = Optimizer.adam() + .optLearningRateTracker(Tracker.fixed(3e-4f)) + .build(); + this.agent = SAC.builder() + .setStateSize(stateExtractor.getDimension()) + .setActionSize(validActions.size()) + .setOptimizer(optimizer) + .optPolicyUpdateInterval(32) + .build(); + this.agent.setActionMaskGetter(() -> { + List performableActions = character.getPerformableActions(); + Boolean[] mask = new Boolean[validActions.size()]; + for (int i = 0; i < validActions.size(); i++) { + mask[i] = performableActions.contains(validActions.get(i)); + } + return mask; + }); + this.agent = ExpertGuidedAgent.builder() + .setOriginalAgent(this.agent) + .setExpertAgent(new RuleBasedBomberAgent(character, board)) + .setValidActions(validActions) + .build(); + } + + public String getModelPath() { + return dir; + } + + public void setModelPath(String dir) { + this.dir = dir; + } + + @Override + public List getNextActions() { + collectReward(); + Action action = getAction(); + List actions = new ArrayList<>(); + actions.add(action); + prevValue = stateExtractor.getValue(board); + return actions; + } + + private void collectReward() { + float currentValue = stateExtractor.getValue(board); + try { + agent.collect(currentValue - prevValue + bonusReward, false); + bonusReward = 0; + } catch (IllegalStateException ignored) {} + } + + private Action getAction() { + float[] state = stateExtractor.getEmbedding(board); + int actionIndex = agent.react(state); + Action action = validActions.get(actionIndex); + return action; + } + + @Override + public void load(String path) { + agent.load(path); + } + + @Override + public void save(String path) { + agent.save(path); + } + + @Override + public void handleWinLevel() { + agent.collect(100, true); + save(dir); + } + + @Override + public void handleLoseLevel() { + agent.collect(-100, true); + save(dir); + } + + @Override + public void addReward(float reward) { + bonusReward += reward; + } + +} diff --git a/src/uet/oop/bomberman/agent/state/NaivePlayerStateExtractor.java b/src/uet/oop/bomberman/agent/state/NaivePlayerStateExtractor.java new file mode 100644 index 0000000..d236421 --- /dev/null +++ b/src/uet/oop/bomberman/agent/state/NaivePlayerStateExtractor.java @@ -0,0 +1,202 @@ +package uet.oop.bomberman.agent.state; + +import java.util.concurrent.atomic.AtomicInteger; + +import uet.oop.bomberman.Board; +import uet.oop.bomberman.Game; +import uet.oop.bomberman.agent.state.base.PlayerStateExtractor; +import uet.oop.bomberman.entities.Entity; +import uet.oop.bomberman.entities.LayeredEntity; +import uet.oop.bomberman.entities.bomb.Bomb; +import uet.oop.bomberman.entities.bomb.Flame; +import uet.oop.bomberman.entities.bomb.FlameSegment; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.character.action.Action; +import uet.oop.bomberman.entities.tile.Portal; +import uet.oop.bomberman.entities.tile.Tile; +import uet.oop.bomberman.entities.tile.item.Item; + +public class NaivePlayerStateExtractor extends PlayerStateExtractor { + + private static final float EPSILON = 0.01f; + private static final int FIELD_OF_VISION = 15; + + public NaivePlayerStateExtractor(Character player) { + super(player); + } + + @Override + public int getDimension() { + return ( + FIELD_OF_VISION * FIELD_OF_VISION // isPassable + + FIELD_OF_VISION * FIELD_OF_VISION // isItem + + FIELD_OF_VISION * FIELD_OF_VISION // isEnemy + + FIELD_OF_VISION * FIELD_OF_VISION // isBomb + + FIELD_OF_VISION * FIELD_OF_VISION // isFlame + + FIELD_OF_VISION * FIELD_OF_VISION // isDestroyable + + FIELD_OF_VISION * FIELD_OF_VISION // isPortal + + 1 // time + + player.getValidActions().size() // action availability + ); + } + + private interface BoardTilePredicate { + float test(Board board, int x, int y); + } + + @Override + public float[] getEmbedding(Board board) { + float[] embedding = new float[getDimension()]; + + AtomicInteger currentIndex = new AtomicInteger(0); + + addSurroundingTileMask(board, embedding, currentIndex, this::isPassable); + addSurroundingTileMask(board, embedding, currentIndex, this::isItem); + addSurroundingTileMask(board, embedding, currentIndex, this::isEnemy); + addSurroundingTileMask(board, embedding, currentIndex, this::isBomb); + addSurroundingTileMask(board, embedding, currentIndex, this::isFlame); + addSurroundingTileMask(board, embedding, currentIndex, this::isDestroyable); + addSurroundingTileMask(board, embedding, currentIndex, this::isPortal); + + embedding[currentIndex.getAndIncrement()] = board.getGameInfoManager().getTime() * 1.0f / Game.TIME; + + for (Action action: player.getValidActions()) { + if (player.canPerformAction(action)) { + embedding[currentIndex.getAndIncrement()] = 1; + } else { + embedding[currentIndex.getAndIncrement()] = EPSILON; + } + } + + return embedding; + } + + private void addSurroundingTileMask(Board board, float[] embedding, AtomicInteger currentIndex, BoardTilePredicate predicate) { + for (int dy = -FIELD_OF_VISION / 2; dy <= FIELD_OF_VISION / 2; dy++) { + for (int dx = -FIELD_OF_VISION / 2; dx <= FIELD_OF_VISION / 2; dx++) { + int x = player.getXTile() + dx; + int y = player.getYTile() + dy; + float value = predicate.test(board, x, y); + if (value == 0) { + value += EPSILON; + } + embedding[currentIndex.getAndIncrement()] = value; + } + } + } + + private float isEnemy(Board board, int x, int y) { + Character character = board.getEntityManager().getCharacterManager().getCharacterAtExcluding(x, y, player); + if (character == null) return 0; + return character.isPlayer() ? 0 : 1; + } + + private float isBomb(Board board, int x, int y) { + Entity entity = board.getEntityManager().getEntityAt(x, y); + if (entity == null) return 0; + if (entity instanceof Bomb) { + return ((Bomb) entity)._timeAfter; + } + return 0; + } + + private float isFlame(Board board, int x, int y) { + Entity entity = board.getEntityManager().getEntityAt(x, y); + if (entity == null) return 0; + if (entity instanceof Flame || entity instanceof FlameSegment) { + return 1; + } + return 0; + } + + private float isItem(Board board, int x, int y) { + Entity entity = board.getEntityManager().getEntityAt(x, y); + if (entity == null) return 0; + if (entity instanceof LayeredEntity) { + LayeredEntity layeredEntity = (LayeredEntity) entity; + return layeredEntity.getTopEntity() instanceof Item ? 1 : 0; + } + return 0; + } + + private float isPassable(Board board, int x, int y) { + Entity entity = board.getEntityManager().getEntityAt(x, y); + if (entity == null) return 0; + return entity.canBePassedThroughBy(player) ? 1 : 0; + } + + private float isDestroyable(Board board, int x, int y) { + Tile tile = board.getEntityManager().getTileManager().getTileAt(x, y); + if (tile == null) return 0; + if (tile.isDestroyable()) return 1; + return 0; + } + + private float isPortal(Board board, int x, int y) { + Tile tile = board.getEntityManager().getTileManager().getTileAt(x, y); + if (tile == null) return 0; + if (tile instanceof LayeredEntity) { + LayeredEntity layeredEntity = (LayeredEntity) tile; + return layeredEntity.getTopEntity() instanceof Portal ? 1 : 0; + } + return 0; + } + + @Override + public float getValue(Board board) { + float value = 0; + + // Penalize based on number of enemies still alive + float enemyPoints = 0; + for (Character character: board.getEntityManager().getCharacterManager().getCharacters()) { + if (character == player) { + continue; + } + if (character.isPlayer()) { + continue; + } + enemyPoints += character.getPoints(); + } + value -= enemyPoints; + + // Reward based on survival time + value -= 5 * board.getGameInfoManager().getTime() / Game.TICKS_PER_SECOND; + + // Penalize based on number of destroyable tiles left + float destroyableTiles = 0; + for (int x = 0; x < board.getLevelManager().getBoardWidth(); x++) { + for (int y = 0; y < board.getLevelManager().getBoardHeight(); y++) { + Tile tile = board.getEntityManager().getTileManager().getTileAt(x, y); + if (tile == null) { + continue; + } + if (tile.isDestroyable()) { + destroyableTiles++; + } + } + } + value -= destroyableTiles * 10; + + // Penalize based on number of items left + float items = 0; + for (int x = 0; x < board.getLevelManager().getBoardWidth(); x++) { + for (int y = 0; y < board.getLevelManager().getBoardHeight(); y++) { + Tile tile = board.getEntityManager().getTileManager().getTileAt(x, y); + if (tile == null) { + continue; + } + if (!(tile instanceof LayeredEntity)) { + continue; + } + LayeredEntity layeredEntity = (LayeredEntity) tile; + if (layeredEntity.hasItem()) { + items++; + } + } + } + value -= items * 50; + + return value; + } + +} diff --git a/src/uet/oop/bomberman/agent/state/base/IStateExtractor.java b/src/uet/oop/bomberman/agent/state/base/IStateExtractor.java new file mode 100644 index 0000000..b248dd3 --- /dev/null +++ b/src/uet/oop/bomberman/agent/state/base/IStateExtractor.java @@ -0,0 +1,13 @@ +package uet.oop.bomberman.agent.state.base; + +import uet.oop.bomberman.Board; + +public interface IStateExtractor { + + public int getDimension(); + + public float[] getEmbedding(Board board); + + public float getValue(Board board); + +} diff --git a/src/uet/oop/bomberman/agent/state/base/PlayerStateExtractor.java b/src/uet/oop/bomberman/agent/state/base/PlayerStateExtractor.java new file mode 100644 index 0000000..3393514 --- /dev/null +++ b/src/uet/oop/bomberman/agent/state/base/PlayerStateExtractor.java @@ -0,0 +1,13 @@ +package uet.oop.bomberman.agent.state.base; + +import uet.oop.bomberman.entities.character.Character; + +public abstract class PlayerStateExtractor implements IStateExtractor { + + protected Character player; + + public PlayerStateExtractor(Character player) { + this.player = player; + } + +} diff --git a/src/uet/oop/bomberman/base/Copyable.java b/src/uet/oop/bomberman/base/Copyable.java new file mode 100644 index 0000000..bd9721e --- /dev/null +++ b/src/uet/oop/bomberman/base/Copyable.java @@ -0,0 +1,7 @@ +package uet.oop.bomberman.base; + +public interface Copyable { + + public Copyable copy(); + +} diff --git a/src/uet/oop/bomberman/base/IBombManager.java b/src/uet/oop/bomberman/base/IBombManager.java new file mode 100644 index 0000000..24e321d --- /dev/null +++ b/src/uet/oop/bomberman/base/IBombManager.java @@ -0,0 +1,16 @@ +package uet.oop.bomberman.base; + +import java.util.List; + +import uet.oop.bomberman.entities.bomb.Bomb; +import uet.oop.bomberman.entities.bomb.FlameSegment; +import uet.oop.bomberman.graphics.IRender; + +public interface IBombManager extends IRender { + + public List getBombs(); + public Bomb getBombAt(double x, double y); + public void addBomb(Bomb e); + public FlameSegment getFlameSegmentAt(int x, int y); + +} \ No newline at end of file diff --git a/src/uet/oop/bomberman/base/ICharacterManager.java b/src/uet/oop/bomberman/base/ICharacterManager.java new file mode 100644 index 0000000..be681b6 --- /dev/null +++ b/src/uet/oop/bomberman/base/ICharacterManager.java @@ -0,0 +1,30 @@ +package uet.oop.bomberman.base; + +import java.util.List; + +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.graphics.IRender; +import uet.oop.bomberman.manager.BombManager; + +public interface ICharacterManager extends IRender { + + public List getCharacters(); + + public Character getCharacterAtExcluding(int x, int y, Character a); + + public void addCharacter(Character e); + + public void setPlayer(Character character); + + public Character getPlayer(); + + public void handleOnDeath(Character character, Character killer); + + public void handleAfterDeath(Character character); + + public void addPlayer(Character e); + + public List getPlayers(); + + public IBombManager getBombManager(); +} \ No newline at end of file diff --git a/src/uet/oop/bomberman/base/IEntityManager.java b/src/uet/oop/bomberman/base/IEntityManager.java new file mode 100644 index 0000000..eaf805d --- /dev/null +++ b/src/uet/oop/bomberman/base/IEntityManager.java @@ -0,0 +1,29 @@ +package uet.oop.bomberman.base; + +import java.util.List; + +import uet.oop.bomberman.entities.Entity; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.graphics.IRender; + +public interface IEntityManager extends IRender { + + public Entity getEntityAtExcluding(double x, double y, Character m); + + public default Entity getEntityAt(double x, double y) { + return getEntityAtExcluding(x, y, null); + }; + + public boolean isEnemyCleared(); + + public Character getPlayer(); + + public List getPlayers(); + + public ITileManager getTileManager(); + + public ICharacterManager getCharacterManager(); + + public IBombManager getBombManager(); + +} \ No newline at end of file diff --git a/src/uet/oop/bomberman/base/IGameInfoManager.java b/src/uet/oop/bomberman/base/IGameInfoManager.java new file mode 100644 index 0000000..6018ea1 --- /dev/null +++ b/src/uet/oop/bomberman/base/IGameInfoManager.java @@ -0,0 +1,34 @@ +package uet.oop.bomberman.base; + +import java.awt.Graphics; +import java.util.List; + +import uet.oop.bomberman.entities.tile.item.Item; +import uet.oop.bomberman.graphics.IRender; +import uet.oop.bomberman.graphics.Screen; + +public interface IGameInfoManager extends IMessageManager, IRender { + + public int subtractTime(); + + public int getTime(); + + public int getPoints(); + + public void addPoints(int points); + + public boolean isPaused(); + + public void pause(); + + public void unpause(); + + public List getPlayerActiveItems(); + + public List getPlayer2ActiveItems(); + + public void setEntityManager(IEntityManager entityManager); + + public void render(Screen screen, Graphics g); + +} \ No newline at end of file diff --git a/src/uet/oop/bomberman/base/ILevelManager.java b/src/uet/oop/bomberman/base/ILevelManager.java new file mode 100644 index 0000000..98ce798 --- /dev/null +++ b/src/uet/oop/bomberman/base/ILevelManager.java @@ -0,0 +1,14 @@ +package uet.oop.bomberman.base; + +public interface ILevelManager { + + public void nextLevel(); + + public void loadGlobalLevel(); + + public void endGame(); + + public int getBoardWidth(); + public int getBoardHeight(); + +} diff --git a/src/uet/oop/bomberman/base/IMessageManager.java b/src/uet/oop/bomberman/base/IMessageManager.java new file mode 100644 index 0000000..ec01310 --- /dev/null +++ b/src/uet/oop/bomberman/base/IMessageManager.java @@ -0,0 +1,9 @@ +package uet.oop.bomberman.base; + +import uet.oop.bomberman.entities.Message; + +public interface IMessageManager { + + public void addMessage(Message e); + +} \ No newline at end of file diff --git a/src/uet/oop/bomberman/base/ITileManager.java b/src/uet/oop/bomberman/base/ITileManager.java new file mode 100644 index 0000000..5b74d39 --- /dev/null +++ b/src/uet/oop/bomberman/base/ITileManager.java @@ -0,0 +1,11 @@ +package uet.oop.bomberman.base; + +import uet.oop.bomberman.entities.tile.Tile; +import uet.oop.bomberman.graphics.IRender; + +public interface ITileManager extends IRender { + + public Tile getTileAt(double x, double y); + public void addTile(int pos, Tile e); + +} \ No newline at end of file diff --git a/src/uet/oop/bomberman/entities/Entity.java b/src/uet/oop/bomberman/entities/Entity.java index c1021cb..c7ad8c8 100644 --- a/src/uet/oop/bomberman/entities/Entity.java +++ b/src/uet/oop/bomberman/entities/Entity.java @@ -40,13 +40,27 @@ public Sprite getSprite() { return _sprite; } + public double getCenterX() { + return _x + _sprite.SIZE / 2; + } + + public double getCenterY() { + return _y - _sprite.SIZE / 2; + } + /** * Phương thức này được gọi để xử lý khi hai entity va chạm vào nhau * @param e * @return */ public abstract boolean collide(Entity e); - //xu li 2 entity va cham + + /** + * Check if other Entity can pass through this + * @param other + * @return + */ + public abstract boolean canBePassedThroughBy(Entity other); public double getX() { return _x; @@ -57,11 +71,11 @@ public double getY() { } public int getXTile() { - return Coordinates.pixelToTile(_x + _sprite.SIZE / 2); + return Coordinates.pixelToTile(getCenterX()); } public int getYTile() { - return Coordinates.pixelToTile(_y - _sprite.SIZE / 2); + return Coordinates.pixelToTile(getCenterY()); } diff --git a/src/uet/oop/bomberman/entities/LayeredEntity.java b/src/uet/oop/bomberman/entities/LayeredEntity.java index e183f24..0db55a9 100644 --- a/src/uet/oop/bomberman/entities/LayeredEntity.java +++ b/src/uet/oop/bomberman/entities/LayeredEntity.java @@ -1,6 +1,8 @@ package uet.oop.bomberman.entities; +import uet.oop.bomberman.entities.tile.Tile; import uet.oop.bomberman.entities.tile.destroyable.DestroyableTile; +import uet.oop.bomberman.entities.tile.item.Item; import uet.oop.bomberman.graphics.Screen; import java.util.LinkedList; @@ -9,11 +11,12 @@ * Chứa và quản lý nhiều Entity tại cùng một vị trí * Ví dụ: tại vị trí dấu Item, có 3 Entity [Grass, Item, Brick] */ -public class LayeredEntity extends Entity { +public class LayeredEntity extends Tile { - protected LinkedList _entities = new LinkedList<>(); + protected LinkedList _entities = new LinkedList<>(); - public LayeredEntity(int x, int y, Entity ... entities) { + public LayeredEntity(int x, int y, Tile ... entities) { + super(x, y, null); _x = x; _y = y; @@ -38,7 +41,7 @@ public void render(Screen screen) { getTopEntity().render(screen); } - public Entity getTopEntity() { + public Tile getTopEntity() { return _entities.getLast(); } @@ -51,15 +54,31 @@ private void clearRemoved() { } } - public void addBeforeTop(Entity e) { + public void addBeforeTop(Tile e) { _entities.add(_entities.size() - 1, e); } + + public boolean hasItem() { + for (Tile e : _entities) { + if(e instanceof Item) return true; + } + return false; + } @Override public boolean collide(Entity e) { // TODO: lấy entity trên cùng ra để xử lý va chạm - return getTopEntity().collide(e); } + @Override + public boolean canBePassedThroughBy(Entity other) { + return getTopEntity().canBePassedThroughBy(other); + } + + @Override + public boolean isDestroyable() { + return getTopEntity().isDestroyable(); + } + } diff --git a/src/uet/oop/bomberman/entities/Message.java b/src/uet/oop/bomberman/entities/Message.java index 1e743e7..74cc779 100644 --- a/src/uet/oop/bomberman/entities/Message.java +++ b/src/uet/oop/bomberman/entities/Message.java @@ -1,5 +1,6 @@ package uet.oop.bomberman.entities; +import uet.oop.bomberman.Game; import uet.oop.bomberman.graphics.Screen; import java.awt.*; @@ -27,7 +28,7 @@ public Message(String message, double x, double y, int duration, Color color, in _x =x; _y = y; _message = message; - _duration = duration * 60; //seconds + _duration = duration * Game.TICKS_PER_SECOND; //seconds _color = color; _size = size; } @@ -64,6 +65,11 @@ public void render(Screen screen) { public boolean collide(Entity e) { return true; } + + @Override + public boolean canBePassedThroughBy(Entity other) { + return true; + } } diff --git a/src/uet/oop/bomberman/entities/bomb/Bomb.java b/src/uet/oop/bomberman/entities/bomb/Bomb.java index 34e1a20..6ad7436 100644 --- a/src/uet/oop/bomberman/entities/bomb/Bomb.java +++ b/src/uet/oop/bomberman/entities/bomb/Bomb.java @@ -1,7 +1,7 @@ package uet.oop.bomberman.entities.bomb; -import uet.oop.bomberman.Board; import uet.oop.bomberman.Game; +import uet.oop.bomberman.base.IEntityManager; import uet.oop.bomberman.entities.AnimatedEntitiy; import uet.oop.bomberman.entities.Entity; import uet.oop.bomberman.entities.character.Bomber; @@ -10,120 +10,136 @@ import uet.oop.bomberman.entities.character.Character; import uet.oop.bomberman.level.Coordinates; import uet.oop.bomberman.sound.Sound; + public class Bomb extends AnimatedEntitiy { - protected double _timeToExplode = 120; //2 seconds - thoi gian phat no + protected double _timeToExplode = 120; // 2 seconds - thoi gian phat no public int _timeAfter = 20;// thoi gian de no - - protected Board _board; + + protected IEntityManager _board; protected Flame[] _flames; protected boolean _exploded = false; protected boolean _allowedToPassThru = true; - - public Bomb(int x, int y, Board board) { + + private final int bombRadius; + + public Bomb(int x, int y, int bombRadius, IEntityManager board) { _x = x; _y = y; _board = board; _sprite = Sprite.bomb; + this.bombRadius = bombRadius; } - + @Override public void update() { - if(_timeToExplode > 0) + if (_timeToExplode > 0) _timeToExplode--; else { - if(!_exploded) + if (!_exploded) explode(); else updateFlames(); - - if(_timeAfter > 0) + + if (_timeAfter > 0) _timeAfter--; else remove(); } - + animate(); } - + @Override public void render(Screen screen) { - if(_exploded) { - _sprite = Sprite.bomb_exploded2; + if (_exploded) { + _sprite = Sprite.bomb_exploded2; renderFlames(screen); } else - _sprite = Sprite.movingSprite(Sprite.bomb, Sprite.bomb_1, Sprite.bomb_2, _animate, 60); - - int xt = (int)_x << 4; - int yt = (int)_y << 4; - - screen.renderEntity(xt, yt , this); + _sprite = Sprite.movingSprite(Sprite.bomb, Sprite.bomb_1, Sprite.bomb_2, _animate, Game.TICKS_PER_SECOND); + + int xt = (int) _x << 4; + int yt = (int) _y << 4; + + screen.renderEntity(xt, yt, this); } - + public void renderFlames(Screen screen) { for (int i = 0; i < _flames.length; i++) { _flames[i].render(screen); } } - + public void updateFlames() { for (int i = 0; i < _flames.length; i++) { _flames[i].update(); } } - /** - * Xử lý Bomb nổ - */ - protected void explode() {//nổ + /** + * Xử lý Bomb nổ + */ + protected void explode() {// nổ _exploded = true; _allowedToPassThru = true; // TODO: xử lý khi Character đứng tại vị trí Bomb - Character x = _board.getCharacterAtExcluding((int)_x, (int)_y, null); - if(x != null){ - x.kill(); - } + Character x = _board.getCharacterManager().getCharacterAtExcluding((int) _x, (int) _y, null); + if (x != null) { + x.handleOnDeath(); + } // TODO: tạo các Flame - _flames = new Flame[4]; - for (int i = 0; i < _flames.length; i++) { - _flames[i] = new Flame((int) _x, (int) _y, i, Game.getBombRadius(), _board); - } - Sound.play("BOM_11_M"); + _flames = new Flame[4]; + for (int i = 0; i < _flames.length; i++) { + _flames[i] = new Flame((int) _x, (int) _y, i, bombRadius, _board); + } + Sound.play("BOM_11_M"); } - public void time_explode() { + + public void handleChainExplode() { _timeToExplode = 0; } + public FlameSegment flameAt(int x, int y) { - if(!_exploded) return null; - + if (!_exploded) + return null; + for (int i = 0; i < _flames.length; i++) { - if(_flames[i] == null) return null; + if (_flames[i] == null) + return null; FlameSegment e = _flames[i].flameSegmentAt(x, y); - if(e != null) return e; + if (e != null) + return e; } - + return null; } @Override public boolean collide(Entity e) { - // TODO: xử lý khi Bomber đi ra sau khi vừa đặt bom (_allowedToPassThru) - - if(e instanceof Bomber) { - double diffX = e.getX() - Coordinates.tileToPixel(getX()); - double diffY = e.getY() - Coordinates.tileToPixel(getY()); - - if(!(diffX >= -10 && diffX < 16 && diffY >= 1 && diffY <= 28)) { // differences to see if the player has moved out of the bomb, tested values + // Xử lý va chạm với Flame của Bomb khác: chain explosion + if (e instanceof Flame) { + handleChainExplode(); + return true; + } + return false; + } + + @Override + public boolean canBePassedThroughBy(Entity other) { + // Xử lý khi Bomber đi ra sau khi vừa đặt bom (_allowedToPassThru) + if (other instanceof Bomber) { + double diffX = other.getX() - Coordinates.tileToPixel(getX()); + double diffY = other.getY() - Coordinates.tileToPixel(getY()); + + if (!(diffX >= -10 && diffX < 16 && diffY >= 1 && diffY <= 28)) { // differences to see if the player has + // moved out of the bomb, tested values _allowedToPassThru = false; } - + return _allowedToPassThru; } - // TODO: xử lý va chạm với Flame của Bomb khác - if(e instanceof Flame ) { - time_explode(); - return true; - } + return false; } + } diff --git a/src/uet/oop/bomberman/entities/bomb/Flame.java b/src/uet/oop/bomberman/entities/bomb/Flame.java index 721bc25..bf25642 100644 --- a/src/uet/oop/bomberman/entities/bomb/Flame.java +++ b/src/uet/oop/bomberman/entities/bomb/Flame.java @@ -1,14 +1,14 @@ package uet.oop.bomberman.entities.bomb; -import uet.oop.bomberman.Board; +import uet.oop.bomberman.base.IEntityManager; import uet.oop.bomberman.entities.Entity; -import uet.oop.bomberman.entities.character.Bomber; -import uet.oop.bomberman.entities.character.enemy.Enemy; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.tile.Tile; import uet.oop.bomberman.graphics.Screen; public class Flame extends Entity { - protected Board _board; + protected IEntityManager entityManager; protected int _direction; private int _radius; protected int xOrigin, yOrigin; @@ -21,14 +21,14 @@ public class Flame extends Entity { * @param direction là hướng của Flame * @param radius độ dài cực đại của Flame */ - public Flame(int x, int y, int direction, int radius, Board board) { + public Flame(int x, int y, int direction, int radius, IEntityManager entityManager) { xOrigin = x; yOrigin = y; _x = x; _y = y; _direction = direction; _radius = radius; - _board = board; + this.entityManager = entityManager; createFlameSegments(); } @@ -60,6 +60,10 @@ private void createFlameSegments() { case 3: x--; break; } _flameSegments[i] = new FlameSegment(x, y, _direction, last); + Entity entity = entityManager.getEntityAt(x, y); + if (entity!=null) { + entity.collide(this); + } } } @@ -78,17 +82,32 @@ private int calculatePermitedDistance() { if(_direction == 2) y++; if(_direction == 3) x--; - Entity a = _board.getEntity(x, y, null); + Entity a = entityManager.getEntityAt(x, y); if(a instanceof Bomb) ++radius; //explosion has to be below the bom - if(a.collide(this) == false) //cannot pass thru + if(!canSpawnFlameOn(a)) { break; + } ++radius; + + // Stop if encounter brick + if (!a.canBePassedThroughBy(this)) { + break; + } } return radius; } + + private boolean canSpawnFlameOn(Entity entity) { + if (entity.canBePassedThroughBy(this)) return true; + if (entity instanceof Tile) { + Tile tile = (Tile) entity; + if (tile.isDestroyable()) return true; + } + return false; + } public FlameSegment flameSegmentAt(int x, int y) { for (int i = 0; i < _flameSegments.length; i++) { @@ -110,9 +129,15 @@ public void render(Screen screen) { @Override public boolean collide(Entity e) { - // TODO: xử lý va chạm với Bomber, Enemy. Chú ý đối tượng này có vị trí chính là vị trí của Bomb đã nổ - if(e instanceof Bomber) ((Bomber) e).kill(); - if(e instanceof Enemy) ((Enemy) e).kill(); - return true; + if (e instanceof Character) { + ((Character)e).handleOnDeath(); + } + return true; + } + + @Override + public boolean canBePassedThroughBy(Entity other) { + return true; } + } diff --git a/src/uet/oop/bomberman/entities/bomb/FlameSegment.java b/src/uet/oop/bomberman/entities/bomb/FlameSegment.java index 8a28539..cbbb2a5 100644 --- a/src/uet/oop/bomberman/entities/bomb/FlameSegment.java +++ b/src/uet/oop/bomberman/entities/bomb/FlameSegment.java @@ -1,8 +1,7 @@ package uet.oop.bomberman.entities.bomb; import uet.oop.bomberman.entities.Entity; -import uet.oop.bomberman.entities.character.Bomber; -import uet.oop.bomberman.entities.character.enemy.Enemy; +import uet.oop.bomberman.entities.character.Character; import uet.oop.bomberman.graphics.Screen; import uet.oop.bomberman.graphics.Sprite; @@ -69,11 +68,13 @@ public void update() {} @Override public boolean collide(Entity e) { - // TODO: xử lý khi FlameSegment va chạm với Character - if(e instanceof Bomber) ((Bomber) e).kill(); - if(e instanceof Enemy) ((Enemy) e).kill(); + if (e instanceof Character) ((Character)e).handleOnDeath(); + return true; + } + + @Override + public boolean canBePassedThroughBy(Entity other) { return true; } - } \ No newline at end of file diff --git a/src/uet/oop/bomberman/entities/character/Bomber.java b/src/uet/oop/bomberman/entities/character/Bomber.java index bcc797d..67e729e 100644 --- a/src/uet/oop/bomberman/entities/character/Bomber.java +++ b/src/uet/oop/bomberman/entities/character/Bomber.java @@ -1,64 +1,61 @@ package uet.oop.bomberman.entities.character; import java.util.ArrayList; -import uet.oop.bomberman.Board; -import uet.oop.bomberman.Game; +import uet.oop.bomberman.base.IEntityManager; import uet.oop.bomberman.entities.Entity; import uet.oop.bomberman.entities.bomb.Bomb; +import uet.oop.bomberman.entities.character.action.Action; +import uet.oop.bomberman.entities.character.action.ActionConstants; +import uet.oop.bomberman.entities.character.action.ActionPlaceBomb; +import uet.oop.bomberman.entities.character.exceptions.ActionOnCooldownException; +import uet.oop.bomberman.entities.character.exceptions.BombQuotaReachedException; +import uet.oop.bomberman.entities.character.exceptions.CannotPerformActionException; +import uet.oop.bomberman.entities.character.exceptions.InvalidActionException; import uet.oop.bomberman.graphics.Screen; import uet.oop.bomberman.graphics.Sprite; -import uet.oop.bomberman.input.Keyboard; -import java.util.Iterator; import java.util.List; -import uet.oop.bomberman.entities.LayeredEntity; -import uet.oop.bomberman.entities.bomb.Flame; -import uet.oop.bomberman.entities.character.enemy.Enemy; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import uet.oop.bomberman.entities.tile.item.BombItem; +import uet.oop.bomberman.entities.tile.item.FlameItem; import uet.oop.bomberman.entities.tile.item.Item; +import uet.oop.bomberman.entities.tile.item.SpeedItem; import uet.oop.bomberman.level.Coordinates; import uet.oop.bomberman.sound.Sound; -public class Bomber extends Character { - - private List _bombs; - protected Keyboard _input; - public static List _items = new ArrayList();//xu li Item - /** - * nếu giá trị này < 0 thì cho phép đặt đối tượng Bomb tiếp theo, - * cứ mỗi lần đặt 1 Bomb mới, giá trị này sẽ được reset về 0 và giảm dần trong mỗi lần update() - */ - protected int _timeBetweenPutBombs = 0; - - public Bomber(int x, int y, Board board) { - super(x, y, board); - _bombs = _board.getBombs(); - _input = _board.getInput(); +public class Bomber extends Character implements CanUseItem { + + private List _bombs = new ArrayList<>(); + private List activeItems = new ArrayList<>(); + + private final int baseBombLimit; + protected int bombCooldown = 0; + + private final int baseBombRadius; + + public Bomber(int x, int y, double baseSpeed, int baseBombLimit, int baseBombRadius, IEntityManager entityManager) { + super(x, y, baseSpeed, entityManager); + this.baseBombLimit = baseBombLimit; + this.baseBombRadius = baseBombRadius; _sprite = Sprite.player_right; } @Override - public void update() { - clearBombs(); - if (!_alive) { - afterKill(); - return; - } - - if (_timeBetweenPutBombs < -7500) _timeBetweenPutBombs = 0; - else _timeBetweenPutBombs--; - + public void handleUpdate() { + clearExpiredBombs(); + if (bombCooldown < -7500) + bombCooldown = 0; + else + bombCooldown--; animate(); - calculateMove(); - - detectPlaceBomb(); } @Override public void render(Screen screen) { - calculateXOffset(); - - if (_alive) + if (isAlive()) chooseSprite(); else _sprite = Sprite.player_dead1; @@ -66,170 +63,152 @@ public void render(Screen screen) { screen.renderEntity((int) _x, (int) _y - _sprite.SIZE, this); } - public void calculateXOffset() { - int xScroll = Screen.calculateXOffset(_board, this); - Screen.setOffset(xScroll, 0); - } - - /** - * Kiểm tra xem có đặt được bom hay không? nếu có thì đặt bom tại vị trí hiện tại của Bomber - */ - private void detectPlaceBomb() { - // TODO: kiểm tra xem phím điều khiển đặt bom có được gõ và giá trị _timeBetweenPutBombs, Game.getBombRate() có thỏa mãn hay không - // TODO: Game.getBombRate() sẽ trả về số lượng bom có thể đặt liên tiếp tại thời điểm hiện tại - // TODO: _timeBetweenPutBombs dùng để ngăn chặn Bomber đặt 2 Bomb cùng tại 1 vị trí trong 1 khoảng thời gian quá ngắn - // TODO: nếu 3 điều kiện trên thỏa mãn thì thực hiện đặt bom bằng placeBomb() - // TODO: sau khi đặt, nhớ giảm số lượng Bomb Rate và reset _timeBetweenPutBombs về 0 - if(_input.space && Game.getBombRate() > 0 && _timeBetweenPutBombs < 0) { - - int xt = Coordinates.pixelToTile(_x + _sprite.getSize() / 2); - int yt = Coordinates.pixelToTile( (_y + _sprite.getSize() / 2) - _sprite.getSize() ); //subtract half player height and minus 1 y position - - placeBomb(xt,yt); - Game.addBombRate(-1); - - _timeBetweenPutBombs = 30; - } - } - - protected void placeBomb(int x, int y) { - // TODO: thực hiện tạo đối tượng bom, đặt vào vị trí (x, y) - Bomb b = new Bomb(x, y, _board); - _board.addBomb(b); - Sound.play("BOM_SET"); + public int getBombLimit() { + int countActiveItem = (int) getActiveItems().filter(item -> item instanceof BombItem).count(); + int bombLimitBonus = countActiveItem * BombItem.BOMB_LIMIT_BONUS; + return this.baseBombLimit + bombLimitBonus; } - private void clearBombs() { - Iterator bs = _bombs.iterator(); - - Bomb b; - while (bs.hasNext()) { - b = bs.next(); - if (b.isRemoved()) { - bs.remove(); - Game.addBombRate(1); - } - } - + public int getBombRemainingQuota() { + return getBombLimit() - _bombs.size(); } - @Override - public void kill() { - if (!_alive) return; - _alive = false; - Sound.play("endgame3"); + public int getBombRadius() { + int countActiveItem = (int) getActiveItems().filter(item -> item instanceof FlameItem).count(); + int bombRadiusBonus = countActiveItem * FlameItem.BOMB_RADIUS_BONUS; + return this.baseBombRadius + bombRadiusBonus; } - @Override - protected void afterKill() { - if (_timeAfter > 0) --_timeAfter; - else { - _board.endGame(); - } + public int getBombCooldown() { + return bombCooldown; } - @Override - protected void calculateMove() { - // TODO: xử lý nhận tín hiệu điều khiển hướng đi từ _input và gọi move() để thực hiện di chuyển - // TODO: nhớ cập nhật lại giá trị cờ _moving khi thay đổi trạng thái di chuyển - int xa = 0, ya = 0; - if(_input.up) ya--; - if(_input.down) ya++; - if(_input.left) xa--; - if(_input.right) xa++; - - if(xa != 0 || ya != 0) { - move(xa * Game.getBomberSpeed(), ya * Game.getBomberSpeed()); - _moving = true; - } else { - _moving = false; - } + public boolean placeBomb() { + if (getBombRemainingQuota() > 0 && bombCooldown < 0) { + + int xt = Coordinates.pixelToTile(_x + _sprite.getSize() / 2); + int yt = Coordinates.pixelToTile((_y + _sprite.getSize() / 2) - _sprite.getSize()); // subtract half player + // height and minus 1 y + // position + + placeBomb(xt, yt); + + bombCooldown = 30; + return true; + } + return false; } - @Override - public boolean canMove(double x, double y) { - // TODO: kiểm tra có đối tượng tại vị trí chuẩn bị di chuyển đến và có thể di chuyển tới đó hay không - for (int c = 0; c < 4; c++) { //colision detection for each corner of the player - double xt = ((_x + x) + c % 2 * 9) / Game.TILES_SIZE; //divide with tiles size to pass to tile coordinate - double yt = ((_y + y) + c / 2 * 10 - 13) / Game.TILES_SIZE; //these values are the best from multiple tests - - Entity a = _board.getEntity(xt, yt, this); - - if(!a.collide(this)) - return false; - } - - return true; - //return false; + public void placeBomb(int x, int y) { + Bomb b = new Bomb(x, y, getBombRadius(), entityManager); + this._bombs.add(b); + entityManager.getBombManager().addBomb(b); + Sound.play("BOM_SET"); } - @Override - public void move(double xa, double ya) { - // TODO: sử dụng canMove() để kiểm tra xem có thể di chuyển tới điểm đã tính toán hay không và thực hiện thay đổi tọa độ _x, _y - // TODO: nhớ cập nhật giá trị _direction sau khi di chuyển - if(xa > 0) _direction = 1; - if(xa < 0) _direction = 3; - if(ya > 0) _direction = 2; - if(ya < 0) _direction = 0; - - if(canMove(0, ya)) { //separate the moves for the player can slide when is colliding - _y += ya; - } - - if(canMove(xa, 0)) { - _x += xa; - } + private void clearExpiredBombs() { + _bombs = _bombs.stream() + .filter(bomb -> !bomb.isRemoved()) + .collect(Collectors.toList()); } @Override public boolean collide(Entity e) { - // TODO: xử lý va chạm với Flame - // TODO: xử lý va chạm với Enemy - if(e instanceof Flame){ - this.kill(); + if (!super.collide(e)) return false; - } - if(e instanceof Enemy){ - this.kill(); - return true; - } - if( e instanceof LayeredEntity) return(e.collide(this)); return true; } - //sprite - private void chooseSprite() { - switch (_direction) { + // sprite + // protected + protected void chooseSprite() { + switch (getDirection()) { case 0: _sprite = Sprite.player_up; - if (_moving) { + if (isMoving()) { _sprite = Sprite.movingSprite(Sprite.player_up_1, Sprite.player_up_2, _animate, 20); } break; case 1: _sprite = Sprite.player_right; - if (_moving) { + if (isMoving()) { _sprite = Sprite.movingSprite(Sprite.player_right_1, Sprite.player_right_2, _animate, 20); } break; case 2: _sprite = Sprite.player_down; - if (_moving) { + if (isMoving()) { _sprite = Sprite.movingSprite(Sprite.player_down_1, Sprite.player_down_2, _animate, 20); } break; case 3: _sprite = Sprite.player_left; - if (_moving) { + if (isMoving()) { _sprite = Sprite.movingSprite(Sprite.player_left_1, Sprite.player_left_2, _animate, 20); } break; default: _sprite = Sprite.player_right; - if (_moving) { + if (isMoving()) { _sprite = Sprite.movingSprite(Sprite.player_right_1, Sprite.player_right_2, _animate, 20); } break; } } + + @Override + public int getPoints() { + return 0; + } + + private static final List VALID_ACTIONS = new ArrayList() { + { + addAll(ActionConstants.LIST_ACTION_MOVE); + add(ActionConstants.DO_NOTHING); + add(ActionConstants.PLACE_BOMB); + } + }; + + @Override + public List getValidActions() { + return VALID_ACTIONS; + } + + @Override + protected void performAction(Action action, boolean isDryRun) + throws InvalidActionException, CannotPerformActionException { + super.performAction(action, isDryRun); + if (action instanceof ActionPlaceBomb) { + if (getBombRemainingQuota() < 0) + throw new BombQuotaReachedException(); + if (bombCooldown > 0) + throw new ActionOnCooldownException(); + if (!isDryRun) + placeBomb(); + } + } + + @Override + public Stream getActiveItems() { + return activeItems.stream().filter(Item::isActive); + } + + @Override + public void addActiveItem(Item item) { + this.activeItems.add(item); + } + + @Override + protected double getSpeedMultiplier() { + double speedMultiplier = 1; + for (Item item : activeItems) { + if (!item.isActive()) + continue; + if (item instanceof SpeedItem) { + speedMultiplier += SpeedItem.SPEED_MULTIPLIER; + } + } + return speedMultiplier; + } + } diff --git a/src/uet/oop/bomberman/entities/character/Bomber2.java b/src/uet/oop/bomberman/entities/character/Bomber2.java new file mode 100644 index 0000000..d87768a --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/Bomber2.java @@ -0,0 +1,59 @@ +package uet.oop.bomberman.entities.character; + +import uet.oop.bomberman.base.IEntityManager; +import uet.oop.bomberman.graphics.Screen; +import uet.oop.bomberman.graphics.Sprite; + +public class Bomber2 extends Bomber { + + public Bomber2(int x, int y, double baseSpeed, int baseBombLimit, int baseBombRadius, + IEntityManager entityManager) { + super(x, y, baseSpeed, baseBombLimit, baseBombRadius, entityManager); + } + + @Override + public void render(Screen screen) { + if (isAlive()) + chooseSprite(); + else + _sprite = Sprite.player_dead1; + + screen.renderEntity((int) _x, (int) _y - _sprite.SIZE, this); + } + + @Override + protected void chooseSprite() { + switch (getDirection()) { + case 0: + _sprite = Sprite.player2_up; + if (isMoving()) { + _sprite = Sprite.movingSprite(Sprite.player2_up_1, Sprite.player2_up_2, _animate, 20); + } + break; + case 1: + _sprite = Sprite.player2_right; + if (isMoving()) { + _sprite = Sprite.movingSprite(Sprite.player2_right_1, Sprite.player2_right_2, _animate, 20); + } + break; + case 2: + _sprite = Sprite.player2_down; + if (isMoving()) { + _sprite = Sprite.movingSprite(Sprite.player2_down_1, Sprite.player2_down_2, _animate, 20); + } + break; + case 3: + _sprite = Sprite.player2_left; + if (isMoving()) { + _sprite = Sprite.movingSprite(Sprite.player2_left_1, Sprite.player2_left_2, _animate, 20); + } + break; + default: + _sprite = Sprite.player2_right; + if (isMoving()) { + _sprite = Sprite.movingSprite(Sprite.player2_right_1, Sprite.player2_right_2, _animate, 20); + } + break; + } + } +} diff --git a/src/uet/oop/bomberman/entities/character/CanUseItem.java b/src/uet/oop/bomberman/entities/character/CanUseItem.java new file mode 100644 index 0000000..0d1103a --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/CanUseItem.java @@ -0,0 +1,12 @@ +package uet.oop.bomberman.entities.character; + +import java.util.stream.Stream; + +import uet.oop.bomberman.entities.tile.item.Item; + +public interface CanUseItem { + + public Stream getActiveItems(); + public void addActiveItem(Item item); + +} \ No newline at end of file diff --git a/src/uet/oop/bomberman/entities/character/Character.java b/src/uet/oop/bomberman/entities/character/Character.java index 52842a9..a52cd55 100644 --- a/src/uet/oop/bomberman/entities/character/Character.java +++ b/src/uet/oop/bomberman/entities/character/Character.java @@ -1,64 +1,296 @@ package uet.oop.bomberman.entities.character; -import uet.oop.bomberman.Board; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.stream.Collectors; + import uet.oop.bomberman.Game; +import uet.oop.bomberman.base.IEntityManager; import uet.oop.bomberman.entities.AnimatedEntitiy; +import uet.oop.bomberman.entities.Entity; +import uet.oop.bomberman.entities.LayeredEntity; +import uet.oop.bomberman.entities.bomb.Flame; +import uet.oop.bomberman.entities.character.action.Action; +import uet.oop.bomberman.entities.character.action.ActionConstants; +import uet.oop.bomberman.entities.character.action.ActionMove; +import uet.oop.bomberman.entities.character.exceptions.ActionOnCooldownException; +import uet.oop.bomberman.entities.character.exceptions.CannotPerformActionException; +import uet.oop.bomberman.entities.character.exceptions.CharacterActionException; +import uet.oop.bomberman.entities.character.exceptions.InvalidActionException; import uet.oop.bomberman.graphics.Screen; +import uet.oop.bomberman.level.Coordinates; /** * Bao gồm Bomber và Enemy */ public abstract class Character extends AnimatedEntitiy { - - protected Board _board; - protected int _direction = -1; - protected boolean _alive = true; - protected boolean _moving = false; - public int _timeAfter = 40; - - public Character(int x, int y, Board board) { + + protected IEntityManager entityManager; + + private int direction = -1; + private boolean alive = true; + private int timerDeathAnimation = 40; + + private final double baseSpeed; + private Queue waypoints = new LinkedList<>(); + + public Character(int x, int y, double baseSpeed, IEntityManager entityManager) { _x = x; _y = y; - _board = board; + this.entityManager = entityManager; + this.baseSpeed = baseSpeed; } - + @Override - public abstract void update(); - + public final void update() { + if (!alive) { + if (timerDeathAnimation > 0) { + timerDeathAnimation -= 1; + } else { + handleAfterDeath(); + } + return; + } + updateMove(); + handleUpdate(); + }; + + protected abstract void handleUpdate(); + @Override public abstract void render(Screen screen); + private static final List VALID_ACTIONS = new ArrayList() { + { + addAll(ActionConstants.LIST_ACTION_MOVE); + add(ActionConstants.DO_NOTHING); + } + }; + + public List getValidActions() { + return VALID_ACTIONS; + } + + public boolean isValidAction(Action action) { + return getValidActions().contains(action); + } + + public final void performAction(Action action) throws InvalidActionException, CannotPerformActionException { + performAction(action, false); + } + + protected void performAction(Action action, boolean isDryRun) + throws InvalidActionException, CannotPerformActionException { + if (!isValidAction(action)) + throw new InvalidActionException(); + if (action instanceof ActionMove) { + ActionMove actionMove = (ActionMove) action; + double dx = actionMove.getDx() * Game.TILES_SIZE; + double dy = actionMove.getDy() * Game.TILES_SIZE; + if (isMoving()) + throw new ActionOnCooldownException(); + if (!isDryRun) + move(dx, dy); + } + }; + + public final boolean canPerformAction(Action action) { + try { + performAction(action, true); + return true; + } catch (CharacterActionException ex) { + return false; + } + }; + + public List getPerformableActions() { + return getValidActions().stream() + .filter(this::canPerformAction) + .collect(Collectors.toList()); + } + /** - * Tính toán hướng đi + * Check if can be moved with vector (xa, ya). + * + * @param xa + * @param ya */ - protected abstract void calculateMove(); - - protected abstract void move(double xa, double ya); + public void move(double xa, double ya) { + double moveDurationBase = Game.TICKS_PER_SECOND / getSpeed(); + Waypoint waypointX = new Waypoint( + xa, + 0, + moveDurationBase); + Waypoint waypointY = new Waypoint( + 0, + ya, + moveDurationBase); + if (xa != 0 && ya != 0 && canMove(xa, ya) && canMove(xa, 0)) { + waypoints.add(waypointX); + waypoints.add(waypointY); + } else if (xa != 0 && ya != 0 && canMove(xa, ya) && canMove(0, ya)) { + waypoints.add(waypointY); + waypoints.add(waypointX); + } else if (xa != 0 && canMove(xa, 0)) { + waypoints.add(waypointX); + } else if (ya != 0 && canMove(0, ya)) { + waypoints.add(waypointY); + } else { + System.out.println(String.format( + "Cannot move character %s to (%s, %s)", + getClass().getSimpleName(), xa, ya)); + } + } + + private void updateMove() { + if (waypoints.isEmpty()) + return; + Waypoint waypoint = waypoints.peek(); + if (!waypoint.started) { + waypoint.started = true; + waypoint.moveDestX = _x + waypoint.moveX; + waypoint.moveDestY = _y + waypoint.moveY; + } + + waypoint.moveDuration -= 1; + if (waypoint.moveDuration > 0) { + _x += waypoint.moveDx; + _y += waypoint.moveDy; + } else { + // Correct rounding errors by force teleporting to destination + _x = waypoint.moveDestX; + _y = waypoint.moveDestY; + // Remove waypoint + waypoints.poll(); + } + + // Adjust direction + if (waypoint.moveDx > 0) + direction = 1; + if (waypoint.moveDx < 0) + direction = 3; + if (waypoint.moveDy > 0) + direction = 2; + if (waypoint.moveDy < 0) + direction = 0; + + Entity collidingEntity = entityManager.getEntityAtExcluding( + Coordinates.pixelToTile(getCenterX()), + Coordinates.pixelToTile(getCenterY()), + this); + if (collidingEntity != null) { + this.collide(collidingEntity); + collidingEntity.collide(this); + } + } /** * Được gọi khi đối tượng bị tiêu diệt */ - public abstract void kill(); + public final void handleOnDeath() { + if (!alive) + return; + alive = false; + // TODO: determine killer + entityManager.getCharacterManager().handleOnDeath(this, null); + } /** * Xử lý hiệu ứng bị tiêu diệt */ - protected abstract void afterKill(); + private void handleAfterDeath() { + entityManager.getCharacterManager().handleAfterDeath(this); + remove(); + } /** * Kiểm tra xem đối tượng có di chuyển tới vị trí đã tính toán hay không + * * @param x * @param y * @return */ - protected abstract boolean canMove(double x, double y); + public boolean canMove(double dx, double dy) { + double x = getCenterX() + dx; + double y = getCenterY() + dy; + Entity a = entityManager.getEntityAtExcluding( + Coordinates.pixelToTile(x), + Coordinates.pixelToTile(y), + this); + if (a == null) + return true; + return a.canBePassedThroughBy(this); + } protected double getXMessage() { return (_x * Game.SCALE) + (_sprite.SIZE / 2 * Game.SCALE); } - + protected double getYMessage() { - return (_y* Game.SCALE) - (_sprite.SIZE / 2 * Game.SCALE); + return (_y * Game.SCALE) - (_sprite.SIZE / 2 * Game.SCALE); + } + + public boolean isPlayer() { + return entityManager.getPlayers().contains(this); + } + + public boolean isAlive() { + return alive; + } + + public boolean isMoving() { + return waypoints.size() > 0; + } + + public final double getSpeed() { + double speedMultiplier = getSpeedMultiplier(); + return speedMultiplier * this.baseSpeed; } - + + protected double getSpeedMultiplier() { + return 1; + } + + public abstract int getPoints(); + + public int getDirection() { + return direction; + } + + public int getTimerDeathAnimation() { + return timerDeathAnimation; + } + + public void setTimerDeathAnimation(int timerDeathAnimation) { + this.timerDeathAnimation = timerDeathAnimation; + } + + @Override + public boolean collide(Entity e) { + if (e instanceof Flame) { + this.handleOnDeath(); + return false; + } + if (e instanceof Character) { + Character other = (Character) e; + if (this.isPlayer() && !other.isPlayer()) { + this.handleOnDeath(); + return false; + } + if (other.isPlayer() && !this.isPlayer()) { + other.handleOnDeath(); + return false; + } + } + + return true; + } + + @Override + public boolean canBePassedThroughBy(Entity other) { + return true; + } + } diff --git a/src/uet/oop/bomberman/entities/character/Waypoint.java b/src/uet/oop/bomberman/entities/character/Waypoint.java new file mode 100644 index 0000000..417df96 --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/Waypoint.java @@ -0,0 +1,21 @@ +package uet.oop.bomberman.entities.character; + +class Waypoint { + double moveX; + double moveY; + double moveDuration; + double moveDx; + double moveDy; + + public boolean started = false; + public double moveDestX; + public double moveDestY; + + public Waypoint(double moveX, double moveY, double moveDuration) { + this.moveX = moveX; + this.moveY = moveY; + this.moveDuration = moveDuration; + this.moveDx = moveX / moveDuration; + this.moveDy = moveY / moveDuration; + } +} \ No newline at end of file diff --git a/src/uet/oop/bomberman/entities/character/action/Action.java b/src/uet/oop/bomberman/entities/character/action/Action.java new file mode 100644 index 0000000..13b7f5a --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/action/Action.java @@ -0,0 +1,4 @@ +package uet.oop.bomberman.entities.character.action; + +public abstract class Action { +} diff --git a/src/uet/oop/bomberman/entities/character/action/ActionConstants.java b/src/uet/oop/bomberman/entities/character/action/ActionConstants.java new file mode 100644 index 0000000..332f398 --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/action/ActionConstants.java @@ -0,0 +1,32 @@ +package uet.oop.bomberman.entities.character.action; + +import java.util.Arrays; +import java.util.List; + +public interface ActionConstants { + + public static final ActionMove MOVE_UP = new ActionMove(0, -1); + public static final ActionMove MOVE_DOWN = new ActionMove(0, +1); + public static final ActionMove MOVE_LEFT = new ActionMove(-1, 0); + public static final ActionMove MOVE_RIGHT = new ActionMove(+1, 0); + + public static final ActionMove MOVE_UP_LEFT = new ActionMove(-1, -1); + public static final ActionMove MOVE_UP_RIGHT = new ActionMove(+1, -1); + public static final ActionMove MOVE_DOWN_LEFT = new ActionMove(-1, +1); + public static final ActionMove MOVE_DOWN_RIGHT = new ActionMove(+1, +1); + + public static final List LIST_ACTION_MOVE = Arrays.asList(new ActionMove[] { + MOVE_UP, + MOVE_DOWN, + MOVE_LEFT, + MOVE_RIGHT, + MOVE_UP_LEFT, + MOVE_UP_RIGHT, + MOVE_DOWN_LEFT, + MOVE_DOWN_RIGHT, + }); + + public static final ActionPlaceBomb PLACE_BOMB = new ActionPlaceBomb(); + public static final Action DO_NOTHING = new ActionNoop(); + +} diff --git a/src/uet/oop/bomberman/entities/character/action/ActionMove.java b/src/uet/oop/bomberman/entities/character/action/ActionMove.java new file mode 100644 index 0000000..38ed878 --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/action/ActionMove.java @@ -0,0 +1,45 @@ +package uet.oop.bomberman.entities.character.action; + +import java.util.Objects; + +public class ActionMove extends Action { + + private final double dx; + private final double dy; + + public ActionMove(double dx, double dy) { + this.dx = dx; + this.dy = dy; + } + + public double getDx() { + return dx; + } + + public double getDy() { + return dy; + } + + @Override + public int hashCode() { + return Objects.hash(dx, dy); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ActionMove other = (ActionMove) obj; + if (Double.doubleToLongBits(dx) != Double.doubleToLongBits(other.dx)) + return false; + if (Double.doubleToLongBits(dy) != Double.doubleToLongBits(other.dy)) + return false; + return true; + } + + +} diff --git a/src/uet/oop/bomberman/entities/character/action/ActionNoop.java b/src/uet/oop/bomberman/entities/character/action/ActionNoop.java new file mode 100644 index 0000000..df0795b --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/action/ActionNoop.java @@ -0,0 +1,5 @@ +package uet.oop.bomberman.entities.character.action; + +public class ActionNoop extends Action { + +} diff --git a/src/uet/oop/bomberman/entities/character/action/ActionPlaceBomb.java b/src/uet/oop/bomberman/entities/character/action/ActionPlaceBomb.java new file mode 100644 index 0000000..93b6556 --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/action/ActionPlaceBomb.java @@ -0,0 +1,21 @@ +package uet.oop.bomberman.entities.character.action; + +public class ActionPlaceBomb extends Action { + + protected ActionPlaceBomb() {} + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + return true; + } + + @Override + public int hashCode() { + return 0; + } + +} diff --git a/src/uet/oop/bomberman/entities/character/enemy/Balloon.java b/src/uet/oop/bomberman/entities/character/enemy/Balloon.java index e0d43dc..2cee00e 100644 --- a/src/uet/oop/bomberman/entities/character/enemy/Balloon.java +++ b/src/uet/oop/bomberman/entities/character/enemy/Balloon.java @@ -1,35 +1,29 @@ package uet.oop.bomberman.entities.character.enemy; -import static java.lang.Math.random; -import java.util.Random; -import uet.oop.bomberman.Board; import uet.oop.bomberman.Game; -import uet.oop.bomberman.entities.character.enemy.ai.AILow; +import uet.oop.bomberman.base.IEntityManager; import uet.oop.bomberman.graphics.Sprite; public class Balloon extends Enemy { - public Balloon(int x, int y, Board board) { - super(x, y, board, Sprite.balloom_dead, 0.5, 100); + public Balloon(int x, int y, IEntityManager entityManager) { + super(x, y, entityManager, Sprite.balloom_dead, Game.BOMBERSPEED / 2, 100); _sprite = Sprite.balloom_left1; - _ai = new AILow(); - _direction = _ai.calculateDirection(); - } @Override protected void chooseSprite() { - switch(_direction) { + switch(getDirection()) { case 0: case 1: - _sprite = Sprite.movingSprite(Sprite.balloom_right1, Sprite.balloom_right2, Sprite.balloom_right3, _animate, 60); + _sprite = Sprite.movingSprite(Sprite.balloom_right1, Sprite.balloom_right2, Sprite.balloom_right3, _animate, Game.TICKS_PER_SECOND); break; case 2: case 3: - _sprite = Sprite.movingSprite(Sprite.balloom_left1, Sprite.balloom_left2, Sprite.balloom_left3, _animate, 60); + _sprite = Sprite.movingSprite(Sprite.balloom_left1, Sprite.balloom_left2, Sprite.balloom_left3, _animate, Game.TICKS_PER_SECOND); break; } } diff --git a/src/uet/oop/bomberman/entities/character/enemy/Doll.java b/src/uet/oop/bomberman/entities/character/enemy/Doll.java index dc85fce..7ad85b2 100644 --- a/src/uet/oop/bomberman/entities/character/enemy/Doll.java +++ b/src/uet/oop/bomberman/entities/character/enemy/Doll.java @@ -5,42 +5,36 @@ */ package uet.oop.bomberman.entities.character.enemy; -import uet.oop.bomberman.Board; -import uet.oop.bomberman.entities.character.enemy.ai.AILow; -import uet.oop.bomberman.entities.character.enemy.ai.AIMedium; +import uet.oop.bomberman.Game; +import uet.oop.bomberman.base.IEntityManager; import uet.oop.bomberman.graphics.Sprite; -/** - * - * @author TUNG318 - */ -public class Doll extends Enemy{ - - public Doll(int x, int y, Board board) { - super(x, y, board, Sprite.balloom_dead, 0.8, 100); +public class Doll extends Enemy { - _sprite = Sprite.balloom_left1; + public Doll(int x, int y, IEntityManager entityManager) { + super(x, y, entityManager, Sprite.balloom_dead, Game.BOMBERSPEED, 200); - _ai = new AIMedium(_board.getBomber(), this); - _direction = _ai.calculateDirection(); + _sprite = Sprite.doll_left1; } @Override protected void chooseSprite() { - switch (_direction) { + switch (getDirection()) { case 0: case 1: - if (_moving) { - _sprite = Sprite.movingSprite(Sprite.doll_right1, Sprite.doll_right2, Sprite.doll_right3, _animate, 60); + if (isMoving()) { + _sprite = Sprite.movingSprite(Sprite.doll_right1, Sprite.doll_right2, Sprite.doll_right3, _animate, + Game.TICKS_PER_SECOND); } else { _sprite = Sprite.doll_left1; } break; case 2: case 3: - if (_moving) { - _sprite = Sprite.movingSprite(Sprite.doll_left1, Sprite.doll_left2, Sprite.doll_left3, _animate, 60); + if (isMoving()) { + _sprite = Sprite.movingSprite(Sprite.doll_left1, Sprite.doll_left2, Sprite.doll_left3, _animate, + Game.TICKS_PER_SECOND); } else { _sprite = Sprite.doll_left1; } diff --git a/src/uet/oop/bomberman/entities/character/enemy/Enemy.java b/src/uet/oop/bomberman/entities/character/enemy/Enemy.java index 61a7b99..3839cb9 100644 --- a/src/uet/oop/bomberman/entities/character/enemy/Enemy.java +++ b/src/uet/oop/bomberman/entities/character/enemy/Enemy.java @@ -1,26 +1,17 @@ package uet.oop.bomberman.entities.character.enemy; -import uet.oop.bomberman.Board; import uet.oop.bomberman.Game; +import uet.oop.bomberman.base.IEntityManager; import uet.oop.bomberman.entities.Entity; -import uet.oop.bomberman.entities.Message; -import uet.oop.bomberman.entities.bomb.Flame; -import uet.oop.bomberman.entities.character.Bomber; import uet.oop.bomberman.entities.character.Character; -import uet.oop.bomberman.entities.character.enemy.ai.AI; import uet.oop.bomberman.graphics.Screen; import uet.oop.bomberman.graphics.Sprite; -import uet.oop.bomberman.level.Coordinates; - -import java.awt.*; -import uet.oop.bomberman.sound.Sound; public abstract class Enemy extends Character { protected int _points; protected double _speed; - protected AI _ai; protected final double MAX_STEPS; protected final double rest; @@ -28,10 +19,10 @@ public abstract class Enemy extends Character { protected int _finalAnimation = 30; protected Sprite _deadSprite; - - public Enemy(int x, int y, Board board, Sprite dead, double speed, int points) { - super(x, y, board); - + + public Enemy(int x, int y, IEntityManager entityManager, Sprite dead, double speed, int points) { + super(x, y, speed, entityManager); + _points = points; _speed = speed; @@ -39,34 +30,25 @@ public Enemy(int x, int y, Board board, Sprite dead, double speed, int points) { rest = (MAX_STEPS - (int) MAX_STEPS) / MAX_STEPS; _steps = MAX_STEPS; - _timeAfter = 20; + setTimerDeathAnimation(20); _deadSprite = dead; } @Override - public void update() { + public void handleUpdate() { animate(); - - if(!_alive) { - afterKill(); - return; - } - - if(_alive) - calculateMove(); } @Override public void render(Screen screen) { - - if(_alive) + if(isAlive()) chooseSprite(); else { - if(_timeAfter > 0) { + if(getTimerDeathAnimation() > 0) { _sprite = _deadSprite; _animate = 0; } else { - _sprite = Sprite.movingSprite(Sprite.mob_dead1, Sprite.mob_dead2, Sprite.mob_dead3, _animate, 60); + _sprite = Sprite.movingSprite(Sprite.mob_dead1, Sprite.mob_dead2, Sprite.mob_dead3, _animate, Game.TICKS_PER_SECOND); } } @@ -74,94 +56,16 @@ public void render(Screen screen) { screen.renderEntity((int)_x, (int)_y - _sprite.SIZE, this); } - @Override - public void calculateMove() { - // TODO: Tính toán hướng đi và di chuyển Enemy theo _ai và cập nhật giá trị cho _direction - // TODO: sử dụng canMove() để kiểm tra xem có thể di chuyển tới điểm đã tính toán hay không - // TODO: sử dụng move() để di chuyển - // TODO: nhớ cập nhật lại giá trị cờ _moving khi thay đổi trạng thái di chuyển - int xa = 0, ya = 0; - if(_steps <= 0){ - _direction = _ai.calculateDirection(); - _steps = MAX_STEPS; - } - - if(_direction == 0) ya--; - if(_direction == 2) ya++; - if(_direction == 3) xa--; - if(_direction == 1) xa++; - - if(canMove(xa, ya)) { - _steps -= 1 + rest; - move(xa * _speed, ya * _speed); - _moving = true; - } else { - _steps = 0; - _moving = false; - } - } - - @Override - public void move(double xa, double ya) { - if(!_alive) return; - _y += ya; - _x += xa; - } - - @Override - public boolean canMove(double x, double y) { - double xr = _x, yr = _y - 16; //subtract y to get more accurate results - - //the thing is, subract 15 to 16 (sprite size), so if we add 1 tile we get the next pixel tile with this - //we avoid the shaking inside tiles with the help of steps - if(_direction == 0) { yr += _sprite.getSize() -1 ; xr += _sprite.getSize()/2; } - if(_direction == 1) {yr += _sprite.getSize()/2; xr += 1;} - if(_direction == 2) { xr += _sprite.getSize()/2; yr += 1;} - if(_direction == 3) { xr += _sprite.getSize() -1; yr += _sprite.getSize()/2;} - - int xx = Coordinates.pixelToTile(xr) +(int)x; - int yy = Coordinates.pixelToTile(yr) +(int)y; - - Entity a = _board.getEntity(xx, yy, this); //entity of the position we want to go - - return a.collide(this); - } - @Override public boolean collide(Entity e) { - if(e instanceof Flame){ - this.kill(); - return false; - } - if(e instanceof Bomber){ - ((Bomber) e).kill(); - return false; - } + if (!super.collide(e)) return false; return true; } - @Override - public void kill() { - if(!_alive) return; - _alive = false; - - _board.addPoints(_points); + protected abstract void chooseSprite(); - Message msg = new Message("+" + _points, getXMessage(), getYMessage(), 2, Color.white, 14); - _board.addMessage(msg); - Sound.play("AA126_11"); - } - - @Override - protected void afterKill() { - if(_timeAfter > 0) --_timeAfter; - else { - if(_finalAnimation > 0) --_finalAnimation; - else - remove(); - } + public int getPoints() { + return _points; } - - protected abstract void chooseSprite(); } diff --git a/src/uet/oop/bomberman/entities/character/enemy/Kondoria.java b/src/uet/oop/bomberman/entities/character/enemy/Kondoria.java new file mode 100644 index 0000000..cabb83e --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/enemy/Kondoria.java @@ -0,0 +1,38 @@ +package uet.oop.bomberman.entities.character.enemy; + +import uet.oop.bomberman.Game; +import uet.oop.bomberman.base.IEntityManager; +import uet.oop.bomberman.graphics.Sprite; + +public class Kondoria extends Enemy { + protected int bombCooldown = 0; + + public Kondoria(int x, int y, IEntityManager entityManager) { + super(x, y, entityManager, Sprite.kondoria_dead, Game.BOMBERSPEED * 2, 300); + + _sprite = Sprite.kondoria_right1; + } + + @Override + protected void chooseSprite() { + switch (getDirection()) { + case 0: + case 1: + if (isMoving()) + _sprite = Sprite.movingSprite(Sprite.kondoria_right1, Sprite.kondoria_right2, + Sprite.kondoria_right3, _animate, Game.TICKS_PER_SECOND); + else + _sprite = Sprite.kondoria_left1; + break; + case 2: + case 3: + if (isMoving()) + _sprite = Sprite.movingSprite(Sprite.kondoria_left1, Sprite.kondoria_left2, Sprite.kondoria_left3, + _animate, Game.TICKS_PER_SECOND); + else + _sprite = Sprite.kondoria_left1; + break; + } + } + +} diff --git a/src/uet/oop/bomberman/entities/character/enemy/Minvo.java b/src/uet/oop/bomberman/entities/character/enemy/Minvo.java new file mode 100644 index 0000000..8bd28fb --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/enemy/Minvo.java @@ -0,0 +1,34 @@ +package uet.oop.bomberman.entities.character.enemy; + +import uet.oop.bomberman.Game; +import uet.oop.bomberman.base.IEntityManager; +import uet.oop.bomberman.graphics.Sprite; + +public class Minvo extends Enemy { + public Minvo(int x, int y, IEntityManager entityManager) { + super(x, y, entityManager, Sprite.minvo_dead, Game.BOMBERSPEED * 2, 500); + _sprite = Sprite.minvo_right1; + } + + @Override + protected void chooseSprite() { + switch (getDirection()) { + case 0: + case 1: + if (isMoving()) + _sprite = Sprite.movingSprite(Sprite.minvo_right1, Sprite.minvo_right2, Sprite.minvo_right3, + _animate, Game.TICKS_PER_SECOND); + else + _sprite = Sprite.minvo_left1; + break; + case 2: + case 3: + if (isMoving()) + _sprite = Sprite.movingSprite(Sprite.minvo_left1, Sprite.minvo_left2, Sprite.minvo_left3, _animate, + Game.TICKS_PER_SECOND); + else + _sprite = Sprite.minvo_left1; + break; + } + } +} diff --git a/src/uet/oop/bomberman/entities/character/enemy/Oneal.java b/src/uet/oop/bomberman/entities/character/enemy/Oneal.java index 3ad4b23..1e8b84c 100644 --- a/src/uet/oop/bomberman/entities/character/enemy/Oneal.java +++ b/src/uet/oop/bomberman/entities/character/enemy/Oneal.java @@ -1,40 +1,33 @@ package uet.oop.bomberman.entities.character.enemy; - -import java.util.Random; -import uet.oop.bomberman.Board; import uet.oop.bomberman.Game; -import uet.oop.bomberman.entities.character.enemy.ai.AILow; -import uet.oop.bomberman.entities.character.enemy.ai.AIMedium; +import uet.oop.bomberman.base.IEntityManager; import uet.oop.bomberman.graphics.Sprite; public class Oneal extends Enemy { - //private Random random = new Random(); - public Oneal(int x, int y, Board board) { - super(x, y, board, Sprite.balloom_dead, 0.8 , 100); - - _sprite = Sprite.balloom_left1; - - _ai = new AILow(); - _direction = _ai.calculateDirection(); - //this._speed += random.nextDouble()/2; - + // private Random random = new Random(); + public Oneal(int x, int y, IEntityManager entityManager) { + super(x, y, entityManager, Sprite.balloom_dead, Game.BOMBERSPEED * 1.5, 400); + + _sprite = Sprite.oneal_left1; } - + @Override protected void chooseSprite() { - switch(_direction) { + switch (getDirection()) { case 0: case 1: - if(_moving) - _sprite = Sprite.movingSprite(Sprite.oneal_right1, Sprite.oneal_right2, Sprite.oneal_right3, _animate, 60); + if (isMoving()) + _sprite = Sprite.movingSprite(Sprite.oneal_right1, Sprite.oneal_right2, Sprite.oneal_right3, + _animate, Game.TICKS_PER_SECOND); else _sprite = Sprite.oneal_left1; break; case 2: case 3: - if(_moving) - _sprite = Sprite.movingSprite(Sprite.oneal_left1, Sprite.oneal_left2, Sprite.oneal_left3, _animate, 60); + if (isMoving()) + _sprite = Sprite.movingSprite(Sprite.oneal_left1, Sprite.oneal_left2, Sprite.oneal_left3, _animate, + Game.TICKS_PER_SECOND); else _sprite = Sprite.oneal_left1; break; diff --git a/src/uet/oop/bomberman/entities/character/enemy/ai/AIHigh.java b/src/uet/oop/bomberman/entities/character/enemy/ai/AIHigh.java new file mode 100644 index 0000000..0a4bc71 --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/enemy/ai/AIHigh.java @@ -0,0 +1,91 @@ +package uet.oop.bomberman.entities.character.enemy.ai; + +import java.util.List; + +import uet.oop.bomberman.base.IBombManager; +import uet.oop.bomberman.base.ICharacterManager; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.bomb.Bomb; + +public class AIHigh extends AI { + private final Character character; + private final ICharacterManager characterManager; + private final IBombManager bombManager; + + public AIHigh(Character character, ICharacterManager entityManager, IBombManager bombManager) { + this.character = character; + this.characterManager = entityManager; + this.bombManager = bombManager; + } + + @Override + public int calculateDirection() { + int safeDirection = calculateSafeDirection(); + if (safeDirection != -1) { + return safeDirection; + } + + int vertical = random.nextInt(2); + + if (vertical == 1) { + int v = calculateRowDirection(); + if (v != -1) + return v; + else + return calculateColDirection(); + + } else { + int h = calculateColDirection(); + + if (h != -1) + return h; + else + return calculateRowDirection(); + } + } + + protected int calculateColDirection() { + Character player = characterManager.getPlayer(); + + if (player.getXTile() < character.getXTile()) + return 3; + else if (player.getXTile() > character.getXTile()) + return 1; + + return -1; + } + + protected int calculateRowDirection() { + Character player = characterManager.getPlayer(); + + if (player.getYTile() < character.getYTile()) + return 0; + else if (player.getYTile() > character.getYTile()) + return 2; + return -1; + } + + protected int calculateSafeDirection() { + List bombs = bombManager.getBombs(); + for (Bomb bomb : bombs) { + int bombX = bomb.getXTile(); + int bombY = bomb.getYTile(); + int characterX = character.getXTile(); + int characterY = character.getYTile(); + double distance = Math.sqrt(Math.pow(bombX - characterX, 2) + Math.pow(bombY - characterY, 2)); + + if (distance < 10) { + if (bombX < characterX) + return 1; // Move right + else if (bombX > characterX) + return 3; // Move left + else if (bombY < characterY) + return 2; // Move down + else if (bombY > characterY) + return 0; // Move up + } + } + return -1; + } + +} \ No newline at end of file diff --git a/src/uet/oop/bomberman/entities/character/enemy/ai/AILow.java b/src/uet/oop/bomberman/entities/character/enemy/ai/AILow.java index c0bcd79..19d9993 100644 --- a/src/uet/oop/bomberman/entities/character/enemy/ai/AILow.java +++ b/src/uet/oop/bomberman/entities/character/enemy/ai/AILow.java @@ -9,3 +9,4 @@ public int calculateDirection() { } } + \ No newline at end of file diff --git a/src/uet/oop/bomberman/entities/character/enemy/ai/AIMedium.java b/src/uet/oop/bomberman/entities/character/enemy/ai/AIMedium.java index 1fa7960..7d8a2d0 100644 --- a/src/uet/oop/bomberman/entities/character/enemy/ai/AIMedium.java +++ b/src/uet/oop/bomberman/entities/character/enemy/ai/AIMedium.java @@ -1,54 +1,57 @@ package uet.oop.bomberman.entities.character.enemy.ai; -import uet.oop.bomberman.entities.character.Bomber; -import uet.oop.bomberman.entities.character.enemy.Enemy; +import uet.oop.bomberman.base.ICharacterManager; +import uet.oop.bomberman.entities.character.Character; public class AIMedium extends AI { - Bomber _bomber; - Enemy _e; - - public AIMedium(Bomber bomber, Enemy e) { - _bomber = bomber; - _e = e; + + private final Character character; + private final ICharacterManager characterManager; + + public AIMedium(Character character, ICharacterManager entityManager) { + this.character = character; + this.characterManager = entityManager; } @Override public int calculateDirection() { - // TODO: cài đặt thuật toán tìm đường đi - if(_bomber == null) - return random.nextInt(4); - - int vertical = random.nextInt(2); - - if(vertical == 1) { + + int vertical = random.nextInt(4); + + if (vertical == 1) { int v = calculateRowDirection(); - if(v != -1) + if (v != -1) return v; else return calculateColDirection(); - + } else { int h = calculateColDirection(); - - if(h != -1) + + if (h != -1) return h; else return calculateRowDirection(); } } - protected int calculateColDirection() { - if(_bomber.getXTile() < _e.getXTile()) + + protected int calculateColDirection() { + Character player = characterManager.getPlayer(); + + if (player.getXTile() < character.getXTile()) return 3; - else if(_bomber.getXTile() > _e.getXTile()) + else if (player.getXTile() > character.getXTile()) return 1; - + return -1; } - + protected int calculateRowDirection() { - if(_bomber.getYTile() < _e.getYTile()) + Character player = characterManager.getPlayer(); + + if (player.getYTile() < character.getYTile()) return 0; - else if(_bomber.getYTile() > _e.getYTile()) + else if (player.getYTile() > character.getYTile()) return 2; return -1; } diff --git a/src/uet/oop/bomberman/entities/character/exceptions/ActionOnCooldownException.java b/src/uet/oop/bomberman/entities/character/exceptions/ActionOnCooldownException.java new file mode 100644 index 0000000..665dc0c --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/exceptions/ActionOnCooldownException.java @@ -0,0 +1,5 @@ +package uet.oop.bomberman.entities.character.exceptions; + +public class ActionOnCooldownException extends CannotPerformActionException { + +} diff --git a/src/uet/oop/bomberman/entities/character/exceptions/BombQuotaReachedException.java b/src/uet/oop/bomberman/entities/character/exceptions/BombQuotaReachedException.java new file mode 100644 index 0000000..6d263d8 --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/exceptions/BombQuotaReachedException.java @@ -0,0 +1,5 @@ +package uet.oop.bomberman.entities.character.exceptions; + +public class BombQuotaReachedException extends CannotPerformActionException { + +} diff --git a/src/uet/oop/bomberman/entities/character/exceptions/CannotPerformActionException.java b/src/uet/oop/bomberman/entities/character/exceptions/CannotPerformActionException.java new file mode 100644 index 0000000..eea7279 --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/exceptions/CannotPerformActionException.java @@ -0,0 +1,5 @@ +package uet.oop.bomberman.entities.character.exceptions; + +public class CannotPerformActionException extends CharacterActionException { + +} diff --git a/src/uet/oop/bomberman/entities/character/exceptions/CharacterActionException.java b/src/uet/oop/bomberman/entities/character/exceptions/CharacterActionException.java new file mode 100644 index 0000000..7ce9a17 --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/exceptions/CharacterActionException.java @@ -0,0 +1,5 @@ +package uet.oop.bomberman.entities.character.exceptions; + +public class CharacterActionException extends Exception { + +} diff --git a/src/uet/oop/bomberman/entities/character/exceptions/InvalidActionException.java b/src/uet/oop/bomberman/entities/character/exceptions/InvalidActionException.java new file mode 100644 index 0000000..43dc374 --- /dev/null +++ b/src/uet/oop/bomberman/entities/character/exceptions/InvalidActionException.java @@ -0,0 +1,5 @@ +package uet.oop.bomberman.entities.character.exceptions; + +public class InvalidActionException extends CharacterActionException { + +} diff --git a/src/uet/oop/bomberman/entities/tile/Grass.java b/src/uet/oop/bomberman/entities/tile/Grass.java index 6046143..acd57c0 100644 --- a/src/uet/oop/bomberman/entities/tile/Grass.java +++ b/src/uet/oop/bomberman/entities/tile/Grass.java @@ -4,7 +4,7 @@ import uet.oop.bomberman.entities.Entity; import uet.oop.bomberman.graphics.Sprite; -public class Grass extends Tile { +public class Grass extends NonDestroyableTile { public Grass(int x, int y, Sprite sprite) { super(x, y, sprite); @@ -19,4 +19,9 @@ public Grass(int x, int y, Sprite sprite) { public boolean collide(Entity e) { return true; } + + @Override + public boolean canBePassedThroughBy(Entity other) { + return true; + } } diff --git a/src/uet/oop/bomberman/entities/tile/NonDestroyableTile.java b/src/uet/oop/bomberman/entities/tile/NonDestroyableTile.java new file mode 100644 index 0000000..53b7101 --- /dev/null +++ b/src/uet/oop/bomberman/entities/tile/NonDestroyableTile.java @@ -0,0 +1,16 @@ +package uet.oop.bomberman.entities.tile; + +import uet.oop.bomberman.graphics.Sprite; + +public abstract class NonDestroyableTile extends Tile { + + public NonDestroyableTile(int x, int y, Sprite sprite) { + super(x, y, sprite); + } + + @Override + public boolean isDestroyable() { + return false; + } + +} diff --git a/src/uet/oop/bomberman/entities/tile/Portal.java b/src/uet/oop/bomberman/entities/tile/Portal.java index 9b077d7..0495f25 100644 --- a/src/uet/oop/bomberman/entities/tile/Portal.java +++ b/src/uet/oop/bomberman/entities/tile/Portal.java @@ -1,40 +1,48 @@ package uet.oop.bomberman.entities.tile; -import uet.oop.bomberman.Board; -import uet.oop.bomberman.Game; +import uet.oop.bomberman.base.IEntityManager; +import uet.oop.bomberman.base.ILevelManager; import uet.oop.bomberman.entities.Entity; -import uet.oop.bomberman.entities.character.Bomber; -import uet.oop.bomberman.entities.tile.item.Item; +import uet.oop.bomberman.entities.character.Character; import uet.oop.bomberman.graphics.Sprite; import uet.oop.bomberman.sound.Sound; -public class Portal extends Tile { - protected Board _board; - public Portal(int x, int y,Board board, Sprite sprite) { +public class Portal extends NonDestroyableTile { + protected ILevelManager levelManager; + protected IEntityManager entityManager; + + public Portal(int x, int y, ILevelManager levelManager, IEntityManager entityManager, Sprite sprite) { super(x, y, sprite); - _board = board; + this.levelManager = levelManager; + this.entityManager = entityManager; } - + @Override - public boolean collide(Entity e) {//xu li khi 2 entity va cham - //true cho di qua - //false khong cho di qua + public boolean collide(Entity e) {// xu li khi 2 entity va cham + // true cho di qua + // false khong cho di qua // TODO: xử lý khi Bomber đi vào - if(e instanceof Bomber ) { - - if(_board.detectNoEnemies() == false) - return false; - - if(e.getXTile() == getX() && e.getYTile() == getY()) { - if(_board.detectNoEnemies()){ - _board.nextLevel(); - Sound.play("CRYST_UP"); - } + if (e instanceof Character && ((Character) e).isPlayer()) { + + if (canBePassedThroughBy(e)) { + levelManager.nextLevel(); + Sound.play("CRYST_UP"); } - + + } + + return true; + } + + @Override + public boolean canBePassedThroughBy(Entity other) { + if (other instanceof Character && ((Character) other).isPlayer()) { + + if (!entityManager.isEnemyCleared()) + return false; + return true; } - return true; } diff --git a/src/uet/oop/bomberman/entities/tile/Tile.java b/src/uet/oop/bomberman/entities/tile/Tile.java index 33f9272..f2036c9 100644 --- a/src/uet/oop/bomberman/entities/tile/Tile.java +++ b/src/uet/oop/bomberman/entities/tile/Tile.java @@ -26,6 +26,12 @@ public boolean collide(Entity e) { return false;//khong cho di qua } + + @Override + public boolean canBePassedThroughBy(Entity other) { + return false; + } + @Override public void render(Screen screen) { screen.renderEntity( Coordinates.tileToPixel(_x), Coordinates.tileToPixel(_y), this); @@ -33,4 +39,6 @@ public void render(Screen screen) { @Override public void update() {} + + public abstract boolean isDestroyable(); } diff --git a/src/uet/oop/bomberman/entities/tile/Wall.java b/src/uet/oop/bomberman/entities/tile/Wall.java index 3af3dad..4404dc1 100644 --- a/src/uet/oop/bomberman/entities/tile/Wall.java +++ b/src/uet/oop/bomberman/entities/tile/Wall.java @@ -3,7 +3,7 @@ import uet.oop.bomberman.graphics.Sprite; -public class Wall extends Tile { +public class Wall extends NonDestroyableTile { public Wall(int x, int y, Sprite sprite) { super(x, y, sprite); diff --git a/src/uet/oop/bomberman/entities/tile/destroyable/DestroyableTile.java b/src/uet/oop/bomberman/entities/tile/destroyable/DestroyableTile.java index 5148855..6c2683f 100644 --- a/src/uet/oop/bomberman/entities/tile/destroyable/DestroyableTile.java +++ b/src/uet/oop/bomberman/entities/tile/destroyable/DestroyableTile.java @@ -42,6 +42,11 @@ public boolean collide(Entity e) { return false; } + @Override + public boolean canBePassedThroughBy(Entity other) { + return false; + } + public void addBelowSprite(Sprite sprite) { _belowSprite = sprite; } @@ -59,5 +64,10 @@ protected Sprite movingSprite(Sprite normal, Sprite x1, Sprite x2) { return x2; } + + @Override + public boolean isDestroyable() { + return true; + } } diff --git a/src/uet/oop/bomberman/entities/tile/item/BombItem.java b/src/uet/oop/bomberman/entities/tile/item/BombItem.java index cfdceba..8ae9c5e 100644 --- a/src/uet/oop/bomberman/entities/tile/item/BombItem.java +++ b/src/uet/oop/bomberman/entities/tile/item/BombItem.java @@ -1,27 +1,25 @@ package uet.oop.bomberman.entities.tile.item; -import uet.oop.bomberman.Game; -import uet.oop.bomberman.entities.Entity; -import uet.oop.bomberman.entities.character.Bomber; import uet.oop.bomberman.graphics.Sprite; -import uet.oop.bomberman.sound.Sound; public class BombItem extends Item { + public static final int BOMB_LIMIT_BONUS = 1; + public BombItem(int x, int y, Sprite sprite) { super(x, y, sprite); } @Override - public boolean collide(Entity e) { - // TODO: xử lý Bomber ăn Item - if (e instanceof Bomber) { - - Sound.play("Item"); - Game.addBombRate(1); - remove(); - } - return false; + protected void handleItemActive() { + } + + @Override + protected void handleItemInactive() { } + @Override + public String getDisplayActiveItem() { + return " 💣 "; + } } diff --git a/src/uet/oop/bomberman/entities/tile/item/FlameItem.java b/src/uet/oop/bomberman/entities/tile/item/FlameItem.java index e251754..863b776 100644 --- a/src/uet/oop/bomberman/entities/tile/item/FlameItem.java +++ b/src/uet/oop/bomberman/entities/tile/item/FlameItem.java @@ -1,26 +1,25 @@ package uet.oop.bomberman.entities.tile.item; -import uet.oop.bomberman.Game; -import uet.oop.bomberman.entities.Entity; -import uet.oop.bomberman.entities.character.Bomber; import uet.oop.bomberman.graphics.Sprite; -import uet.oop.bomberman.sound.Sound; public class FlameItem extends Item { + public static final int BOMB_RADIUS_BONUS = 1; + public FlameItem(int x, int y, Sprite sprite) { super(x, y, sprite); } @Override - public boolean collide(Entity e) { - // TODO: xử lý Bomber ăn - if (e instanceof Bomber) { - - Sound.play("Item"); - Game.addBombRadius(1); - remove(); - } - return false; + protected void handleItemActive() { + } + + @Override + protected void handleItemInactive() { + } + + @Override + public String getDisplayActiveItem() { + return " 🔥 "; } } diff --git a/src/uet/oop/bomberman/entities/tile/item/Item.java b/src/uet/oop/bomberman/entities/tile/item/Item.java index b3a1b8f..f48d13c 100644 --- a/src/uet/oop/bomberman/entities/tile/item/Item.java +++ b/src/uet/oop/bomberman/entities/tile/item/Item.java @@ -1,17 +1,66 @@ package uet.oop.bomberman.entities.tile.item; +import uet.oop.bomberman.Game; import uet.oop.bomberman.entities.Entity; -import uet.oop.bomberman.entities.character.Bomber; -import uet.oop.bomberman.entities.tile.Tile; +import uet.oop.bomberman.entities.character.CanUseItem; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.tile.NonDestroyableTile; import uet.oop.bomberman.graphics.Sprite; import uet.oop.bomberman.sound.Sound; -public abstract class Item extends Tile { - protected int _duration = -1; //thoi gian cua item ,-1 la vo han +public abstract class Item extends NonDestroyableTile { + protected int _duration = 30 * Game.TICKS_PER_SECOND; // 30s protected boolean _active = false; + protected int _level; + public Item(int x, int y, Sprite sprite) { super(x, y, sprite); } - + + protected abstract void handleItemActive(); + + protected abstract void handleItemInactive(); + + @Override + public boolean collide(Entity e) { + if (isRemoved()) + return false; + if (e instanceof CanUseItem) { + CanUseItem player = (CanUseItem) e; + Sound.play("Item"); + handleItemActive(); + _active = true; + player.addActiveItem(this); + remove(); + } + return false; + } + + @Override + public boolean canBePassedThroughBy(Entity e) { + return (e instanceof CanUseItem); + } + + @Override + public void update() { + if (!_active) + return; + if (_duration > 0) { + _duration--; + } else { + handleItemInactive(); + _active = false; + } + } + + public int getDuration() { + return _duration; + } + + public abstract String getDisplayActiveItem(); + + public boolean isActive() { + return _active; + } } diff --git a/src/uet/oop/bomberman/entities/tile/item/SpeedItem.java b/src/uet/oop/bomberman/entities/tile/item/SpeedItem.java index 7bc50b7..87a111a 100644 --- a/src/uet/oop/bomberman/entities/tile/item/SpeedItem.java +++ b/src/uet/oop/bomberman/entities/tile/item/SpeedItem.java @@ -1,26 +1,25 @@ package uet.oop.bomberman.entities.tile.item; -import uet.oop.bomberman.Game; -import uet.oop.bomberman.entities.Entity; -import uet.oop.bomberman.entities.character.Bomber; import uet.oop.bomberman.graphics.Sprite; -import uet.oop.bomberman.sound.Sound; public class SpeedItem extends Item { + public static final double SPEED_MULTIPLIER = 0.5; + public SpeedItem(int x, int y, Sprite sprite) { super(x, y, sprite); } @Override - public boolean collide(Entity e) { - // TODO: xử lý Bomber ăn Item - if (e instanceof Bomber) { - - Sound.play("Item"); - Game.addBomberSpeed(0.5); - remove(); - } - return false; + protected void handleItemActive() { + } + + @Override + protected void handleItemInactive() { + } + + @Override + public String getDisplayActiveItem() { + return " 👟 "; } } diff --git a/src/uet/oop/bomberman/graphics/Screen.java b/src/uet/oop/bomberman/graphics/Screen.java index 2494ea2..3b05f19 100644 --- a/src/uet/oop/bomberman/graphics/Screen.java +++ b/src/uet/oop/bomberman/graphics/Screen.java @@ -1,9 +1,8 @@ package uet.oop.bomberman.graphics; -import uet.oop.bomberman.Board; import uet.oop.bomberman.Game; import uet.oop.bomberman.entities.Entity; -import uet.oop.bomberman.entities.character.Bomber; +import uet.oop.bomberman.utils.Global; import java.awt.*; @@ -14,38 +13,37 @@ public class Screen { protected int _width, _height; public int[] _pixels; private int _transparentColor = 0xffff00ff; - + public static int xOffset = 0, yOffset = 0; - + public Screen(int width, int height) { _width = width; _height = height; - + _pixels = new int[width * height]; - - } - - public void clear() { - for (int i = 0; i < _pixels.length; i++) { - _pixels[i] = 0; - } + + Global.screenWidth = this.getRealWidth(); + Global.screenHeight = this.getRealHeight(); } - - public void renderEntity(int xp, int yp, Entity entity) { //save entity pixels + + public void renderEntity(int xp, int yp, Entity entity) { // save entity pixels xp -= xOffset; yp -= yOffset; for (int y = 0; y < entity.getSprite().getSize(); y++) { - int ya = y + yp; //add offset + int ya = y + yp; // add offset for (int x = 0; x < entity.getSprite().getSize(); x++) { - int xa = x + xp; //add offset - if(xa < -entity.getSprite().getSize() || xa >= _width || ya < 0 || ya >= _height) break; //fix black margins - if(xa < 0) xa = 0; //start at 0 from left + int xa = x + xp; // add offset + if (xa < -entity.getSprite().getSize() || xa >= _width || ya < 0 || ya >= _height) + break; // fix black margins + if (xa < 0) + xa = 0; // start at 0 from left int color = entity.getSprite().getPixel(x + y * entity.getSprite().getSize()); - if(color != _transparentColor) _pixels[xa + ya * _width] = color; + if (color != _transparentColor) + _pixels[xa + ya * _width] = color; } } } - + public void renderEntityWithBelowSprite(int xp, int yp, Entity entity, Sprite below) { xp -= xOffset; yp -= yOffset; @@ -53,91 +51,91 @@ public void renderEntityWithBelowSprite(int xp, int yp, Entity entity, Sprite be int ya = y + yp; for (int x = 0; x < entity.getSprite().getSize(); x++) { int xa = x + xp; - if(xa < -entity.getSprite().getSize() || xa >= _width || ya < 0 || ya >= _height) break; //fix black margins - if(xa < 0) xa = 0; + if (xa < -entity.getSprite().getSize() || xa >= _width || ya < 0 || ya >= _height) + break; // fix black margins + if (xa < 0) + xa = 0; int color = entity.getSprite().getPixel(x + y * entity.getSprite().getSize()); - if(color != _transparentColor) + if (color != _transparentColor) _pixels[xa + ya * _width] = color; else _pixels[xa + ya * _width] = below.getPixel(x + y * below.getSize()); } } } - + + public void clear() { + for (int i = 0; i < _pixels.length; i++) { + _pixels[i] = 0; + } + } + public static void setOffset(int xO, int yO) { xOffset = xO; yOffset = yO; } - - public static int calculateXOffset(Board board, Bomber bomber) { - if(bomber == null) return 0; - int temp = xOffset; - - double BomberX = bomber.getX() / 16; - double complement = 0.5; - int firstBreakpoint = board.getWidth() / 4; - int lastBreakpoint = board.getWidth() - firstBreakpoint; - - if( BomberX > firstBreakpoint + complement && BomberX < lastBreakpoint - complement) { - temp = (int)bomber.getX() - (Game.WIDTH / 2); - } - - return temp; - } - + public void drawEndGame(Graphics g, int points) { g.setColor(Color.black); g.fillRect(0, 0, getRealWidth(), getRealHeight()); - + Font font = new Font("Arial", Font.PLAIN, 20 * Game.SCALE); g.setFont(font); g.setColor(Color.white); - drawCenteredString("GAME OVER", getRealWidth(), getRealHeight(), g); - + drawCenteredString("GAME OVER", getRealWidth(), getRealHeight() / 4, g); + font = new Font("Arial", Font.PLAIN, 10 * Game.SCALE); g.setFont(font); g.setColor(Color.yellow); - drawCenteredString("POINTS: " + points, getRealWidth(), getRealHeight() + (Game.TILES_SIZE * 2) * Game.SCALE, g); + drawCenteredString("POINTS: " + points, getRealWidth(), + getRealHeight() / 3 + (Game.TILES_SIZE * 2) * Game.SCALE, g); + font = new Font("Arial", Font.PLAIN, 8 * Game.SCALE); + g.setFont(font); + g.setColor(Color.white); + drawCenteredString("Press R to retry", getRealWidth(), getRealHeight() / 2 + (Game.TILES_SIZE * 4) * Game.SCALE, + g); + drawCenteredString("Press B to Back Home", getRealWidth(), + getRealHeight() / 2 + (Game.TILES_SIZE * 5) * Game.SCALE, g); } public void drawChangeLevel(Graphics g, int level) { g.setColor(Color.black); g.fillRect(0, 0, getRealWidth(), getRealHeight()); - + Font font = new Font("Arial", Font.PLAIN, 20 * Game.SCALE); g.setFont(font); g.setColor(Color.white); drawCenteredString("LEVEL " + level, getRealWidth(), getRealHeight(), g); - + } - + public void drawPaused(Graphics g) { Font font = new Font("Arial", Font.PLAIN, 20 * Game.SCALE); g.setFont(font); g.setColor(Color.white); drawCenteredString("PAUSED", getRealWidth(), getRealHeight(), g); - + } public void drawCenteredString(String s, int w, int h, Graphics g) { - FontMetrics fm = g.getFontMetrics(); - int x = (w - fm.stringWidth(s)) / 2; - int y = (fm.getAscent() + (h - (fm.getAscent() + fm.getDescent())) / 2); - g.drawString(s, x, y); - } - + FontMetrics fm = g.getFontMetrics(); + int x = (w - fm.stringWidth(s)) / 2; + int y = (fm.getAscent() + (h - (fm.getAscent() + fm.getDescent())) / 2); + g.drawString(s, x, y); + } + public int getWidth() { return _width; } - + public int getHeight() { return _height; } - + public int getRealWidth() { return _width * Game.SCALE; } - + public int getRealHeight() { return _height * Game.SCALE; } diff --git a/src/uet/oop/bomberman/graphics/Sprite.java b/src/uet/oop/bomberman/graphics/Sprite.java index fb1d38c..8c4ec63 100644 --- a/src/uet/oop/bomberman/graphics/Sprite.java +++ b/src/uet/oop/bomberman/graphics/Sprite.java @@ -4,170 +4,193 @@ * Lưu trữ thông tin các pixel của 1 sprite (hình ảnh game) */ public class Sprite { - + public final int SIZE; private int _x, _y; public int[] _pixels; protected int _realWidth; protected int _realHeight; private SpriteSheet _sheet; - private SpriteSheet1 _sheet1; + private SpriteSheet1 _sheet1; + public static final int SCALED_SIZE = 16; /* - |-------------------------------------------------------------------------- - | Board sprites - |-------------------------------------------------------------------------- + * |-------------------------------------------------------------------------- + * | Board sprites + * |-------------------------------------------------------------------------- */ public static Sprite grass = new Sprite(16, 8, 4, SpriteSheet1.tiles, 16, 16); public static Sprite brick = new Sprite(16, 7, 0, SpriteSheet.tiles, 16, 16); public static Sprite wall = new Sprite(16, 3, 13, SpriteSheet1.tiles, 16, 16); public static Sprite portal = new Sprite(16, 8, 7, SpriteSheet1.tiles, 14, 14); - + /* - |-------------------------------------------------------------------------- - | Bomber Sprites - |-------------------------------------------------------------------------- + * |-------------------------------------------------------------------------- + * | Bomber Sprites + * |-------------------------------------------------------------------------- */ public static Sprite player_up = new Sprite(16, 0, 0, SpriteSheet.tiles, 15, 16); public static Sprite player_down = new Sprite(16, 2, 0, SpriteSheet.tiles, 12, 15); public static Sprite player_left = new Sprite(16, 3, 0, SpriteSheet.tiles, 10, 15); public static Sprite player_right = new Sprite(16, 1, 0, SpriteSheet.tiles, 10, 16); - + public static Sprite player_up_1 = new Sprite(16, 0, 1, SpriteSheet.tiles, 12, 16); public static Sprite player_up_2 = new Sprite(16, 0, 2, SpriteSheet.tiles, 12, 15); - + public static Sprite player_down_1 = new Sprite(16, 2, 1, SpriteSheet.tiles, 12, 15); public static Sprite player_down_2 = new Sprite(16, 2, 2, SpriteSheet.tiles, 12, 16); - + public static Sprite player_left_1 = new Sprite(16, 3, 1, SpriteSheet.tiles, 11, 16); - public static Sprite player_left_2 = new Sprite(16, 3, 2, SpriteSheet.tiles, 12 ,16); - + public static Sprite player_left_2 = new Sprite(16, 3, 2, SpriteSheet.tiles, 12, 16); + public static Sprite player_right_1 = new Sprite(16, 1, 1, SpriteSheet.tiles, 11, 16); public static Sprite player_right_2 = new Sprite(16, 1, 2, SpriteSheet.tiles, 12, 16); - + public static Sprite player_dead1 = new Sprite(16, 4, 2, SpriteSheet.tiles, 14, 16); public static Sprite player_dead2 = new Sprite(16, 5, 2, SpriteSheet.tiles, 13, 15); public static Sprite player_dead3 = new Sprite(16, 6, 2, SpriteSheet.tiles, 16, 16); - + + // Render player 2 + public static Sprite player2_up = new Sprite(16, 8, 9, SpriteSheet.tiles, 15, 16); + public static Sprite player2_down = new Sprite(16, 10, 9, SpriteSheet.tiles, 12, 15); + public static Sprite player2_left = new Sprite(16, 11, 9, SpriteSheet.tiles, 10, 15); + public static Sprite player2_right = new Sprite(16, 9, 9, SpriteSheet.tiles, 10, 16); + + public static Sprite player2_up_1 = new Sprite(16, 8, 10, SpriteSheet.tiles, 12, 16); + public static Sprite player2_up_2 = new Sprite(16, 8, 11, SpriteSheet.tiles, 12, 15); + + public static Sprite player2_down_1 = new Sprite(16, 10, 10, SpriteSheet.tiles, 12, 15); + public static Sprite player2_down_2 = new Sprite(16, 10, 11, SpriteSheet.tiles, 12, 16); + + public static Sprite player2_left_1 = new Sprite(16, 11, 10, SpriteSheet.tiles, 11, 16); + public static Sprite player2_left_2 = new Sprite(16, 11, 11, SpriteSheet.tiles, 12, 16); + + public static Sprite player2_right_1 = new Sprite(16, 9, 10, SpriteSheet.tiles, 11, 16); + public static Sprite player2_right_2 = new Sprite(16, 9, 11, SpriteSheet.tiles, 12, 16); + + public static Sprite player2_dead1 = new Sprite(16, 12, 11, SpriteSheet.tiles, 14, 16); + public static Sprite player2_dead2 = new Sprite(16, 13, 11, SpriteSheet.tiles, 13, 15); + public static Sprite player2_dead3 = new Sprite(16, 14, 11, SpriteSheet.tiles, 16, 16); + /* - |-------------------------------------------------------------------------- - | Character - |-------------------------------------------------------------------------- + * |-------------------------------------------------------------------------- + * | Character + * |-------------------------------------------------------------------------- */ - //BALLOM + // BALLOM public static Sprite balloom_left1 = new Sprite(16, 9, 0, SpriteSheet.tiles, 16, 16); public static Sprite balloom_left2 = new Sprite(16, 9, 1, SpriteSheet.tiles, 16, 16); public static Sprite balloom_left3 = new Sprite(16, 9, 2, SpriteSheet.tiles, 16, 16); - + public static Sprite balloom_right1 = new Sprite(16, 10, 0, SpriteSheet.tiles, 16, 16); public static Sprite balloom_right2 = new Sprite(16, 10, 1, SpriteSheet.tiles, 16, 16); public static Sprite balloom_right3 = new Sprite(16, 10, 2, SpriteSheet.tiles, 16, 16); - + public static Sprite balloom_dead = new Sprite(16, 9, 3, SpriteSheet.tiles, 16, 16); - - //ONEAL + + // ONEAL public static Sprite oneal_left1 = new Sprite(16, 11, 0, SpriteSheet.tiles, 16, 16); public static Sprite oneal_left2 = new Sprite(16, 11, 1, SpriteSheet.tiles, 16, 16); public static Sprite oneal_left3 = new Sprite(16, 11, 2, SpriteSheet.tiles, 16, 16); - + public static Sprite oneal_right1 = new Sprite(16, 12, 0, SpriteSheet.tiles, 16, 16); public static Sprite oneal_right2 = new Sprite(16, 12, 1, SpriteSheet.tiles, 16, 16); public static Sprite oneal_right3 = new Sprite(16, 12, 2, SpriteSheet.tiles, 16, 16); - + public static Sprite oneal_dead = new Sprite(16, 11, 3, SpriteSheet.tiles, 16, 16); - - //Doll + + // Doll public static Sprite doll_left1 = new Sprite(16, 13, 0, SpriteSheet.tiles, 16, 16); public static Sprite doll_left2 = new Sprite(16, 13, 1, SpriteSheet.tiles, 16, 16); public static Sprite doll_left3 = new Sprite(16, 13, 2, SpriteSheet.tiles, 16, 16); - + public static Sprite doll_right1 = new Sprite(16, 14, 0, SpriteSheet.tiles, 16, 16); public static Sprite doll_right2 = new Sprite(16, 14, 1, SpriteSheet.tiles, 16, 16); public static Sprite doll_right3 = new Sprite(16, 14, 2, SpriteSheet.tiles, 16, 16); - + public static Sprite doll_dead = new Sprite(16, 13, 3, SpriteSheet.tiles, 16, 16); - - //Minvo + + // Minvo public static Sprite minvo_left1 = new Sprite(16, 8, 5, SpriteSheet.tiles, 16, 16); public static Sprite minvo_left2 = new Sprite(16, 8, 6, SpriteSheet.tiles, 16, 16); public static Sprite minvo_left3 = new Sprite(16, 8, 7, SpriteSheet.tiles, 16, 16); - + public static Sprite minvo_right1 = new Sprite(16, 9, 5, SpriteSheet.tiles, 16, 16); public static Sprite minvo_right2 = new Sprite(16, 9, 6, SpriteSheet.tiles, 16, 16); public static Sprite minvo_right3 = new Sprite(16, 9, 7, SpriteSheet.tiles, 16, 16); - + public static Sprite minvo_dead = new Sprite(16, 8, 8, SpriteSheet.tiles, 16, 16); - - //Kondoria + + // Kondoria public static Sprite kondoria_left1 = new Sprite(16, 10, 5, SpriteSheet.tiles, 16, 16); public static Sprite kondoria_left2 = new Sprite(16, 10, 6, SpriteSheet.tiles, 16, 16); public static Sprite kondoria_left3 = new Sprite(16, 10, 7, SpriteSheet.tiles, 16, 16); - + public static Sprite kondoria_right1 = new Sprite(16, 11, 5, SpriteSheet.tiles, 16, 16); public static Sprite kondoria_right2 = new Sprite(16, 11, 6, SpriteSheet.tiles, 16, 16); public static Sprite kondoria_right3 = new Sprite(16, 11, 7, SpriteSheet.tiles, 16, 16); - + public static Sprite kondoria_dead = new Sprite(16, 10, 8, SpriteSheet.tiles, 16, 16); - - //ALL + + // ALL public static Sprite mob_dead1 = new Sprite(16, 15, 0, SpriteSheet.tiles, 16, 16); public static Sprite mob_dead2 = new Sprite(16, 15, 1, SpriteSheet.tiles, 16, 16); public static Sprite mob_dead3 = new Sprite(16, 15, 2, SpriteSheet.tiles, 16, 16); - + /* - |-------------------------------------------------------------------------- - | Bomb Sprites - |-------------------------------------------------------------------------- + * |-------------------------------------------------------------------------- + * | Bomb Sprites + * |-------------------------------------------------------------------------- */ public static Sprite bomb = new Sprite(16, 0, 3, SpriteSheet.tiles, 15, 15); public static Sprite bomb_1 = new Sprite(16, 1, 3, SpriteSheet.tiles, 13, 15); public static Sprite bomb_2 = new Sprite(16, 2, 3, SpriteSheet.tiles, 12, 14); - + /* - |-------------------------------------------------------------------------- - | FlameSegment Sprites - |-------------------------------------------------------------------------- + * |-------------------------------------------------------------------------- + * | FlameSegment Sprites + * |-------------------------------------------------------------------------- */ public static Sprite bomb_exploded = new Sprite(16, 0, 4, SpriteSheet.tiles, 16, 16); public static Sprite bomb_exploded1 = new Sprite(16, 0, 5, SpriteSheet.tiles, 16, 16); public static Sprite bomb_exploded2 = new Sprite(16, 0, 6, SpriteSheet.tiles, 16, 16); - + public static Sprite explosion_vertical = new Sprite(16, 1, 5, SpriteSheet.tiles, 16, 16); public static Sprite explosion_vertical1 = new Sprite(16, 2, 5, SpriteSheet.tiles, 16, 16); public static Sprite explosion_vertical2 = new Sprite(16, 3, 5, SpriteSheet.tiles, 16, 16); - + public static Sprite explosion_horizontal = new Sprite(16, 1, 7, SpriteSheet.tiles, 16, 16); public static Sprite explosion_horizontal1 = new Sprite(16, 1, 8, SpriteSheet.tiles, 16, 16); public static Sprite explosion_horizontal2 = new Sprite(16, 1, 9, SpriteSheet.tiles, 16, 16); - + public static Sprite explosion_horizontal_left_last = new Sprite(16, 0, 7, SpriteSheet.tiles, 16, 16); public static Sprite explosion_horizontal_left_last1 = new Sprite(16, 0, 8, SpriteSheet.tiles, 16, 16); public static Sprite explosion_horizontal_left_last2 = new Sprite(16, 0, 9, SpriteSheet.tiles, 16, 16); - + public static Sprite explosion_horizontal_right_last = new Sprite(16, 2, 7, SpriteSheet.tiles, 16, 16); public static Sprite explosion_horizontal_right_last1 = new Sprite(16, 2, 8, SpriteSheet.tiles, 16, 16); public static Sprite explosion_horizontal_right_last2 = new Sprite(16, 2, 9, SpriteSheet.tiles, 16, 16); - + public static Sprite explosion_vertical_top_last = new Sprite(16, 1, 4, SpriteSheet.tiles, 16, 16); public static Sprite explosion_vertical_top_last1 = new Sprite(16, 2, 4, SpriteSheet.tiles, 16, 16); public static Sprite explosion_vertical_top_last2 = new Sprite(16, 3, 4, SpriteSheet.tiles, 16, 16); - + public static Sprite explosion_vertical_down_last = new Sprite(16, 1, 6, SpriteSheet.tiles, 16, 16); public static Sprite explosion_vertical_down_last1 = new Sprite(16, 2, 6, SpriteSheet.tiles, 16, 16); public static Sprite explosion_vertical_down_last2 = new Sprite(16, 3, 6, SpriteSheet.tiles, 16, 16); - + /* - |-------------------------------------------------------------------------- - | Brick FlameSegment - |-------------------------------------------------------------------------- + * |-------------------------------------------------------------------------- + * | Brick FlameSegment + * |-------------------------------------------------------------------------- */ public static Sprite brick_exploded = new Sprite(16, 7, 1, SpriteSheet.tiles, 16, 16); public static Sprite brick_exploded1 = new Sprite(16, 7, 2, SpriteSheet.tiles, 16, 16); public static Sprite brick_exploded2 = new Sprite(16, 7, 3, SpriteSheet.tiles, 16, 16); - + /* - |-------------------------------------------------------------------------- - | Powerups - |-------------------------------------------------------------------------- + * |-------------------------------------------------------------------------- + * | Powerups + * |-------------------------------------------------------------------------- */ public static Sprite powerup_bombs = new Sprite(16, 0, 10, SpriteSheet.tiles, 16, 16); public static Sprite powerup_flames = new Sprite(16, 1, 10, SpriteSheet.tiles, 16, 16); @@ -176,7 +199,7 @@ public class Sprite { public static Sprite powerup_detonator = new Sprite(16, 4, 10, SpriteSheet.tiles, 16, 16); public static Sprite powerup_bombpass = new Sprite(16, 5, 10, SpriteSheet.tiles, 16, 16); public static Sprite powerup_flamepass = new Sprite(16, 6, 10, SpriteSheet.tiles, 16, 16); - + public Sprite(int size, int x, int y, SpriteSheet sheet, int rw, int rh) { SIZE = size; _pixels = new int[SIZE * SIZE]; @@ -187,6 +210,7 @@ public Sprite(int size, int x, int y, SpriteSheet sheet, int rw, int rh) { _realHeight = rh; load(); } + public Sprite(int size, int x, int y, SpriteSheet1 sheet1, int rw, int rh) { SIZE = size; _pixels = new int[SIZE * SIZE]; @@ -197,12 +221,13 @@ public Sprite(int size, int x, int y, SpriteSheet1 sheet1, int rw, int rh) { _realHeight = rh; load1(); } + public Sprite(int size, int color) { SIZE = size; _pixels = new int[SIZE * SIZE]; setColor(color); } - + private void setColor(int color) { for (int i = 0; i < _pixels.length; i++) { _pixels[i] = color; @@ -216,6 +241,7 @@ private void load() { } } } + private void load1() { for (int y = 0; y < SIZE; y++) { for (int x = 0; x < SIZE; x++) { @@ -223,26 +249,27 @@ private void load1() { } } } + public static Sprite movingSprite(Sprite normal, Sprite x1, Sprite x2, int animate, int time) { int calc = animate % time; int diff = time / 3; - - if(calc < diff) { + + if (calc < diff) { return normal; } - - if(calc < diff * 2) { + + if (calc < diff * 2) { return x1; } - + return x2; } - + public static Sprite movingSprite(Sprite x1, Sprite x2, int animate, int time) { int diff = time / 2; - return (animate % time > diff) ? x1 : x2; + return (animate % time > diff) ? x1 : x2; } - + public int getSize() { return SIZE; } diff --git a/src/uet/oop/bomberman/gui/Frame.java b/src/uet/oop/bomberman/gui/Frame.java index 1a331e4..5aeb3bc 100644 --- a/src/uet/oop/bomberman/gui/Frame.java +++ b/src/uet/oop/bomberman/gui/Frame.java @@ -4,6 +4,8 @@ import javax.swing.*; import java.awt.*; +import java.io.File; +import java.io.IOException; /** * Swing Frame chứa toàn bộ các component @@ -17,8 +19,18 @@ public class Frame extends JFrame { private Game _game; public Frame() { - - _containerpane = new JPanel(new BorderLayout()); + + try { + Font f = Font.createFont(Font.TRUETYPE_FONT, new File("res/font/Minecraft.ttf")); + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + ge.registerFont(f); + } catch (FontFormatException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + + _containerpane = new JPanel(new BorderLayout()); _gamepane = new GamePanel(this); _infopanel = new InfoPanel(_gamepane.getGame()); @@ -33,8 +45,9 @@ public Frame() { setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); pack(); setLocationRelativeTo(null); - setVisible(true); - + } + + public void start() { _game.start(); } @@ -45,5 +58,25 @@ public void setTime(int time) { public void setPoints(int points) { _infopanel.setPoints(points); } + + public void setLevel(int level) { + _infopanel.setLevel(level); + } + + public void setEnemy(int enemy) { + _infopanel.setEnemies(enemy); + } + + public void setHideInfoPanel() { + _infopanel.hideInfoPanel(); + } + + public void loadInfo() { + _infopanel.loadInfo(); + } + + public void renderItemTime() { + _infopanel.renderItemTime(); + } } diff --git a/src/uet/oop/bomberman/gui/GameScreen.java b/src/uet/oop/bomberman/gui/GameScreen.java new file mode 100644 index 0000000..a473496 --- /dev/null +++ b/src/uet/oop/bomberman/gui/GameScreen.java @@ -0,0 +1,9 @@ +package uet.oop.bomberman.gui; + +import java.awt.*; + +public abstract class GameScreen { + public abstract void drawScreen(Graphics g); + public abstract void update(); + public abstract void onDestroy(); +} diff --git a/src/uet/oop/bomberman/gui/InfoPanel.java b/src/uet/oop/bomberman/gui/InfoPanel.java index de01310..848cb67 100644 --- a/src/uet/oop/bomberman/gui/InfoPanel.java +++ b/src/uet/oop/bomberman/gui/InfoPanel.java @@ -1,42 +1,139 @@ package uet.oop.bomberman.gui; import uet.oop.bomberman.Game; +import uet.oop.bomberman.entities.tile.item.Item; +import uet.oop.bomberman.utils.EGameMode; +import uet.oop.bomberman.utils.EScreenName; +import uet.oop.bomberman.utils.Global; import javax.swing.*; import java.awt.*; +import java.util.List; /** * Swing Panel hiển thị thông tin thời gian, điểm mà người chơi đạt được */ public class InfoPanel extends JPanel { - private JLabel timeLabel; + private JLabel p2TimeLabel; private JLabel pointsLabel; + private JLabel itemTimeLabel; + private JLabel levelLabel; + private JLabel enemyLabel; + + private final Game game; public InfoPanel(Game game) { + this.game = game; setLayout(new GridLayout()); - - timeLabel = new JLabel("Time: " + game.getBoard().getTime()); + + levelLabel = new JLabel("Level: " + Global.gameLevel); + levelLabel.setForeground(Color.white); + levelLabel.setHorizontalAlignment(JLabel.CENTER); + + enemyLabel = new JLabel("Enemies: " + Global.enemies); + enemyLabel.setForeground(Color.white); + enemyLabel.setFont(new Font("DejaVu Sans", Font.PLAIN, 15)); + enemyLabel.setHorizontalAlignment(JLabel.CENTER); + + timeLabel = new JLabel("Time: " + game.getBoard().getGameInfoManager().getTime() / Game.TICKS_PER_SECOND); timeLabel.setForeground(Color.white); + timeLabel.setFont(new Font("DejaVu Sans", Font.PLAIN, 15)); timeLabel.setHorizontalAlignment(JLabel.CENTER); - - pointsLabel = new JLabel("Points: " + game.getBoard().getPoints()); + + pointsLabel = new JLabel("Points: " + game.getBoard().getGameInfoManager().getPoints()); pointsLabel.setForeground(Color.white); + pointsLabel.setFont(new Font("DejaVu Sans", Font.PLAIN, 15)); pointsLabel.setHorizontalAlignment(JLabel.CENTER); - - add(timeLabel); - add(pointsLabel); - + + itemTimeLabel = new JLabel(""); + itemTimeLabel.setForeground(Color.white); + itemTimeLabel.setHorizontalAlignment(JLabel.CENTER); + itemTimeLabel.setFont(new Font("DejaVu Sans", Font.PLAIN, 15)); + itemTimeLabel.setBorder(BorderFactory.createEmptyBorder(0, 40, 0, 0)); + + p2TimeLabel = new JLabel(""); + p2TimeLabel.setForeground(Color.white); + p2TimeLabel.setHorizontalAlignment(JLabel.CENTER); + p2TimeLabel.setFont(new Font("DejaVu Sans", Font.PLAIN, 15)); + p2TimeLabel.setBorder(BorderFactory.createEmptyBorder(0, 40, 0, 0)); + setBackground(Color.black); setPreferredSize(new Dimension(0, 40)); } - + public void setTime(int t) { - timeLabel.setText("Time: " + t); + timeLabel.setText("⏰ " + t / Game.TICKS_PER_SECOND); } public void setPoints(int t) { - pointsLabel.setText("Score: " + t); + pointsLabel.setText("💵 " + t); + } + + public void setLevel(int t) { + levelLabel.setText("Level: " + t); + } + + public void setEnemies(int t) { + enemyLabel.setText("👻 " + t); + } + + public void renderItemTime() { + String label = ""; + + List items = game.getBoard().getGameInfoManager().getPlayerActiveItems(); + for (int i = 0; i < items.size(); i++) { + Item item = items.get(i); + if ((item.getDuration()) == 0) { + continue; + } + label += item.getDisplayActiveItem() + item.getDuration() / Game.TICKS_PER_SECOND + " "; + } + + if (Global.gameMode == EGameMode.TWO_PLAYER) { + String label2 = ""; + List p2Items = game.getBoard().getGameInfoManager().getPlayer2ActiveItems(); + for (int i = 0; i < p2Items.size(); i++) { + Item item = p2Items.get(i); + if ((item.getDuration()) == 0) { + continue; + } + label2 += item.getDisplayActiveItem() + item.getDuration() / Game.TICKS_PER_SECOND + " "; + } + + if (label2 != "") { + label2 = "P2 " + label2; + } + if (label != "") { + label = "P1 " + label; + } + + p2TimeLabel.setText(label2); + itemTimeLabel.setText(label); + } else { + itemTimeLabel.setText(label); + } + } + + public void hideInfoPanel() { + if (Global.currentScreen == EScreenName.GAME_PLAY_SCREEN) { + System.out.println("vao day"); + remove(itemTimeLabel); + remove(timeLabel); + remove(pointsLabel); + remove(enemyLabel); + remove(p2TimeLabel); + } + } + + public void loadInfo() { + add(itemTimeLabel); + add(timeLabel); + add(pointsLabel); + add(enemyLabel); + + if (Global.gameMode == EGameMode.TWO_PLAYER) { + add(p2TimeLabel); + } } - } diff --git a/src/uet/oop/bomberman/input/Keyboard.java b/src/uet/oop/bomberman/input/Keyboard.java index bb8daad..88b8604 100644 --- a/src/uet/oop/bomberman/input/Keyboard.java +++ b/src/uet/oop/bomberman/input/Keyboard.java @@ -1,37 +1,122 @@ package uet.oop.bomberman.input; +import uet.oop.bomberman.utils.EGameControl; + +import java.awt.RenderingHints.Key; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; +import java.util.Optional; /** * Tiếp nhận và xử lý các sự kiện nhập từ bàn phím */ public class Keyboard implements KeyListener { - - private boolean[] keys = new boolean[120]; //120 is enough to this game - public boolean up, down, left, right, space; - + + private static Keyboard INST = null; + + public static Keyboard i() { + if (INST == null) { + INST = new Keyboard(); + } + return INST; + } + + private Keyboard() { + } + + public interface KeyboardInputCallback { + void onKeyPressed(EGameControl gameControl); + } + + private boolean[] keys = new boolean[65536]; // 120 is enough to this game + public boolean up, down, left, right, space, x, pause, resume; + public Optional keyboardInputCallback; + + public boolean player1_up, player1_down, player1_left, player1_right, player1_bomb; + public boolean player2_up, player2_down, player2_left, player2_right, player2_bomb; + + public boolean back; + public void update() { up = keys[KeyEvent.VK_UP] || keys[KeyEvent.VK_W]; down = keys[KeyEvent.VK_DOWN] || keys[KeyEvent.VK_S]; left = keys[KeyEvent.VK_LEFT] || keys[KeyEvent.VK_A]; right = keys[KeyEvent.VK_RIGHT] || keys[KeyEvent.VK_D]; - space = keys[KeyEvent.VK_SPACE] || keys[KeyEvent.VK_X]; + + space = keys[KeyEvent.VK_SPACE]; + x = keys[KeyEvent.VK_X]; + + pause = keys[KeyEvent.VK_ESCAPE]; + resume = keys[KeyEvent.VK_ENTER]; + back = keys[KeyEvent.VK_B]; + + // Player 1 + player1_up = keys[KeyEvent.VK_W]; + player1_down = keys[KeyEvent.VK_S]; + player1_left = keys[KeyEvent.VK_A]; + player1_right = keys[KeyEvent.VK_D]; + player1_bomb = keys[KeyEvent.VK_X]; + + // Player 2 + player2_up = keys[KeyEvent.VK_UP]; + player2_down = keys[KeyEvent.VK_DOWN]; + player2_left = keys[KeyEvent.VK_LEFT]; + player2_right = keys[KeyEvent.VK_RIGHT]; + player2_bomb = keys[KeyEvent.VK_SPACE]; + + } + + private EGameControl keyToGameControl(int keyCode) { + if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_W) { + return EGameControl.UP; + } + + if (keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_S) { + return EGameControl.DOWN; + } + + if (keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_A) { + return EGameControl.LEFT; + } + + if (keyCode == KeyEvent.VK_RIGHT || keyCode == KeyEvent.VK_D) { + return EGameControl.RIGHT; + } + + if (keyCode == KeyEvent.VK_SPACE) { + return EGameControl.SPACE; + } + + if (keyCode == KeyEvent.VK_ENTER) { + return EGameControl.ENTER; + } + if (keyCode == KeyEvent.VK_X) { + return EGameControl.X; + } + if (keyCode == KeyEvent.VK_B) { + return EGameControl.BACK; + } + + return EGameControl.NONE; } @Override - public void keyTyped(KeyEvent e) {} + public void keyTyped(KeyEvent e) { + } @Override public void keyPressed(KeyEvent e) { keys[e.getKeyCode()] = true; - + if (keyboardInputCallback.isPresent()) { + KeyboardInputCallback callback = keyboardInputCallback.get(); + callback.onKeyPressed(keyToGameControl(e.getKeyCode())); + } } @Override public void keyReleased(KeyEvent e) { keys[e.getKeyCode()] = false; - + } -} +} \ No newline at end of file diff --git a/src/uet/oop/bomberman/level/FileLevelLoader.java b/src/uet/oop/bomberman/level/FileLevelLoader.java index 3432f1e..60a6853 100644 --- a/src/uet/oop/bomberman/level/FileLevelLoader.java +++ b/src/uet/oop/bomberman/level/FileLevelLoader.java @@ -2,15 +2,32 @@ import java.io.BufferedReader; import java.io.FileReader; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import uet.oop.bomberman.Board; import uet.oop.bomberman.Game; +import uet.oop.bomberman.agent.KeyboardAgent; +import uet.oop.bomberman.agent.KeyboardAgentPlayer1; +import uet.oop.bomberman.agent.KeyboardAgentPlayer2; +import uet.oop.bomberman.agent.MovingAgent; +import uet.oop.bomberman.agent.base.Agent; +import uet.oop.bomberman.agent.ppo.NaivePPOAgent; +import uet.oop.bomberman.agent.rulebased.RuleBasedBomberAgent; +import uet.oop.bomberman.agent.sac.SacAgent; +import uet.oop.bomberman.agent.state.NaivePlayerStateExtractor; import uet.oop.bomberman.entities.LayeredEntity; import uet.oop.bomberman.entities.character.Bomber; +import uet.oop.bomberman.entities.character.Bomber2; import uet.oop.bomberman.entities.character.enemy.Balloon; import uet.oop.bomberman.entities.character.enemy.Doll; +import uet.oop.bomberman.entities.character.enemy.Enemy; +import uet.oop.bomberman.entities.character.enemy.Kondoria; +import uet.oop.bomberman.entities.character.enemy.Minvo; import uet.oop.bomberman.entities.character.enemy.Oneal; +import uet.oop.bomberman.entities.character.enemy.ai.AIHigh; +import uet.oop.bomberman.entities.character.enemy.ai.AILow; +import uet.oop.bomberman.entities.character.enemy.ai.AIMedium; import uet.oop.bomberman.entities.tile.Grass; import uet.oop.bomberman.entities.tile.Portal; import uet.oop.bomberman.entities.tile.Wall; @@ -21,6 +38,8 @@ import uet.oop.bomberman.exceptions.LoadLevelException; import uet.oop.bomberman.graphics.Screen; import uet.oop.bomberman.graphics.Sprite; +import uet.oop.bomberman.utils.EGameMode; +import uet.oop.bomberman.utils.Global; public class FileLevelLoader extends LevelLoader { @@ -36,41 +55,45 @@ public FileLevelLoader(Board board, int level) throws LoadLevelException { @Override public void loadLevel(int level) { - // TODO: đọc dữ liệu từ tệp cấu hình /levels/Level{level}.txt - // TODO: cập nhật các giá trị đọc được vào _width, _height, _level, _map List list = new ArrayList<>(); try { - FileReader fr = new FileReader("res\\levels\\Level" + level + ".txt");//doc tep luu map + String filePath; + if (Global.gameMode == EGameMode.ONE_PLAYER) { + filePath = "res/levels/Level1P_" + level + ".txt"; + } else { + filePath = "res/levels/Level2P_" + level + ".txt"; + } + + FileReader fr = new FileReader(filePath); // Đọc tệp lưu map BufferedReader br = new BufferedReader(fr); String line = br.readLine(); - while (!line.equals("")) { + while (line != null && !line.isEmpty()) { list.add(line); line = br.readLine(); - //doc file txt luu vao list } } catch (Exception e) { e.printStackTrace(); } - String[] arrays = list.get(0).trim().split(" "); - _level = Integer.parseInt(arrays[0]); - _height = Integer.parseInt(arrays[1]); - _width = Integer.parseInt(arrays[2]); - _map = new char[_height][_width]; - for (int i = 0; i < _height; i++) { - for (int j = 0; j < _width; j++) { - _map[i][j] = list.get(i + 1).charAt(j); + + if (!list.isEmpty()) { + String[] arrays = list.get(0).trim().split(" "); + _level = Integer.parseInt(arrays[0]); + _height = Integer.parseInt(arrays[1]); + _width = Integer.parseInt(arrays[2]); + _map = new char[_height][_width]; + for (int i = 0; i < _height; i++) { + for (int j = 0; j < _width; j++) { + _map[i][j] = list.get(i + 1).charAt(j); + } } } - //gan cac phan tu cho mang } @Override public void createEntities() { - // TODO: tạo các Entity của màn chơi - // TODO: sau khi tạo xong, gọi _board.addEntity() để thêm Entity vào game - - // TODO: phần code mẫu ở dưới để hướng dẫn cách thêm các loại Entity vào game - // TODO: hãy xóa nó khi hoàn thành chức năng load màn chơi từ tệp cấu hình + Enemy enemy; + uet.oop.bomberman.agent.base.Agent agent; + LayeredEntity layeredEntity; for (int y = 0; y < getHeight(); y++) { for (int x = 0; x < getWidth(); x++) { int pos = x + y * getWidth(); @@ -78,78 +101,173 @@ public void createEntities() { switch (c) { // Thêm grass case ' ': - _board.addEntity(pos, new Grass(x, y, Sprite.grass)); + _board.getEntityManager().getTileManager().addTile(pos, new Grass(x, y, Sprite.grass)); break; // Thêm Wall case '#': - _board.addEntity(pos, new Wall(x, y, Sprite.wall)); + _board.getEntityManager().getTileManager().addTile(pos, new Wall(x, y, Sprite.wall)); break; // Thêm Portal case 'x': - _board.addEntity(pos, new LayeredEntity(x, y, + layeredEntity = new LayeredEntity( + x, y, new Grass(x, y, Sprite.grass), - new Portal(x, y, _board, Sprite.portal), - new Brick(x, y, Sprite.brick))); + new Portal(x, y, _board.getLevelManager(), _board.getEntityManager(), Sprite.portal), + new Brick(x, y, Sprite.brick)); + _board.getEntityManager().getTileManager().addTile(pos, layeredEntity); break; // Thêm brick case '*': - _board.addEntity(x + y * _width, - new LayeredEntity(x, y, - new Grass(x, y, Sprite.grass), - new Brick(x, y, Sprite.brick) - ) - ); + layeredEntity = new LayeredEntity( + x, y, + new Grass(x, y, Sprite.grass), + new Brick(x, y, Sprite.brick)); + _board.getEntityManager().getTileManager().addTile(x + y * _width, layeredEntity); break; - // Thêm Bomber + // Thêm Bomber player case 'p': - _board.addCharacter(new Bomber(Coordinates.tileToPixel(x), Coordinates.tileToPixel(y) + Game.TILES_SIZE, _board)); + Bomber bomber = new Bomber( + Coordinates.tileToPixel(x), + Coordinates.tileToPixel(y) + Game.TILES_SIZE, + Game.BOMBERSPEED, + Game.BOMBRATE, + Game.BOMBRADIUS, + _board.getEntityManager()); + _board.getEntityManager().getCharacterManager().addCharacter(bomber); + _board.getEntityManager().getCharacterManager().setPlayer(bomber); + _board.getEntityManager().getCharacterManager().addPlayer(bomber); Screen.setOffset(0, 0); - _board.addEntity(x + y * _width, new Grass(x, y, Sprite.grass)); + _board.getEntityManager().getTileManager().addTile(x + y * _width, + new Grass(x, y, Sprite.grass)); + // if + if (Global.gameMode == EGameMode.TWO_PLAYER) { + agent = new KeyboardAgentPlayer1(bomber); + } else if (Global.isRuleBasedPlayer) { + agent = new RuleBasedBomberAgent(bomber, _board); + } else if (Global.isAIPlayer) { + SacAgent bomberAgent = new SacAgent( + bomber, + _board, + new NaivePlayerStateExtractor(bomber) + ); + bomberAgent.setModelPath("models/sac"); + bomberAgent.load(bomberAgent.getModelPath()); + agent = bomberAgent; + } else { + agent = new KeyboardAgent(bomber); + } + _board.addAgent(agent); + break; + // Thêm player 2: + case 'a': + Bomber bomber2 = new Bomber2( + Coordinates.tileToPixel(x), + Coordinates.tileToPixel(y) + Game.TILES_SIZE, + Game.BOMBERSPEED, + Game.BOMBRATE, + Game.BOMBRADIUS, + _board.getEntityManager()); + _board.getEntityManager().getCharacterManager().addCharacter(bomber2); + _board.getEntityManager().getCharacterManager().setPlayer(bomber2); + _board.getEntityManager().getCharacterManager().addPlayer(bomber2); + Screen.setOffset(0, 0); + _board.getEntityManager().getTileManager().addTile(x + y * _width, + new Grass(x, y, Sprite.grass)); + agent = new KeyboardAgentPlayer2(bomber2); + _board.addAgent(agent); break; - // Thêm balloon case '1': - _board.addCharacter(new Balloon(Coordinates.tileToPixel(x), Coordinates.tileToPixel(y) + Game.TILES_SIZE, _board)); - _board.addEntity(x + y * _width, new Grass(x, y, Sprite.grass)); + enemy = new Balloon( + Coordinates.tileToPixel(x), + Coordinates.tileToPixel(y) + Game.TILES_SIZE, + _board.getEntityManager()); + _board.getEntityManager().getCharacterManager().addCharacter(enemy); + _board.getEntityManager().getTileManager().addTile(x + y * _width, + new Grass(x, y, Sprite.grass)); + agent = new MovingAgent(enemy, new AILow()); + _board.addAgent(agent); break; // Thêm oneal case '2': - _board.addCharacter(new Oneal(Coordinates.tileToPixel(x), Coordinates.tileToPixel(y) + Game.TILES_SIZE, _board)); - _board.addEntity(pos, new Grass(x, y, Sprite.grass)); + enemy = new Oneal( + Coordinates.tileToPixel(x), + Coordinates.tileToPixel(y) + Game.TILES_SIZE, + _board.getEntityManager()); + _board.getEntityManager().getCharacterManager().addCharacter(enemy); + _board.getEntityManager().getTileManager().addTile(pos, new Grass(x, y, Sprite.grass)); + agent = new MovingAgent(enemy, new AILow()); + _board.addAgent(agent); break; // Thêm doll case '3': - _board.addCharacter(new Doll(Coordinates.tileToPixel(x), Coordinates.tileToPixel(y) + Game.TILES_SIZE, _board)); - _board.addEntity(x + y * _width, new Grass(x, y, Sprite.grass)); + enemy = new Doll( + Coordinates.tileToPixel(x), + Coordinates.tileToPixel(y) + Game.TILES_SIZE, + _board.getEntityManager()); + _board.getEntityManager().getCharacterManager().addCharacter(enemy); + _board.getEntityManager().getTileManager().addTile(x + y * _width, + new Grass(x, y, Sprite.grass)); + agent = new MovingAgent(enemy, new AILow()); + _board.addAgent(agent); break; - // Thêm oneal - // Thêm BomItem + // Thêm minvo + case '4': + enemy = new Minvo( + Coordinates.tileToPixel(x), + Coordinates.tileToPixel(y) + Game.TILES_SIZE, + _board.getEntityManager()); + _board.getEntityManager().getCharacterManager().addCharacter(enemy); + _board.getEntityManager().getTileManager().addTile(x + y * _width, + new Grass(x, y, Sprite.grass)); + agent = new MovingAgent(enemy, + new AIMedium(enemy, _board.getEntityManager().getCharacterManager())); + _board.addAgent(agent); + break; + // Thêm kondoria + case '5': + enemy = new Kondoria( + Coordinates.tileToPixel(x), + Coordinates.tileToPixel(y) + Game.TILES_SIZE, + _board.getEntityManager()); + _board.getEntityManager().getCharacterManager().addCharacter(enemy); + _board.getEntityManager().getTileManager().addTile(x + y * _width, + new Grass(x, y, Sprite.grass)); + agent = new MovingAgent(enemy, + new AIHigh(enemy, _board.getEntityManager().getCharacterManager(), + _board.getEntityManager().getBombManager())); + _board.addAgent(agent); + break; + // Thêm BomItem case 'b': - LayeredEntity layer = new LayeredEntity(x, y, + layeredEntity = new LayeredEntity( + x, y, new Grass(x, y, Sprite.grass), new BombItem(x, y, Sprite.powerup_bombs), new Brick(x, y, Sprite.brick)); - _board.addEntity(pos, layer); + _board.getEntityManager().getTileManager().addTile(pos, layeredEntity); break; // Thêm SpeedItem case 's': - layer = new LayeredEntity(x, y, + layeredEntity = new LayeredEntity( + x, y, new Grass(x, y, Sprite.grass), new SpeedItem(x, y, Sprite.powerup_speed), new Brick(x, y, Sprite.brick)); - _board.addEntity(pos, layer); + _board.getEntityManager().getTileManager().addTile(pos, layeredEntity); break; // Thêm FlameItem case 'f': - layer = new LayeredEntity(x, y, + layeredEntity = new LayeredEntity( + x, y, new Grass(x, y, Sprite.grass), new FlameItem(x, y, Sprite.powerup_flames), new Brick(x, y, Sprite.brick)); - _board.addEntity(pos, layer); + _board.getEntityManager().getTileManager().addTile(pos, layeredEntity); break; default: - _board.addEntity(pos, new Grass(x, y, Sprite.grass)); + _board.getEntityManager().getTileManager().addTile(pos, new Grass(x, y, Sprite.grass)); break; } diff --git a/src/uet/oop/bomberman/manager/BombManager.java b/src/uet/oop/bomberman/manager/BombManager.java new file mode 100644 index 0000000..0c2c9fb --- /dev/null +++ b/src/uet/oop/bomberman/manager/BombManager.java @@ -0,0 +1,72 @@ +package uet.oop.bomberman.manager; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import uet.oop.bomberman.base.IBombManager; +import uet.oop.bomberman.entities.bomb.Bomb; +import uet.oop.bomberman.entities.bomb.FlameSegment; +import uet.oop.bomberman.graphics.Screen; + +public class BombManager implements IBombManager { + + private List bombs = new ArrayList<>(); + + public BombManager() { + } + + @Override + public void addBomb(Bomb e) { + bombs.add(e); + } + + @Override + public List getBombs() { + return bombs; + } + + @Override + public Bomb getBombAt(double x, double y) { + Iterator bs = bombs.iterator(); + Bomb b; + while (bs.hasNext()) { + b = bs.next(); + if (b.getX() == (int) x && b.getY() == (int) y) + return b; + } + + return null; + } + + @Override + public FlameSegment getFlameSegmentAt(int x, int y) { + Iterator bs = bombs.iterator(); + Bomb b; + while (bs.hasNext()) { + b = bs.next(); + + FlameSegment e = b.flameAt(x, y); + if (e != null) { + return e; + } + } + + return null; + } + + @Override + public void update() { + bombs.forEach(Bomb::update); + bombs = bombs.stream() + .filter(bomb -> !bomb.isRemoved()) + .collect(Collectors.toList()); + } + + @Override + public void render(Screen screen) { + bombs.forEach(bomb -> bomb.render(screen)); + } + +} diff --git a/src/uet/oop/bomberman/manager/CharacterManager.java b/src/uet/oop/bomberman/manager/CharacterManager.java new file mode 100644 index 0000000..9dd28f4 --- /dev/null +++ b/src/uet/oop/bomberman/manager/CharacterManager.java @@ -0,0 +1,140 @@ +package uet.oop.bomberman.manager; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import uet.oop.bomberman.Game; +import uet.oop.bomberman.base.IBombManager; +import uet.oop.bomberman.base.ICharacterManager; +import uet.oop.bomberman.base.IGameInfoManager; +import uet.oop.bomberman.base.ILevelManager; +import uet.oop.bomberman.entities.Message; +import uet.oop.bomberman.entities.character.CanUseItem; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.tile.item.Item; +import uet.oop.bomberman.graphics.Screen; +import uet.oop.bomberman.sound.Sound; +import uet.oop.bomberman.utils.Global; + +public class CharacterManager implements ICharacterManager { + + private List characters = new ArrayList<>(); + // list + private Character player; + private List players = new ArrayList<>(); + + private final IGameInfoManager gameInfoManager; + private final ILevelManager levelManager; + private int numberOfPlayers; + + public CharacterManager(IGameInfoManager gameInfoManager, ILevelManager levelManager) { + this.gameInfoManager = gameInfoManager; + this.levelManager = levelManager; + // initializeNumberOfPlayers(0); + } + + @Override + public Character getCharacterAtExcluding(int x, int y, Character a) { + Iterator itr = characters.iterator(); + + Character cur; + while (itr.hasNext()) { + cur = itr.next(); + if (cur == a) { + continue; + } + + if (cur.getXTile() == x && cur.getYTile() == y) { + return cur; + } + + } + + return null; + } + + @Override + public void addCharacter(Character e) { + characters.add(e); + } + + @Override + public void setPlayer(Character character) { + this.player = character; + } + + @Override + public Character getPlayer() { + return this.player; + } + + @Override + public void addPlayer(Character e) { + players.add(e); + numberOfPlayers += 1; + } + + @Override + public List getPlayers() { + return players; + } + + @Override + public void handleOnDeath(Character character, Character killer) { + if (character.isPlayer()) { + Sound.play("endgame3"); + } else { + double messageX = (character.getX() * Game.SCALE) + (character.getSprite().SIZE / 2 * Game.SCALE); + double messageY = (character.getY() * Game.SCALE) - (character.getSprite().SIZE / 2 * Game.SCALE); + int points = character.getPoints(); + gameInfoManager.addPoints(points); + Global.enemies--; + Message msg = new Message("+" + points, messageX, messageY, 2, Color.white, 14); + gameInfoManager.addMessage(msg); + Sound.play("AA126_11"); + } + } + + @Override + public void handleAfterDeath(Character character) { + if (character.isPlayer()) { + numberOfPlayers--; + if (numberOfPlayers == 0) { + levelManager.endGame(); + } + } + } + + @Override + public void update() { + for (Character character : characters) { + character.update(); + if (character instanceof CanUseItem) { + CanUseItem characterCanUseItem = ((CanUseItem) character); + characterCanUseItem.getActiveItems().forEach(Item::update); + } + } + characters = characters.stream() + .filter(character -> !character.isRemoved()) + .collect(Collectors.toList()); + } + + @Override + public void render(Screen screen) { + characters.forEach(character -> character.render(screen)); + } + + @Override + public List getCharacters() { + return characters; + } + + @Override + public IBombManager getBombManager() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'getBombManager'"); + } +} diff --git a/src/uet/oop/bomberman/manager/EntityManager.java b/src/uet/oop/bomberman/manager/EntityManager.java new file mode 100644 index 0000000..2b624c9 --- /dev/null +++ b/src/uet/oop/bomberman/manager/EntityManager.java @@ -0,0 +1,114 @@ +package uet.oop.bomberman.manager; + +import java.util.List; + +import uet.oop.bomberman.base.IBombManager; +import uet.oop.bomberman.base.ICharacterManager; +import uet.oop.bomberman.base.IEntityManager; +import uet.oop.bomberman.base.IGameInfoManager; +import uet.oop.bomberman.base.ILevelManager; +import uet.oop.bomberman.base.ITileManager; +import uet.oop.bomberman.entities.Entity; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.graphics.Screen; + +public class EntityManager implements IEntityManager { + + private ITileManager tileManager; + private ICharacterManager characterManager; + private IBombManager bombManager; + + private final int boardWidth; + private final int boardHeight; + + public EntityManager(int boardWidth, int boardHeight, IGameInfoManager gameInfoManager, ILevelManager levelManager) { + this.boardWidth = boardWidth; + this.boardHeight = boardHeight; + this.tileManager = new TileManager(boardWidth, boardHeight); + this.characterManager = new CharacterManager(gameInfoManager, levelManager); + this.bombManager = new BombManager(); + } + + @Override + public Entity getEntityAtExcluding(double x, double y, Character m) { + + Entity res = null; + + if (x < 0) return null; + if (y < 0) return null; + if (x >= boardWidth) return null; + if (y >= boardHeight) return null; + + res = bombManager.getFlameSegmentAt((int) x, (int) y); + if (res != null) + return res; + + res = bombManager.getBombAt(x, y); + if (res != null) + return res; + + res = characterManager.getCharacterAtExcluding((int) x, (int) y, m); + if (res != null) + return res; + + res = tileManager.getTileAt((int) x, (int) y); + + return res; + } + + @Override + public boolean isEnemyCleared() { + // viết lại thành dòng for: kiểm tra trong list character xem có ai nằm trong + // list players hay không + // Nếu có thì chưa clear -> false + // Nếu không còn thì clear -> true + // return !characterManager.getCharacters().stream() + // .anyMatch(character -> characterManager.getPlayers().contains(character)); + for (Character character : characterManager.getCharacters()) { + if (!characterManager.getPlayers().contains(character)) { + return false; + } + } + return true; + } + + @Override + public void update() { + tileManager.update(); + characterManager.update(); + bombManager.update(); + } + + @Override + public void render(Screen screen) { + tileManager.render(screen); + characterManager.render(screen); + bombManager.render(screen); + } + + @Override + public Character getPlayer() { + return characterManager.getPlayer(); + } + + @Override + public List getPlayers() { + return characterManager.getPlayers(); + } + + @Override + public ITileManager getTileManager() { + return tileManager; + } + + @Override + public ICharacterManager getCharacterManager() { + return characterManager; + } + + @Override + public IBombManager getBombManager() { + return bombManager; + } + +} diff --git a/src/uet/oop/bomberman/manager/GameInfoManager.java b/src/uet/oop/bomberman/manager/GameInfoManager.java new file mode 100644 index 0000000..74cbb35 --- /dev/null +++ b/src/uet/oop/bomberman/manager/GameInfoManager.java @@ -0,0 +1,139 @@ +package uet.oop.bomberman.manager; + +import java.awt.Font; +import java.awt.Graphics; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import uet.oop.bomberman.Game; +import uet.oop.bomberman.base.IEntityManager; +import uet.oop.bomberman.base.IGameInfoManager; +import uet.oop.bomberman.entities.Message; +import uet.oop.bomberman.entities.character.CanUseItem; +import uet.oop.bomberman.entities.character.Character; +import uet.oop.bomberman.entities.tile.item.Item; +import uet.oop.bomberman.graphics.Screen; + +public class GameInfoManager implements IGameInfoManager { + + private int time; + private int points; + private boolean paused; + private List messages = new ArrayList<>(); + + private IEntityManager entityManager; + + public GameInfoManager() { + this.time = Game.TIME; + this.points = Game.POINTS; + } + + public void setEntityManager(IEntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + public List getPlayerActiveItems() { + List players = entityManager.getPlayers(); + if (players.size() < 1) { + return new ArrayList<>(); + } + Character player = players.get(0); + if (!(player instanceof CanUseItem)) return new ArrayList<>(); + return ((CanUseItem)player).getActiveItems().collect(Collectors.toList()); + } + + @Override + public List getPlayer2ActiveItems() { + List players = entityManager.getPlayers(); + if (players.size() < 2) { + return new ArrayList<>(); + } + Character player = players.get(1); + if (!(player instanceof CanUseItem)) return new ArrayList<>(); + return ((CanUseItem)player).getActiveItems().collect(Collectors.toList()); + } + + @Override + public void addMessage(Message e) { + messages.add(e); + } + + @Override + public int subtractTime() { + if (!isPaused() && time > 0) + return --time; + else + return time; + } + + @Override + public int getTime() { + return time; + } + + @Override + public int getPoints() { + return points; + } + + @Override + public void addPoints(int points) { + this.points += points; + } + + @Override + public void update() { + updateMessages(); + subtractTime(); + } + + @Override + public void render(Screen screen) {} + + private void updateMessages() { + Message m; + int left; + for (int i = 0; i < messages.size(); i++) { + m = messages.get(i); + left = m.getDuration(); + + if (left > 0) + m.setDuration(--left); + else + messages.remove(i); + } + } + + public void render(Screen screen, Graphics g) { + renderMessages(g); + } + + public void renderMessages(Graphics g) { + Message m; + for (int i = 0; i < messages.size(); i++) { + m = messages.get(i); + + g.setFont(new Font("Arial", Font.PLAIN, m.getSize())); + g.setColor(m.getColor()); + g.drawString(m.getMessage(), (int) m.getX() - Screen.xOffset * Game.SCALE, (int) m.getY()); + } + } + + @Override + public boolean isPaused() { + return paused; + } + + @Override + public void pause() { + paused = true; + } + + @Override + public void unpause() { + paused = false; + } + +} diff --git a/src/uet/oop/bomberman/manager/LevelManager.java b/src/uet/oop/bomberman/manager/LevelManager.java new file mode 100644 index 0000000..9db70ea --- /dev/null +++ b/src/uet/oop/bomberman/manager/LevelManager.java @@ -0,0 +1,83 @@ +package uet.oop.bomberman.manager; + +import uet.oop.bomberman.Board; +import uet.oop.bomberman.base.ILevelManager; +import uet.oop.bomberman.exceptions.LoadLevelException; +import uet.oop.bomberman.level.FileLevelLoader; +import uet.oop.bomberman.level.LevelLoader; +import uet.oop.bomberman.utils.EGameMode; +import uet.oop.bomberman.utils.EScreenName; +import uet.oop.bomberman.utils.Global; + +public class LevelManager implements ILevelManager { + + private LevelLoader levelLoader; + protected Board board; + + public LevelManager(Board board) { + this.board = board; + } + + @Override + public void nextLevel() { + board.handleWinLevel(); + Global.gameLevel += 1; + loadGlobalLevel(); + } + + @Override + public void loadGlobalLevel() { + loadLevel(Global.gameLevel); + } + + private void loadLevel(int level) { + board.clear(); + try { + levelLoader = new FileLevelLoader(board, level); + calculateEnemies(); + } catch (LoadLevelException e) { + e.printStackTrace(); + } + synchronized (board) { + board.init(); + levelLoader.createEntities(); + } + } + + private void calculateEnemies() { + int level = Global.gameLevel; + EGameMode gameMode = Global.gameMode; + if (level == 1 && gameMode == EGameMode.ONE_PLAYER) { + Global.enemies = 2; + } else if (level == 2 && gameMode == EGameMode.ONE_PLAYER) { + Global.enemies = 5; + } else if (level == 3 && gameMode == EGameMode.ONE_PLAYER) { + Global.enemies = 9; + } else if (level == 1 && gameMode == EGameMode.TWO_PLAYER) { + Global.enemies = 2; + } else if (level == 2 && gameMode == EGameMode.TWO_PLAYER) { + Global.enemies = 5; + } else if (level == 3 && gameMode == EGameMode.TWO_PLAYER) { + Global.enemies = 9; + } + } + + @Override + public void endGame() { + board.handleLoseLevel(); + Global.currentScreen = EScreenName.END_GAME_SCREEN; + + board.getGameInfoManager().pause(); + } + + @Override + public int getBoardWidth() { + return levelLoader.getWidth(); + } + + @Override + public int getBoardHeight() { + return levelLoader.getHeight(); + } + +} diff --git a/src/uet/oop/bomberman/manager/TileManager.java b/src/uet/oop/bomberman/manager/TileManager.java new file mode 100644 index 0000000..7e6d377 --- /dev/null +++ b/src/uet/oop/bomberman/manager/TileManager.java @@ -0,0 +1,56 @@ +package uet.oop.bomberman.manager; + +import uet.oop.bomberman.Game; +import uet.oop.bomberman.base.ITileManager; +import uet.oop.bomberman.entities.tile.Tile; +import uet.oop.bomberman.graphics.Screen; + +public class TileManager implements ITileManager { + + private int width; + @SuppressWarnings("unused") private int height; + private final Tile[] tiles; + + public TileManager(int width, int height) { + this.width = width; + this.height = height; + tiles = new Tile[width * height]; + } + + @Override + public Tile getTileAt(double x, double y) { + int index = (int) x + (int) y * width; + if (index < 0 || index >= tiles.length) { + return null; + } + return tiles[index]; + } + + @Override + public void addTile(int pos, Tile e) { + tiles[pos] = e; + } + + @Override + public void update() { + for (Tile tile: tiles) { + tile.update(); + } + } + + @Override + public void render(Screen screen) { + // only render the visible part of screen + int x0 = Screen.xOffset / Game.TILES_SIZE; // tile precision, -> left X + int x1 = (Screen.xOffset + screen.getWidth() + Game.TILES_SIZE) / Game.TILES_SIZE; // -> right X + int y0 = Screen.yOffset / Game.TILES_SIZE; + int y1 = (Screen.yOffset + screen.getHeight()) / Game.TILES_SIZE; // render one tile plus to fix black margins + + for (int y = y0; y < y1; y++) { + for (int x = x0; x < x1; x++) { + tiles[x + y * width].render(screen); + } + } + } + +} diff --git a/src/uet/oop/bomberman/screen/DeadScreen.java b/src/uet/oop/bomberman/screen/DeadScreen.java new file mode 100644 index 0000000..2906aca --- /dev/null +++ b/src/uet/oop/bomberman/screen/DeadScreen.java @@ -0,0 +1,200 @@ +package uet.oop.bomberman.screen; + +import uet.oop.bomberman.Game; +import uet.oop.bomberman.gui.GameScreen; +import uet.oop.bomberman.input.Keyboard; +import uet.oop.bomberman.utils.EGameControl; +import uet.oop.bomberman.utils.Global; +import java.awt.*; +import java.util.ArrayList; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; + +public class DeadScreen extends GameScreen { + ArrayList options = new ArrayList<>(); + int selectorIndex = 0; + private Game game; + private BufferedImage restartIcon; + private BufferedImage homeIcon; + private BufferedImage backgroundImage; + private BufferedImage gameover; + + public DeadScreen(Game game) { + this.game = game; + options.add("Restart"); + options.add("Back Home"); + try { + gameover = ImageIO.read(getClass().getResource("/menu/gameover.png")); + restartIcon = ImageIO.read(getClass().getResource("/menu/restart.png")); + homeIcon = ImageIO.read(getClass().getResource("/menu/9165683_home_house_icon.png")); + backgroundImage = ImageIO.read(getClass().getResource("/menu/forest_by_forheksed_d9q4k94-fullview 1.png")); + homeIcon = resizeImage(homeIcon, 11 * Game.SCALE, 11 * Game.SCALE); + restartIcon = resizeImage(restartIcon, 11 * Game.SCALE, 11 * Game.SCALE); + restartIcon = colorizeIcon(restartIcon, Color.black); + homeIcon = colorizeIcon(homeIcon, Color.black); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public void setInput() { + Keyboard.i().keyboardInputCallback = java.util.Optional.of(new Keyboard.KeyboardInputCallback() { + @Override + public void onKeyPressed(EGameControl gameControl) { + switch (gameControl) { + case LEFT: + selectorIndex--; + break; + case RIGHT: + selectorIndex++; + break; + case ENTER: + if (selectorIndex == 0) { + game.restartGame(); + } else if (selectorIndex == 1) { + game.startNewGame(); + } + break; + } + + if (selectorIndex < 0) { + selectorIndex = options.size() - 1; + } else if (selectorIndex > options.size() - 1) { + selectorIndex = 0; + } + } + }); + } + + private BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, int targetHeight) { + BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = resizedImage.createGraphics(); + g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null); + g2d.dispose(); + return resizedImage; + } + + // Rest of the class remains unchanged + private BufferedImage colorizeIcon(BufferedImage icon, Color color) { + BufferedImage newIcon = new BufferedImage(icon.getWidth(), icon.getHeight(), BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < icon.getWidth(); x++) { + for (int y = 0; y < icon.getHeight(); y++) { + int argb = icon.getRGB(x, y); + if ((argb >> 24) != 0x00) { // Check if pixel is not transparent + newIcon.setRGB(x, y, color.getRGB()); + } else { + newIcon.setRGB(x, y, argb); + } + } + } + return newIcon; + } + + @Override + public void drawScreen(Graphics g) { + + if (backgroundImage != null) { + g.drawImage(backgroundImage, 0, 0, Global.screenWidth, Global.screenHeight, null); + } else { + g.setColor(Color.BLACK); + g.fillRect(0, 0, Global.screenWidth, Global.screenHeight); + } + drawTitle(g); + drawPOINTS(g, game.getBoard().getGameInfoManager().getPoints()); + drawTIMES(g, game.getBoard().getGameInfoManager().getTime()); + drawOptions(g); + drawSelector(g); + g.drawImage(gameover, Global.screenWidth / 3 + 40, Global.screenHeight / 2 + 80, 50 * Game.SCALE, + 50 * Game.SCALE, null); + } + + private void drawTitle(Graphics g) { + String title = "GAME OVER"; + Font font = new Font("Arial", Font.BOLD, 22 * Game.SCALE); + g.setFont(font); + g.setColor(Color.BLACK); + + FontMetrics fm = g.getFontMetrics(); + int x = (Global.screenWidth - fm.stringWidth(title)) / 2; + int marginTop = 180; + int y = marginTop + fm.getAscent(); + + g.drawString(title, x, y); + } + + private void drawPOINTS(Graphics g, int points) { + String Point = "POINTS: " + points; + Font font = new Font("Arial", Font.BOLD, 6 * Game.SCALE); + g.setFont(font); + g.setColor(Color.BLACK); + + FontMetrics fm = g.getFontMetrics(); + int textWidth = fm.stringWidth(Point); + int x = (Global.screenWidth - textWidth) / 2; // Vị trí x để chuỗi ở giữa màn hình + int marginTop = 255; + int y = marginTop + fm.getAscent(); + + g.drawString(Point, x, y); + + } + + private void drawTIMES(Graphics g, int times) { + String Point = "TIME : " + times / Game.TICKS_PER_SECOND; + Font font = new Font("Arial", Font.BOLD, 6 * Game.SCALE); + g.setFont(font); + g.setColor(Color.BLACK); + + FontMetrics fm = g.getFontMetrics(); + int textWidth = fm.stringWidth(Point); + int x = (Global.screenWidth - textWidth) / 2; // Vị trí x để chuỗi ở giữa màn hình + int marginTop = 280; + int y = marginTop + fm.getAscent(); + + g.drawString(Point, x, y); + + } + + private void drawOptions(Graphics g) { + int w = Global.screenWidth; + int h = Global.screenHeight; + int iconHeight = restartIcon.getHeight(); + int marginTop = (h - iconHeight + 50) / (2); + + int spacing = 70; + int totalOptionsWidth = restartIcon.getWidth() + spacing + homeIcon.getWidth(); + + int startX = (w - totalOptionsWidth) / 2; + + g.drawImage(restartIcon, startX, marginTop, null); + + int homeIconX = startX + restartIcon.getWidth() + spacing; + g.drawImage(homeIcon, homeIconX, marginTop, null); + } + + private void drawSelector(Graphics g) { + int w = Global.screenWidth; + int h = Global.screenHeight; + int iconHeight = restartIcon.getHeight(); + int marginTop = (h - iconHeight) / 2; + + int spacing = 80; + int totalOptionsWidth = restartIcon.getWidth() + spacing + homeIcon.getWidth(); + + int startX = (w - totalOptionsWidth) / 2; + + int selectorX = selectorIndex == 0 ? startX - 20 : startX + restartIcon.getWidth() + spacing - 30; + int y = marginTop + (iconHeight / 2) + 35; + + g.drawString(">", selectorX, y); + } + + @Override + public void update() { + } + + @Override + public void onDestroy() { + } +} diff --git a/src/uet/oop/bomberman/screen/GradientText.java b/src/uet/oop/bomberman/screen/GradientText.java new file mode 100644 index 0000000..6f1e493 --- /dev/null +++ b/src/uet/oop/bomberman/screen/GradientText.java @@ -0,0 +1,54 @@ +package uet.oop.bomberman.screen; + +import java.awt.*; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; + +public class GradientText { + private Font font; + private Color color1; + private Color color2; + private Color color3; + + public GradientText(Font font, Color color1, Color color2, Color color3) { + this.font = font; + this.color1 = color1; + this.color2 = color2; + this.color3 = color3; + } + + public void draw(Graphics2D g2d, String text, int x, int y) { + FontRenderContext frc = g2d.getFontRenderContext(); + GlyphVector gv = font.createGlyphVector(frc, text); + Shape outline = gv.getOutline(x, y); + + Rectangle bounds = outline.getBounds(); + float thirdWidth = bounds.width / 2f; + + GradientPaint gradientPaint1 = new GradientPaint( + bounds.x, bounds.y, color1, + bounds.x + thirdWidth, bounds.y, color2); + GradientPaint gradientPaint2 = new GradientPaint( + bounds.x + thirdWidth, bounds.y, color2, + bounds.x + 2 * thirdWidth, bounds.y, color3); + GradientPaint gradientPaint3 = new GradientPaint( + bounds.x + 2 * thirdWidth, bounds.y, color3, + bounds.x + bounds.width, bounds.y, color3); + + Shape originalClip = g2d.getClip(); + + g2d.setClip(bounds.x, bounds.y, (int) thirdWidth, bounds.height); + g2d.setPaint(gradientPaint1); + g2d.fill(outline); + + g2d.setClip(bounds.x + (int) thirdWidth, bounds.y, (int) thirdWidth, bounds.height); + g2d.setPaint(gradientPaint2); + g2d.fill(outline); + + g2d.setClip(bounds.x + 2 * (int) thirdWidth, bounds.y, (int) thirdWidth, bounds.height); + g2d.setPaint(gradientPaint3); + g2d.fill(outline); + + g2d.setClip(originalClip); + } +} diff --git a/src/uet/oop/bomberman/screen/SelectGameModeScreen.java b/src/uet/oop/bomberman/screen/SelectGameModeScreen.java new file mode 100644 index 0000000..5712767 --- /dev/null +++ b/src/uet/oop/bomberman/screen/SelectGameModeScreen.java @@ -0,0 +1,177 @@ +package uet.oop.bomberman.screen; + +import uet.oop.bomberman.Game; +import uet.oop.bomberman.gui.GameScreen; +import uet.oop.bomberman.input.Keyboard; +import uet.oop.bomberman.utils.*; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Optional; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; + +public class SelectGameModeScreen extends GameScreen { + ArrayList gameModes = new ArrayList(); + int selectorIndex = 0; + private Optional _input; + private BufferedImage backgroundImage; + private int OFFSET = 40; + private BufferedImage pointerImage; + + public SelectGameModeScreen() { + gameModes.add(EGameMode.ONE_PLAYER.getStringLevel()); + gameModes.add(EGameMode.TWO_PLAYER.getStringLevel()); + + try { + backgroundImage = ImageIO.read(getClass().getResource("/menu/bgBombman.png")); + pointerImage = ImageIO.read(getClass().getResource("/menu/pointer.png")); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void setInput(Keyboard input) { + _input = Optional.ofNullable(input); + + _input.get().keyboardInputCallback = Optional.of(new Keyboard.KeyboardInputCallback() { + @Override + public void onKeyPressed(EGameControl gameControl) { + switch (gameControl) { + case UP: + selectorIndex--; + break; + case DOWN: + selectorIndex++; + break; + case ENTER: + Global.currentScreen = EScreenName.SELECT_LEVEL_SCREEN; + if (selectorIndex == 1) { + Global.gameMode = EGameMode.TWO_PLAYER; + } else { + Global.gameMode = EGameMode.ONE_PLAYER; + } + break; + } + + if (selectorIndex < 0) { + selectorIndex = gameModes.size() - 1; + } else if (selectorIndex > gameModes.size() - 1) { + selectorIndex = 0; + } + } + }); + } + + @Override + public void drawScreen(Graphics g) { + // set background + if (backgroundImage != null) { + g.drawImage(backgroundImage, 0, 0, Global.screenWidth, Global.screenHeight, null); + } else { + g.setColor(Color.BLACK); + g.fillRect(0, 0, Global.screenWidth, Global.screenHeight); + } + + drawTitle(g, "SELECT GAME MODE"); + drawOptions(g); + drawSelector(g); + } + + private void drawTitle(Graphics g, String title) { + Graphics2D g2d = (Graphics2D) g; + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + Font font = new Font("Minecraft", Font.BOLD, 20 * Game.SCALE); + + // Create gradient colors + Color color1 = Color.RED; + Color color2 = Color.ORANGE; + Color color3 = Color.YELLOW; + + // Create GradientText object + GradientText gradientText = new GradientText(font, color1, color2, color3); + + // Calculate the position for the title + int x = (Global.screenWidth - g.getFontMetrics().stringWidth(title)) / 15; + int y = 190; + + // Draw the gradient text + gradientText.draw((Graphics2D) g, title, (Global.screenWidth - g.getFontMetrics().stringWidth(title)) / 15, + 190); + + // Create outline for the text + FontRenderContext frc = g2d.getFontRenderContext(); + GlyphVector gv = font.createGlyphVector(frc, title); + Shape outline = gv.getOutline(x, y); + + // Draw the outline + g2d.setColor(Color.BLACK); + g2d.draw(outline); + } + + private void drawOptions(Graphics g) { + Font font = new Font("Minecraft", Font.PLAIN, 10 * Game.SCALE); + g.setFont(font); + + // Position of Options + int w = Global.screenWidth; + int h = Global.screenHeight; + FontMetrics fm = g.getFontMetrics(); + int textHeight = fm.getAscent() + fm.getDescent(); + + // Khoảng cách giữa các lựa chọn + int spacing = 10 * Game.SCALE; + + // Tính toán tổng chiều cao của các lựa chọn cộng với khoảng cách giữa chúng + int boxHeight = (textHeight) * this.gameModes.size() - spacing; + int marginTop = (h - boxHeight) / 2; + + for (int i = 0; i < gameModes.size(); i++) { + String mode = gameModes.get(i); + + int x = (w - fm.stringWidth(mode)) / 2; + int y = marginTop + fm.getAscent() + (textHeight + spacing) * i; + + if (i == selectorIndex) { + g.setColor(Color.YELLOW); + } else { + g.setColor(Color.WHITE); + } + + g.drawString(mode, x, y); + } + } + + private void drawSelector(Graphics g) { + Font font = new Font("Minecraft", Font.PLAIN, 12 * Game.SCALE); + g.setFont(font); + g.setColor(Color.white); + + String level = this.gameModes.get(selectorIndex); + int w = Global.screenWidth; + int h = Global.screenHeight; + FontMetrics fm = g.getFontMetrics(); + int textHeight = fm.getAscent() + fm.getDescent(); + int boxHeight = textHeight * this.gameModes.size(); + int marginTop = (h - boxHeight) / 2 + 15; + + int spacing = 10 * Game.SCALE; + int x = (w - fm.stringWidth(level)) / 2 - 50; + int y = marginTop + fm.getAscent() + (textHeight + spacing) * selectorIndex; + + g.drawImage(pointerImage, x, y - fm.getAscent(), null); + } + + @Override + public void update() { + + } + + @Override + public void onDestroy() { + this._input.get().keyboardInputCallback = Optional.ofNullable(null); + } +} diff --git a/src/uet/oop/bomberman/screen/SelectLevelScreen.java b/src/uet/oop/bomberman/screen/SelectLevelScreen.java new file mode 100644 index 0000000..475e95d --- /dev/null +++ b/src/uet/oop/bomberman/screen/SelectLevelScreen.java @@ -0,0 +1,191 @@ +package uet.oop.bomberman.screen; + +import uet.oop.bomberman.Board; +import uet.oop.bomberman.Game; +import uet.oop.bomberman.gui.Frame; +import uet.oop.bomberman.gui.GameScreen; +import uet.oop.bomberman.input.Keyboard; + +import uet.oop.bomberman.utils.*; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Optional; + +public class SelectLevelScreen extends GameScreen { + ArrayList levels = new ArrayList(); + int selectorIndex = 0; + private Optional _input; + private Board _board; + private Frame _frame; + private BufferedImage backgroundImage; + private BufferedImage pointerImage; + + public SelectLevelScreen(Board board, Frame frame) { + _board = board; + _frame = frame; + + levels.add(EGameLevel.EASY.getStringLevel()); + levels.add(EGameLevel.MEDIUM.getStringLevel()); + levels.add(EGameLevel.HARD.getStringLevel()); + levels.add(EGameLevel.BACK.getStringLevel()); + + try { + backgroundImage = ImageIO.read(getClass().getResource("/menu/bgBombman.png")); + pointerImage = ImageIO.read(getClass().getResource("/menu/pointer.png")); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void setInput(Keyboard input) { + _input = Optional.ofNullable(input); + + _input.get().keyboardInputCallback = java.util.Optional.of(new Keyboard.KeyboardInputCallback() { + @Override + public void onKeyPressed(EGameControl gameControl) { + switch (gameControl) { + case UP: + selectorIndex--; + break; + case DOWN: + selectorIndex++; + break; + case ENTER: + if (selectorIndex == levels.size() - 1) { + Global.currentScreen = EScreenName.SELECT_GAME_MODE; + } else { + Global.gameLevel = selectorIndex + 1; + _board.getLevelManager().loadGlobalLevel(); + Global.currentScreen = EScreenName.GAME_PLAY_SCREEN; + } + _frame.loadInfo(); + onDestroy(); + break; + case BACK: + Global.currentScreen = EScreenName.SELECT_GAME_MODE; + _frame.loadInfo(); + onDestroy(); + break; + default: + break; + } + + if (selectorIndex < 0) { + selectorIndex = levels.size() - 1; + } else if (selectorIndex > levels.size() - 1) { + selectorIndex = 0; + } + } + }); + } + + @Override + public void drawScreen(Graphics g) { + // set background + if (backgroundImage != null) { + g.drawImage(backgroundImage, 0, 0, Global.screenWidth, Global.screenHeight, null); + } else { + g.setColor(Color.BLACK); + g.fillRect(0, 0, Global.screenWidth, Global.screenHeight); + } + + drawTitle(g, "SELECT LEVEL"); + drawOptions(g); + drawSelector(g); + } + + private void drawTitle(Graphics g, String title) { + Graphics2D g2d = (Graphics2D) g; + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + Font font = new Font("Minecraft", Font.BOLD, 20 * Game.SCALE); + Color color1 = Color.RED; + Color color2 = Color.ORANGE; + Color color3 = Color.YELLOW; + + // Calculate the position for the title + int x = (Global.screenWidth - g.getFontMetrics().stringWidth(title)) / 5; + int y = 120; + + GradientText gradientText = new GradientText(font, color1, color2, color3); + gradientText.draw((Graphics2D) g, title, (Global.screenWidth - g.getFontMetrics().stringWidth(title)) / 5, + 120); + + // Create outline for the text + FontRenderContext frc = g2d.getFontRenderContext(); + GlyphVector gv = font.createGlyphVector(frc, title); + Shape outline = gv.getOutline(x, y); + + // Draw the outline + g2d.setColor(Color.BLACK); + g2d.draw(outline); + } + + private void drawOptions(Graphics g) { + Font font = new Font("Minecraft", Font.PLAIN, 10 * Game.SCALE); + g.setFont(font); + + // Position of Options + int w = Global.screenWidth; + int h = Global.screenHeight; + FontMetrics fm = g.getFontMetrics(); + int textHeight = fm.getAscent() + fm.getDescent(); + + // Khoảng cách giữa các lựa chọn + int spacing = 10 * Game.SCALE; + + // Tính toán tổng chiều cao của các lựa chọn cộng với khoảng cách giữa chúng + int boxHeight = (textHeight) * this.levels.size() - spacing; + int marginTop = (h - boxHeight) / 2; + + for (int i = 0; i < levels.size(); i++) { + String mode = levels.get(i); + + int x = (w - fm.stringWidth(mode)) / 2; + int y = marginTop + (textHeight + spacing) * i - 55; + + if (i == selectorIndex) { + g.setColor(Color.YELLOW); + } else { + g.setColor(Color.WHITE); + } + + g.drawString(mode, x, y); + } + } + + private void drawSelector(Graphics g) { + Font font = new Font("Minecraft", Font.PLAIN, 12 * Game.SCALE); + g.setFont(font); + g.setColor(Color.WHITE); + + String level = this.levels.get(selectorIndex); + int w = Global.screenWidth; + int h = Global.screenHeight; + FontMetrics fm = g.getFontMetrics(); + int textHeight = fm.getAscent() + fm.getDescent(); + int boxHeight = textHeight * this.levels.size(); + int marginTop = (h - boxHeight) / 2; + + int spacing = 9 * Game.SCALE; + int x = (w - fm.stringWidth(level)) / 2 - 50; // Đặt vị trí mũi tên ở bên trái văn bản + int y = marginTop + fm.getAscent() + (textHeight + spacing) * selectorIndex - 60; + + g.drawImage(pointerImage, x, y - fm.getAscent(), null); + } + + @Override + public void update() { + } + + @Override + public void onDestroy() { + this._input.get().keyboardInputCallback = Optional.ofNullable(null); + ; + } +} diff --git a/src/uet/oop/bomberman/sound/Sound.java b/src/uet/oop/bomberman/sound/Sound.java index efafdc1..b231175 100644 --- a/src/uet/oop/bomberman/sound/Sound.java +++ b/src/uet/oop/bomberman/sound/Sound.java @@ -18,13 +18,14 @@ public void run() { clip.open(inputStream); clip.start(); } catch (Exception e) { - System.err.println(e.getMessage()); + System.err.println("Failed playing sound '" + sound + "': " + e.getMessage()); } } }).start(); - + } - public static void stop(String sound){ + + public static void stop(String sound) { new Thread(new Runnable() { public void run() { try { @@ -34,7 +35,7 @@ public void run() { clip.open(inputStream); clip.stop(); } catch (Exception e) { - System.err.println(e.getMessage()); + System.err.println("Failed stopping sound '" + sound + "': " + e.getMessage()); } } }).start(); diff --git a/src/uet/oop/bomberman/utils/EGameControl.java b/src/uet/oop/bomberman/utils/EGameControl.java new file mode 100644 index 0000000..c2e51b7 --- /dev/null +++ b/src/uet/oop/bomberman/utils/EGameControl.java @@ -0,0 +1,13 @@ +package uet.oop.bomberman.utils; + +public enum EGameControl { + NONE, + UP, + DOWN, + LEFT, + RIGHT, + ENTER, + SPACE, + X, + BACK, +} diff --git a/src/uet/oop/bomberman/utils/EGameLevel.java b/src/uet/oop/bomberman/utils/EGameLevel.java new file mode 100644 index 0000000..774465e --- /dev/null +++ b/src/uet/oop/bomberman/utils/EGameLevel.java @@ -0,0 +1,18 @@ +package uet.oop.bomberman.utils; + +public enum EGameLevel { + EASY("EASY"), + MEDIUM("MEDIUM"), + HARD("HARD"), + BACK("BACK (B)"); + + private final String level; + + EGameLevel(String level) { + this.level = level; + } + + public String getStringLevel() { + return this.level; + } +} diff --git a/src/uet/oop/bomberman/utils/EGameMode.java b/src/uet/oop/bomberman/utils/EGameMode.java new file mode 100644 index 0000000..ac77373 --- /dev/null +++ b/src/uet/oop/bomberman/utils/EGameMode.java @@ -0,0 +1,16 @@ +package uet.oop.bomberman.utils; + +public enum EGameMode { + ONE_PLAYER("1 PLAYER"), + TWO_PLAYER("2 PLAYER"); + + private final String mode; + + EGameMode(String level) { + this.mode = level; + } + + public String getStringLevel() { + return this.mode; + } +} diff --git a/src/uet/oop/bomberman/utils/EScreenName.java b/src/uet/oop/bomberman/utils/EScreenName.java new file mode 100644 index 0000000..9410891 --- /dev/null +++ b/src/uet/oop/bomberman/utils/EScreenName.java @@ -0,0 +1,8 @@ +package uet.oop.bomberman.utils; + +public enum EScreenName { + SELECT_GAME_MODE, + SELECT_LEVEL_SCREEN, + GAME_PLAY_SCREEN, + END_GAME_SCREEN, +} diff --git a/src/uet/oop/bomberman/utils/Global.java b/src/uet/oop/bomberman/utils/Global.java new file mode 100644 index 0000000..46dbabe --- /dev/null +++ b/src/uet/oop/bomberman/utils/Global.java @@ -0,0 +1,17 @@ +package uet.oop.bomberman.utils; + +public class Global { + public static int screenWidth; + public static int screenHeight; + + public static EScreenName currentScreen = EScreenName.SELECT_GAME_MODE; + public static EScreenName previousScreen = EScreenName.GAME_PLAY_SCREEN; + + // GAME PLAY + public static int gameLevel = 1; + public static EGameMode gameMode = EGameMode.ONE_PLAYER; + public static int enemies = 0; + + public static boolean isAIPlayer = false; + public static boolean isRuleBasedPlayer = false; +}