diff --git a/README.md b/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/__pycache__/duckhunt.cpython-312.pyc b/__pycache__/duckhunt.cpython-312.pyc new file mode 100644 index 0000000..0008df4 Binary files /dev/null and b/__pycache__/duckhunt.cpython-312.pyc differ diff --git a/duckhunt/simple_duckhunt.py b/backup/simple_duckhunt.py similarity index 100% rename from duckhunt/simple_duckhunt.py rename to backup/simple_duckhunt.py diff --git a/duckhunt/config.json b/config.json similarity index 98% rename from duckhunt/config.json rename to config.json index 4e97991..0c74b5f 100644 --- a/duckhunt/config.json +++ b/config.json @@ -1,11 +1,11 @@ { "server": "irc.rizon.net", "port": 6697, - "nick": "DuckHunt", - "channels": ["#computertech"], + "nick": "DickHunt", + "channels": ["#ct"], "ssl": true, "sasl": { - "enabled": true, + "enabled": false, "username": "duckhunt", "password": "duckhunt//789//" }, diff --git a/duckhunt/duckhunt.json b/duckhunt.json similarity index 91% rename from duckhunt/duckhunt.json rename to duckhunt.json index dd16754..b34f2ee 100644 --- a/duckhunt/duckhunt.json +++ b/duckhunt.json @@ -22,7 +22,7 @@ "caught": 12, "ammo": 5, "max_ammo": 10, - "chargers": 0, + "chargers": 2, "max_chargers": 2, "xp": 100, "accuracy": 65, @@ -31,22 +31,37 @@ "gun_level": 1, "luck": 0, "gun_type": "pistol", - "gun_confiscated": false, + "gun_confiscated": true, "jammed": false, "jammed_count": 1, - "total_ammo_used": 12, + "total_ammo_used": 18, "shot_at": 9, "reflex_shots": 6, "total_reflex_time": 47.87793278694153, "best_time": 2.6269078254699707, "karma": 1, - "wild_shots": 4, + "wild_shots": 10, "befriended": 6, "missed": 1, "inventory": { "15": 1 }, - "sand": 1 + "sand": 1, + "shots": 9, + "max_shots": 10, + "reload_time": 5.0, + "ducks_shot": 12, + "ducks_befriended": 6, + "accuracy_bonus": 0, + "xp_bonus": 0, + "charm_bonus": 0, + "exp": 100, + "money": 119, + "last_hunt": 0, + "last_reload": 0, + "level": 1, + "ignored_users": [], + "confiscated_count": 2 }, "colby_": { "xp": 0, @@ -1014,7 +1029,77 @@ "bread": 0, "duck_detector": 0, "mechanical": 0 + }, + "py-ctcp": { + "xp": 0, + "caught": 0, + "befriended": 0, + "missed": 0, + "ammo": 6, + "max_ammo": 6, + "chargers": 2, + "max_chargers": 2, + "accuracy": 65, + "reliability": 70, + "weapon": "pistol", + "gun_confiscated": false, + "explosive_ammo": false, + "settings": { + "output_mode": "PUBLIC", + "notices": false, + "private_messages": false + }, + "inventory": {}, + "golden_ducks": 0, + "karma": 0, + "deflection": 0, + "defense": 0, + "jammed": false, + "jammed_count": 0, + "deaths": 0, + "neutralized": 0, + "deflected": 0, + "best_time": 999.9, + "total_reflex_time": 0.0, + "reflex_shots": 0, + "wild_shots": 0, + "accidents": 0, + "total_ammo_used": 0, + "shot_at": 0, + "lucky_shots": 0, + "luck": 0, + "detector": 0, + "silencer": 0, + "sunglasses": 0, + "clothes": 0, + "grease": 0, + "brush": 0, + "mirror": 0, + "sand": 0, + "water": 0, + "sabotage": 0, + "life_insurance": 0, + "liability": 0, + "decoy": 0, + "bread": 0, + "duck_detector": 0, + "mechanical": 0, + "shots": 6, + "max_shots": 6, + "reload_time": 5.0, + "ducks_shot": 0, + "ducks_befriended": 0, + "accuracy_bonus": 0, + "xp_bonus": 0, + "charm_bonus": 0, + "exp": 0, + "money": 100, + "last_hunt": 0, + "last_reload": 0, + "level": 1, + "ignored_users": [], + "confiscated_count": 0 } }, - "last_save": "1757771132.8947892" + "last_save": "1758314578.1957033" } \ No newline at end of file diff --git a/duckhunt.log b/duckhunt.log new file mode 100644 index 0000000..8037e9f --- /dev/null +++ b/duckhunt.log @@ -0,0 +1,198 @@ +2025-09-19 21:04:25,978 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:04:25,978 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:04:25,978 [INFO ] DuckHuntBot - main:26: 🦆 Starting DuckHunt Bot... +2025-09-19 21:04:25,980 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 21 players from duckhunt.json +2025-09-19 21:04:32,607 [INFO ] DuckHuntBot - signal_handler:188: Received signal 2, shutting down... +2025-09-19 21:04:33,747 [INFO ] DuckHuntBot - signal_handler:188: Received signal 2, shutting down... +2025-09-19 21:05:02,022 [INFO ] DuckHuntBot - connect:92: Connected to irc.rizon.net:6697 +2025-09-19 21:05:02,086 [ERROR ] DuckHuntBot - run:898: Bot error: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-19 21:06:16,216 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:06:16,217 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:06:16,221 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 21 players from duckhunt.json +2025-09-19 21:06:17,222 [INFO ] DuckHuntBot - signal_handler:188: Received signal 2, shutting down... +2025-09-19 21:06:17,223 [INFO ] DuckHuntBot - run:908: Shutting down bot... +2025-09-19 21:08:34,809 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:08:34,810 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:08:34,817 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 21 players from duckhunt.json +2025-09-19 21:08:34,977 [INFO ] DuckHuntBot - connect:92: Connected to irc.libera.chat:6667 +2025-09-19 21:08:35,507 [INFO ] DuckHuntBot - handle_message:205: Successfully registered with IRC server +2025-09-19 21:08:35,815 [INFO ] DuckHuntBot - signal_handler:188: Received signal 2, shutting down... +2025-09-19 21:08:35,816 [INFO ] DuckHuntBot - message_loop:960: Message loop cancelled +2025-09-19 21:08:35,817 [INFO ] DuckHuntBot - message_loop:966: Message loop ended +2025-09-19 21:08:35,818 [INFO ] DuckHuntBot.Game - duck_timeout_checker:295: Duck timeout checker cancelled +2025-09-19 21:08:35,819 [INFO ] DuckHuntBot - run:899: Main loop cancelled +2025-09-19 21:08:35,821 [INFO ] DuckHuntBot - run:909: Shutting down bot... +2025-09-19 21:08:35,822 [INFO ] DuckHuntBot.Game - spawn_ducks:250: Duck spawning loop cancelled +2025-09-19 21:08:35,830 [INFO ] DuckHuntBot - run:925: Database saved +2025-09-19 21:08:35,831 [INFO ] DuckHuntBot - run:935: IRC connection closed +2025-09-19 21:08:35,831 [INFO ] DuckHuntBot - run:939: Bot shutdown complete +2025-09-19 21:08:44,430 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:08:44,431 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:08:44,431 [INFO ] DuckHuntBot - main:26: 🦆 Starting DuckHunt Bot... +2025-09-19 21:08:44,432 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 21 players from duckhunt.json +2025-09-19 21:08:44,740 [INFO ] DuckHuntBot - connect:92: Connected to irc.rizon.net:6697 +2025-09-19 21:08:49,547 [INFO ] DuckHuntBot - signal_handler:188: Received signal 2, shutting down... +2025-09-19 21:08:49,558 [INFO ] DuckHuntBot - run:899: Main loop cancelled +2025-09-19 21:08:49,559 [INFO ] DuckHuntBot - run:909: Shutting down bot... +2025-09-19 21:08:49,559 [INFO ] DuckHuntBot - message_loop:960: Message loop cancelled +2025-09-19 21:08:49,560 [INFO ] DuckHuntBot - message_loop:966: Message loop ended +2025-09-19 21:08:49,560 [INFO ] DuckHuntBot.Game - spawn_ducks:250: Duck spawning loop cancelled +2025-09-19 21:08:49,560 [INFO ] DuckHuntBot.Game - duck_timeout_checker:295: Duck timeout checker cancelled +2025-09-19 21:08:49,563 [INFO ] DuckHuntBot - run:925: Database saved +2025-09-19 21:08:49,655 [ERROR ] DuckHuntBot - run:937: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-19 21:08:49,655 [INFO ] DuckHuntBot - run:939: Bot shutdown complete +2025-09-19 21:09:05,620 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:09:05,621 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:09:05,621 [INFO ] DuckHuntBot - main:26: 🦆 Starting DuckHunt Bot... +2025-09-19 21:09:05,622 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 21 players from duckhunt.json +2025-09-19 21:09:05,871 [INFO ] DuckHuntBot - connect:92: Connected to irc.rizon.net:6697 +2025-09-19 21:09:06,058 [INFO ] DuckHuntBot - handle_message:205: Successfully registered with IRC server +2025-09-19 21:09:09,738 [ERROR ] DuckHuntBot - handle_command:287: Error in command handler: 'shots' +2025-09-19 21:09:17,085 [INFO ] DuckHuntBot.Game - spawn_duck_now:203: Spawned normal duck in #ct +2025-09-19 21:09:17,085 [DEBUG ] DuckHuntBot.Game - send_duck_alerts:217: Duck alerts for normal duck in #ct +2025-09-19 21:09:22,905 [INFO ] DuckHuntBot.Game - spawn_duck_now:203: Spawned rare duck in #ct +2025-09-19 21:09:22,906 [DEBUG ] DuckHuntBot.Game - send_duck_alerts:217: Duck alerts for rare duck in #ct +2025-09-19 21:09:24,398 [INFO ] DuckHuntBot.Game - spawn_duck_now:203: Spawned rare duck in #ct +2025-09-19 21:09:24,398 [DEBUG ] DuckHuntBot.Game - send_duck_alerts:217: Duck alerts for rare duck in #ct +2025-09-19 21:09:25,076 [DEBUG ] DuckHuntBot.Game - spawn_duck_now:161: Max ducks already in #ct +2025-09-19 21:10:05,877 [DEBUG ] DuckHuntBot.Game - duck_timeout_checker:290: Duck timed out in #ct +2025-09-19 21:10:35,879 [DEBUG ] DuckHuntBot.Game - duck_timeout_checker:290: Duck timed out in #ct +2025-09-19 21:10:35,880 [DEBUG ] DuckHuntBot.Game - duck_timeout_checker:290: Duck timed out in #ct +2025-09-19 21:10:53,732 [INFO ] DuckHuntBot - signal_handler:188: Received signal 2, shutting down... +2025-09-19 21:10:53,764 [INFO ] DuckHuntBot - run:899: Main loop cancelled +2025-09-19 21:10:53,768 [INFO ] DuckHuntBot - run:909: Shutting down bot... +2025-09-19 21:10:53,768 [INFO ] DuckHuntBot - message_loop:960: Message loop cancelled +2025-09-19 21:10:53,770 [INFO ] DuckHuntBot - message_loop:966: Message loop ended +2025-09-19 21:10:53,771 [INFO ] DuckHuntBot.Game - spawn_ducks:250: Duck spawning loop cancelled +2025-09-19 21:10:53,775 [INFO ] DuckHuntBot.Game - duck_timeout_checker:295: Duck timeout checker cancelled +2025-09-19 21:10:53,789 [INFO ] DuckHuntBot - run:925: Database saved +2025-09-19 21:10:53,853 [ERROR ] DuckHuntBot - run:937: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-19 21:10:53,854 [INFO ] DuckHuntBot - run:939: Bot shutdown complete +2025-09-19 21:12:51,877 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:12:51,879 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:12:51,881 [INFO ] DuckHuntBot - main:26: 🦆 Starting DuckHunt Bot... +2025-09-19 21:12:51,887 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 22 players from duckhunt.json +2025-09-19 21:12:52,184 [INFO ] DuckHuntBot - connect:92: Connected to irc.rizon.net:6697 +2025-09-19 21:12:52,366 [INFO ] DuckHuntBot - handle_message:205: Successfully registered with IRC server +2025-09-19 21:13:21,643 [INFO ] DuckHuntBot - signal_handler:188: Received signal 15, shutting down... +2025-09-19 21:13:21,681 [INFO ] DuckHuntBot - run:899: Main loop cancelled +2025-09-19 21:13:21,682 [INFO ] DuckHuntBot - run:909: Shutting down bot... +2025-09-19 21:13:21,684 [INFO ] DuckHuntBot - message_loop:960: Message loop cancelled +2025-09-19 21:13:21,685 [INFO ] DuckHuntBot - message_loop:966: Message loop ended +2025-09-19 21:13:21,686 [INFO ] DuckHuntBot.Game - spawn_ducks:250: Duck spawning loop cancelled +2025-09-19 21:13:21,686 [INFO ] DuckHuntBot.Game - duck_timeout_checker:295: Duck timeout checker cancelled +2025-09-19 21:13:21,690 [INFO ] DuckHuntBot - run:925: Database saved +2025-09-19 21:13:21,752 [ERROR ] DuckHuntBot - run:937: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-19 21:13:21,753 [INFO ] DuckHuntBot - run:939: Bot shutdown complete +2025-09-19 21:13:27,996 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:13:27,997 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:13:27,997 [INFO ] DuckHuntBot - main:26: 🦆 Starting DuckHunt Bot... +2025-09-19 21:13:27,999 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 22 players from duckhunt.json +2025-09-19 21:13:28,246 [INFO ] DuckHuntBot - connect:92: Connected to irc.rizon.net:6697 +2025-09-19 21:13:28,721 [INFO ] DuckHuntBot - handle_message:205: Successfully registered with IRC server +2025-09-19 21:18:41,831 [INFO ] DuckHuntBot - signal_handler:188: Received signal 2, shutting down... +2025-09-19 21:18:41,878 [INFO ] DuckHuntBot - run:899: Main loop cancelled +2025-09-19 21:18:41,879 [INFO ] DuckHuntBot - run:909: Shutting down bot... +2025-09-19 21:18:41,879 [INFO ] DuckHuntBot - message_loop:960: Message loop cancelled +2025-09-19 21:18:41,880 [INFO ] DuckHuntBot - message_loop:966: Message loop ended +2025-09-19 21:18:41,881 [INFO ] DuckHuntBot.Game - spawn_ducks:250: Duck spawning loop cancelled +2025-09-19 21:18:41,881 [INFO ] DuckHuntBot.Game - duck_timeout_checker:295: Duck timeout checker cancelled +2025-09-19 21:18:41,902 [INFO ] DuckHuntBot - run:925: Database saved +2025-09-19 21:18:41,987 [ERROR ] DuckHuntBot - run:937: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-19 21:18:41,988 [INFO ] DuckHuntBot - run:939: Bot shutdown complete +2025-09-19 21:19:02,499 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:19:02,500 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:19:02,501 [INFO ] DuckHuntBot - main:26: 🦆 Starting DuckHunt Bot... +2025-09-19 21:19:02,504 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 22 players from duckhunt.json +2025-09-19 21:19:02,778 [INFO ] DuckHuntBot - connect:92: Connected to irc.rizon.net:6697 +2025-09-19 21:19:03,277 [INFO ] DuckHuntBot - handle_message:205: Successfully registered with IRC server +2025-09-19 21:19:12,233 [INFO ] DuckHuntBot - signal_handler:188: Received signal 15, shutting down... +2025-09-19 21:19:12,323 [INFO ] DuckHuntBot - run:1012: Main loop cancelled +2025-09-19 21:19:12,324 [INFO ] DuckHuntBot - run:1022: Shutting down bot... +2025-09-19 21:19:12,324 [INFO ] DuckHuntBot - message_loop:1073: Message loop cancelled +2025-09-19 21:19:12,324 [INFO ] DuckHuntBot - message_loop:1079: Message loop ended +2025-09-19 21:19:12,324 [INFO ] DuckHuntBot.Game - duck_timeout_checker:306: Duck timeout checker cancelled +2025-09-19 21:19:12,324 [INFO ] DuckHuntBot.Game - spawn_ducks:261: Duck spawning loop cancelled +2025-09-19 21:19:12,327 [INFO ] DuckHuntBot - run:1038: Database saved +2025-09-19 21:19:12,383 [ERROR ] DuckHuntBot - run:1050: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-19 21:19:12,383 [INFO ] DuckHuntBot - run:1052: Bot shutdown complete +2025-09-19 21:19:43,902 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:19:43,903 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:19:43,903 [INFO ] DuckHuntBot - main:26: 🦆 Starting DuckHunt Bot... +2025-09-19 21:19:43,904 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 22 players from duckhunt.json +2025-09-19 21:19:44,156 [INFO ] DuckHuntBot - connect:92: Connected to irc.rizon.net:6697 +2025-09-19 21:19:44,322 [INFO ] DuckHuntBot - handle_message:205: Successfully registered with IRC server +2025-09-19 21:19:56,638 [INFO ] DuckHuntBot - signal_handler:188: Received signal 2, shutting down... +2025-09-19 21:19:56,731 [INFO ] DuckHuntBot - run:1012: Main loop cancelled +2025-09-19 21:19:56,731 [INFO ] DuckHuntBot - run:1022: Shutting down bot... +2025-09-19 21:19:56,732 [INFO ] DuckHuntBot - message_loop:1073: Message loop cancelled +2025-09-19 21:19:56,732 [INFO ] DuckHuntBot - message_loop:1079: Message loop ended +2025-09-19 21:19:56,733 [INFO ] DuckHuntBot.Game - spawn_ducks:261: Duck spawning loop cancelled +2025-09-19 21:19:56,733 [INFO ] DuckHuntBot.Game - duck_timeout_checker:306: Duck timeout checker cancelled +2025-09-19 21:19:56,738 [INFO ] DuckHuntBot - run:1038: Database saved +2025-09-19 21:19:56,794 [ERROR ] DuckHuntBot - run:1050: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-19 21:19:56,795 [INFO ] DuckHuntBot - run:1052: Bot shutdown complete +2025-09-19 21:21:50,220 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:21:50,221 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:21:50,221 [INFO ] DuckHuntBot - main:26: 🦆 Starting DuckHunt Bot... +2025-09-19 21:21:50,224 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 22 players from duckhunt.json +2025-09-19 21:21:50,526 [INFO ] DuckHuntBot - connect:92: Connected to irc.rizon.net:6697 +2025-09-19 21:21:52,130 [INFO ] DuckHuntBot - handle_message:205: Successfully registered with IRC server +2025-09-19 21:26:33,004 [INFO ] DuckHuntBot - signal_handler:188: Received signal 2, shutting down... +2025-09-19 21:26:33,069 [INFO ] DuckHuntBot - run:1012: Main loop cancelled +2025-09-19 21:26:33,070 [INFO ] DuckHuntBot - run:1022: Shutting down bot... +2025-09-19 21:26:33,070 [INFO ] DuckHuntBot - message_loop:1073: Message loop cancelled +2025-09-19 21:26:33,071 [INFO ] DuckHuntBot - message_loop:1079: Message loop ended +2025-09-19 21:26:33,074 [INFO ] DuckHuntBot.Game - spawn_ducks:261: Duck spawning loop cancelled +2025-09-19 21:26:33,075 [INFO ] DuckHuntBot.Game - duck_timeout_checker:306: Duck timeout checker cancelled +2025-09-19 21:26:33,097 [INFO ] DuckHuntBot - run:1038: Database saved +2025-09-19 21:26:33,184 [ERROR ] DuckHuntBot - run:1050: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-19 21:26:33,185 [INFO ] DuckHuntBot - run:1052: Bot shutdown complete +2025-09-19 21:27:12,029 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:27:12,030 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:27:12,030 [INFO ] DuckHuntBot - main:26: 🦆 Starting DuckHunt Bot... +2025-09-19 21:27:12,033 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 22 players from duckhunt.json +2025-09-19 21:27:12,302 [INFO ] DuckHuntBot - connect:92: Connected to irc.rizon.net:6697 +2025-09-19 21:27:12,685 [INFO ] DuckHuntBot - handle_message:205: Successfully registered with IRC server +2025-09-19 21:27:21,684 [INFO ] DuckHuntBot - signal_handler:188: Received signal 15, shutting down... +2025-09-19 21:27:21,734 [INFO ] DuckHuntBot - run:1000: Main loop cancelled +2025-09-19 21:27:21,734 [INFO ] DuckHuntBot - run:1010: Shutting down bot... +2025-09-19 21:27:21,735 [INFO ] DuckHuntBot.Game - duck_timeout_checker:307: Duck timeout checker cancelled +2025-09-19 21:27:21,735 [INFO ] DuckHuntBot - message_loop:1061: Message loop cancelled +2025-09-19 21:27:21,735 [INFO ] DuckHuntBot - message_loop:1067: Message loop ended +2025-09-19 21:27:21,735 [INFO ] DuckHuntBot.Game - spawn_ducks:261: Duck spawning loop cancelled +2025-09-19 21:27:21,743 [INFO ] DuckHuntBot - run:1026: Database saved +2025-09-19 21:27:21,799 [ERROR ] DuckHuntBot - run:1038: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-19 21:27:21,800 [INFO ] DuckHuntBot - run:1040: Bot shutdown complete +2025-09-19 21:27:36,083 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:27:36,084 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:27:36,084 [INFO ] DuckHuntBot - main:26: 🦆 Starting DuckHunt Bot... +2025-09-19 21:27:36,086 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 22 players from duckhunt.json +2025-09-19 21:27:36,338 [INFO ] DuckHuntBot - connect:92: Connected to irc.rizon.net:6697 +2025-09-19 21:27:36,504 [INFO ] DuckHuntBot - handle_message:205: Successfully registered with IRC server +2025-09-19 21:30:40,213 [INFO ] DuckHuntBot - signal_handler:188: Received signal 2, shutting down... +2025-09-19 21:30:40,256 [INFO ] DuckHuntBot - run:1000: Main loop cancelled +2025-09-19 21:30:40,257 [INFO ] DuckHuntBot - run:1010: Shutting down bot... +2025-09-19 21:30:40,257 [INFO ] DuckHuntBot - message_loop:1061: Message loop cancelled +2025-09-19 21:30:40,257 [INFO ] DuckHuntBot - message_loop:1067: Message loop ended +2025-09-19 21:30:40,257 [INFO ] DuckHuntBot.Game - spawn_ducks:261: Duck spawning loop cancelled +2025-09-19 21:30:40,257 [INFO ] DuckHuntBot.Game - duck_timeout_checker:307: Duck timeout checker cancelled +2025-09-19 21:30:40,262 [INFO ] DuckHuntBot - run:1026: Database saved +2025-09-19 21:30:40,318 [ERROR ] DuckHuntBot - run:1038: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-19 21:30:40,319 [INFO ] DuckHuntBot - run:1040: Bot shutdown complete +2025-09-19 21:42:07,003 [INFO ] DuckHuntBot - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:42:07,005 [INFO ] SASL - setup_logger:65: Enhanced logging system initialized with file rotation +2025-09-19 21:42:07,005 [INFO ] DuckHuntBot - main:26: 🦆 Starting DuckHunt Bot... +2025-09-19 21:42:07,009 [INFO ] DuckHuntBot.DB - load_database:40: Loaded 22 players from duckhunt.json +2025-09-19 21:42:07,297 [INFO ] DuckHuntBot - connect:95: Connected to irc.rizon.net:6697 +2025-09-19 21:42:07,739 [INFO ] DuckHuntBot - handle_message:208: Successfully registered with IRC server +2025-09-19 21:42:58,096 [INFO ] DuckHuntBot - signal_handler:191: Received signal 2, shutting down... +2025-09-19 21:42:58,194 [INFO ] DuckHuntBot.Game - spawn_ducks:261: Duck spawning loop cancelled +2025-09-19 21:42:58,194 [INFO ] DuckHuntBot - run:1128: Main loop cancelled +2025-09-19 21:42:58,194 [INFO ] DuckHuntBot - run:1138: Shutting down bot... +2025-09-19 21:42:58,195 [INFO ] DuckHuntBot.Game - duck_timeout_checker:307: Duck timeout checker cancelled +2025-09-19 21:42:58,195 [INFO ] DuckHuntBot - message_loop:1189: Message loop cancelled +2025-09-19 21:42:58,195 [INFO ] DuckHuntBot - message_loop:1195: Message loop ended +2025-09-19 21:42:58,198 [INFO ] DuckHuntBot - run:1154: Database saved +2025-09-19 21:42:58,261 [ERROR ] DuckHuntBot - run:1166: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-19 21:42:58,262 [INFO ] DuckHuntBot - run:1168: Bot shutdown complete diff --git a/duckhunt/duckhunt.py b/duckhunt.py similarity index 76% rename from duckhunt/duckhunt.py rename to duckhunt.py index c9545d7..aa8414e 100644 --- a/duckhunt/duckhunt.py +++ b/duckhunt.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Main entry point for DuckHunt Bot +DuckHunt IRC Bot - Main Entry Point """ import asyncio @@ -11,14 +11,18 @@ import os # Add src directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) -from src.duckhuntbot import IRCBot +from src.duckhuntbot import DuckHuntBot + def main(): + """Main entry point for DuckHunt Bot""" try: + # Load configuration with open('config.json') as f: config = json.load(f) - bot = IRCBot(config) + # Create and run bot + bot = DuckHuntBot(config) bot.logger.info("🦆 Starting DuckHunt Bot...") # Run the bot @@ -33,5 +37,6 @@ def main(): print(f"❌ Error: {e}") sys.exit(1) + if __name__ == '__main__': main() diff --git a/duckhunt/CONFIG_GUIDE.md b/duckhunt/CONFIG_GUIDE.md deleted file mode 100644 index d45e7aa..0000000 --- a/duckhunt/CONFIG_GUIDE.md +++ /dev/null @@ -1,272 +0,0 @@ -# DuckHunt Bot Configuration Guide - -This document explains all the configuration options available in `config.json` to customize your DuckHunt bot experience. - -## Basic IRC Settings -```json -{ - "server": "irc.rizon.net", // IRC server hostname - "port": 6697, // IRC server port (6667 for non-SSL, 6697 for SSL) - "nick": "DuckHunt", // Bot's nickname - "channels": ["#channel"], // List of channels to join - "ssl": true, // Enable SSL/TLS connection - "password": "", // Server password (if required) - "admins": ["nick1", "nick2"] // List of admin nicknames -} -``` - -## SASL Authentication -```json -"sasl": { - "enabled": true, // Enable SASL authentication - "username": "botaccount", // NickServ account username - "password": "botpassword" // NickServ account password -} -``` - -## Duck Spawning Configuration -```json -"duck_spawn_min": 1800, // Minimum time between duck spawns (seconds) -"duck_spawn_max": 5400, // Maximum time between duck spawns (seconds) -"duck_timeout_min": 45, // Minimum time duck stays alive (seconds) -"duck_timeout_max": 75, // Maximum time duck stays alive (seconds) -"sleep_hours": [], // Hours when no ducks spawn [start_hour, end_hour] -"max_ducks_per_channel": 3, // Maximum ducks that can exist per channel -``` - -### Duck Types -Configure different duck types with spawn rates and rewards: -```json -"duck_types": { - "normal": { - "enabled": true, // Enable this duck type - "spawn_rate": 70, // Percentage chance to spawn (out of 100) - "xp_reward": 10, // XP gained when caught - "health": 1 // How many hits to kill - }, - "golden": { - "enabled": true, - "spawn_rate": 8, - "xp_reward": 50, - "health": 1 - } - // ... more duck types -} -``` - -## Duck Befriending System -```json -"befriending": { - "enabled": true, // Enable !bef command - "base_success_rate": 65, // Base chance of successful befriend (%) - "max_success_rate": 90, // Maximum possible success rate (%) - "level_bonus_per_level": 2, // Success bonus per player level (%) - "level_bonus_cap": 20, // Maximum level bonus (%) - "luck_bonus_per_point": 3, // Success bonus per luck point (%) - "xp_reward": 8, // XP gained on successful befriend - "xp_reward_min": 1, // Minimum XP from befriending - "xp_reward_max": 3, // Maximum XP from befriending - "failure_xp_penalty": 1, // XP lost on failed befriend - "scared_away_chance": 10, // Chance duck flies away on failure (%) - "lucky_item_chance": 5 // Base chance for lucky item drops (%) -} -``` - -## Shooting Mechanics -```json -"shooting": { - "enabled": true, // Enable !bang command - "base_accuracy": 85, // Starting player accuracy (%) - "base_reliability": 90, // Starting gun reliability (%) - "jam_chance_base": 10, // Base gun jam chance (%) - "friendly_fire_enabled": true, // Allow shooting other players - "friendly_fire_chance": 5, // Chance of friendly fire (%) - "reflex_shot_bonus": 5, // Bonus for quick shots (%) - "miss_xp_penalty": 5, // XP lost on missed shot - "wild_shot_xp_penalty": 10, // XP lost on wild shot - "teamkill_xp_penalty": 20 // XP lost on team kill -} -``` - -## Weapon System -```json -"weapons": { - "enabled": true, // Enable weapon mechanics - "starting_weapon": "pistol", // Default weapon for new players - "starting_ammo": 6, // Starting ammo count - "max_ammo_base": 6, // Base maximum ammo capacity - "starting_chargers": 2, // Starting reload items - "max_chargers_base": 2, // Base maximum reload items - "durability_enabled": true, // Enable weapon wear/breaking - "confiscation_enabled": true // Allow admin gun confiscation -} -``` - -## Economy System -```json -"economy": { - "enabled": true, // Enable coin/shop system - "starting_coins": 100, // Coins for new players - "shop_enabled": true, // Enable !shop command - "trading_enabled": true, // Enable !trade command - "theft_enabled": true, // Enable !steal command - "theft_success_rate": 30, // Chance theft succeeds (%) - "theft_penalty": 50, // Coins lost if theft fails - "banking_enabled": true, // Enable banking system - "interest_rate": 5, // Bank interest rate (%) - "loan_enabled": true // Enable loan system -} -``` - -## Player Progression -```json -"progression": { - "enabled": true, // Enable XP/leveling system - "max_level": 40, // Maximum player level - "xp_multiplier": 1.0, // Global XP multiplier - "level_benefits_enabled": true, // Level bonuses (accuracy, etc.) - "titles_enabled": true, // Show player titles - "prestige_enabled": false // Enable prestige system -} -``` - -## Karma System -```json -"karma": { - "enabled": true, // Enable karma tracking - "hit_bonus": 2, // Karma for successful shots - "golden_hit_bonus": 5, // Karma for golden duck hits - "teamkill_penalty": 10, // Karma lost for team kills - "wild_shot_penalty": 3, // Karma lost for wild shots - "miss_penalty": 1, // Karma lost for misses - "befriend_success_bonus": 2, // Karma for successful befriends - "befriend_fail_penalty": 1 // Karma lost for failed befriends -} -``` - -## Items and Powerups -```json -"items": { - "enabled": true, // Enable item system - "lucky_items_enabled": true, // Enable lucky item drops - "lucky_item_base_chance": 5, // Base lucky item chance (%) - "detector_enabled": true, // Enable duck detector item - "silencer_enabled": true, // Enable silencer item - "sunglasses_enabled": true, // Enable sunglasses item - "explosive_ammo_enabled": true, // Enable explosive ammo - "sabotage_enabled": true, // Enable sabotage mechanics - "insurance_enabled": true, // Enable insurance system - "decoy_enabled": true // Enable decoy ducks -} -``` - -## Social Features -```json -"social": { - "leaderboards_enabled": true, // Enable !top command - "duck_alerts_enabled": true, // Enable duck spawn notifications - "private_messages_enabled": true, // Allow PM commands - "statistics_sharing_enabled": true, // Enable !stats sharing - "achievements_enabled": false // Enable achievement system -} -``` - -## Moderation Features -```json -"moderation": { - "ignore_system_enabled": true, // Enable !ignore command - "rate_limiting_enabled": true, // Prevent command spam - "rate_limit_cooldown": 2.0, // Seconds between commands - "admin_commands_enabled": true, // Enable admin commands - "ban_system_enabled": true, // Enable player banning - "database_reset_enabled": true, // Allow database resets - "admin_rearm_gives_full_ammo": true, // Admin !rearm gives full ammo - "admin_rearm_gives_full_chargers": true // Admin !rearm gives full chargers -} -``` - -## Advanced Features -```json -"advanced": { - "gun_jamming_enabled": true, // Enable gun jam mechanics - "weather_effects_enabled": false, // Weather affecting gameplay - "seasonal_events_enabled": false, // Special holiday events - "daily_challenges_enabled": false, // Daily quest system - "guild_system_enabled": false, // Player guilds/teams - "pvp_enabled": false // Player vs player combat -} -``` - -## Message Customization -```json -"messages": { - "custom_duck_messages_enabled": true, // Varied duck spawn messages - "color_enabled": true, // IRC color codes in messages - "emoji_enabled": true, // Unicode emojis in messages - "verbose_messages": true, // Detailed action messages - "success_sound_effects": true // Text sound effects -} -``` - -## Database Settings -```json -"database": { - "auto_save_enabled": true, // Automatic database saving - "auto_save_interval": 300, // Auto-save every N seconds - "backup_enabled": true, // Create database backups - "backup_interval": 3600, // Backup every N seconds - "compression_enabled": false // Compress database files -} -``` - -## Debug Options -```json -"debug": { - "debug_mode": false, // Enable debug features - "verbose_logging": false, // Extra detailed logs - "command_logging": false, // Log all commands - "performance_monitoring": false // Track performance metrics -} -``` - -## Configuration Tips - -1. **Duck Spawn Timing**: Adjust `duck_spawn_min/max` based on channel activity -2. **Difficulty**: Lower `befriending.base_success_rate` for harder gameplay -3. **Economy**: Adjust XP rewards to balance progression -4. **Features**: Disable unwanted features by setting `enabled: false` -5. **Performance**: Enable rate limiting and disable verbose logging for busy channels -6. **Testing**: Use debug mode and shorter spawn times for testing - -## Example Configurations - -### Casual Server (Easy) -```json -"befriending": { - "base_success_rate": 80, - "max_success_rate": 95 -}, -"economy": { - "starting_coins": 200 -} -``` - -### Competitive Server (Hard) -```json -"befriending": { - "base_success_rate": 45, - "max_success_rate": 75 -}, -"shooting": { - "base_accuracy": 70, - "friendly_fire_chance": 10 -} -``` - -### Minimal Features -```json -"befriending": { "enabled": false }, -"items": { "enabled": false }, -"karma": { "enabled": false }, -"social": { "leaderboards_enabled": false } -``` diff --git a/duckhunt/README.md b/duckhunt/README.md deleted file mode 100644 index f17e78d..0000000 --- a/duckhunt/README.md +++ /dev/null @@ -1,172 +0,0 @@ -# 🦆 DuckHunt IRC Bot - -A feature-rich IRC game bot where players hunt ducks, upgrade weapons, trade items, and compete on leaderboards! - -## 🚀 Features - -### 🎯 Core Game Mechanics -- **Different Duck Types**: Common, Rare, Golden, and Armored ducks with varying rewards -- **Weapon System**: Multiple weapon types (Basic Gun, Shotgun, Rifle) with durability -- **Ammunition Types**: Standard, Rubber Bullets, Explosive Rounds -- **Weapon Attachments**: Laser Sight, Extended Magazine, Bipod -- **Accuracy & Reliability**: Skill-based hit/miss and reload failure mechanics - -### 🏦 Economy System -- **Shop**: Buy/sell weapons, attachments, and upgrades -- **Banking**: Deposit coins for interest, take loans -- **Trading**: Trade coins and items with other players -- **Insurance**: Protect your equipment from damage -- **Hunting Licenses**: Unlock premium features and bonuses - -### 👤 Player Progression -- **Hunter Levels**: Gain XP and level up for better abilities -- **Account System**: Register accounts with password authentication -- **Multiple Auth Methods**: Nick-based, hostmask, or registered account -- **Persistent Stats**: All progress saved to SQLite database - -### 🏆 Social Features -- **Leaderboards**: Compete for top rankings -- **Duck Alerts**: Get notified when rare ducks spawn -- **Sabotage**: Interfere with other players (for a cost!) -- **Comprehensive Help**: Detailed command reference - -## 📋 Requirements - -- Python 3.7+ -- asyncio support -- SQLite3 (included with Python) - -## 🛠️ Installation - -1. Clone or download the bot files -2. Edit `config.json` with your IRC server details: - ```json - { - "server": "irc.libera.chat", - "port": 6697, - "nick": "DuckHuntBot", - "channels": ["#yourchannel"], - "ssl": true, - "sasl": false, - "password": "", - "duck_spawn_min": 60, - "duck_spawn_max": 300 - } - ``` - -3. Test the bot: - ```bash - python test_bot.py - ``` - -4. Run the bot: - ```bash - python duckhunt.py - ``` - -## 🎮 Commands - -### 🎯 Hunting -- `!bang` - Shoot at a duck (accuracy-based hit/miss) -- `!reload` - Reload weapon (can fail based on reliability) -- `!catch` - Catch a duck with your hands -- `!bef` - Befriend a duck instead of shooting - -### 🛒 Economy -- `!shop` - View available items -- `!buy ` - Purchase items -- `!sell ` - Sell equipment -- `!bank` - Banking services -- `!trade ` - Trade with others - -### 📊 Stats & Info -- `!stats` - Detailed combat statistics -- `!duckstats` - Personal hunting record -- `!leaderboard` - Top players ranking -- `!license` - Hunting license management - -### ⚙️ Settings -- `!alerts` - Toggle duck spawn notifications -- `!help` - Complete command reference - -### 🔐 Account System -- `/msg BotNick register ` - Register account -- `/msg BotNick identify ` - Login to account - -### 🎮 Advanced -- `!sabotage ` - Sabotage another hunter's weapon - -## 🗂️ File Structure - -``` -duckhunt/ -├── src/ -│ ├── duckhuntbot.py # Main IRC bot logic -│ ├── game.py # Game mechanics and commands -│ ├── db.py # SQLite database handling -│ ├── auth.py # Authentication system -│ ├── items.py # Duck types, weapons, attachments -│ ├── logging_utils.py # Colored logging setup -│ └── utils.py # IRC message parsing -├── config.json # Bot configuration -├── duckhunt.py # Main entry point -├── test_bot.py # Test script -└── README.md # This file -``` - -## 🎯 Game Balance - -### Duck Types & Rewards -- **Common Duck** 🦆: 1 coin, 10 XP (70% spawn rate) -- **Rare Duck** 🦆✨: 3 coins, 25 XP (20% spawn rate) -- **Golden Duck** 🥇🦆: 10 coins, 50 XP (8% spawn rate) -- **Armored Duck** 🛡️🦆: 15 coins, 75 XP (2% spawn rate, 3 health) - -### Weapon Stats -- **Basic Gun**: 0% accuracy bonus, 100 durability, 1 attachment slot -- **Shotgun**: -10% accuracy, 80 durability, 2 slots, spread shot -- **Rifle**: +20% accuracy, 120 durability, 3 slots - -### Progression -- Players start with 100 coins and basic stats -- Level up by gaining XP from successful hunts -- Unlock better equipment and abilities as you progress - -## 🔧 Configuration - -Edit `config.json` to customize: -- IRC server and channels -- Duck spawn timing (min/max seconds) -- SSL and SASL authentication -- Bot nickname - -## 🛡️ Security - -- Passwords are hashed with PBKDF2 -- Account data stored separately from temporary nick data -- Multiple authentication methods supported -- Database uses prepared statements to prevent injection - -## 🐛 Troubleshooting - -1. **Bot won't connect**: Check server/port in config.json -2. **Database errors**: Ensure write permissions in bot directory -3. **Commands not working**: Verify bot has joined the channel -4. **Test failures**: Run `python test_bot.py` to diagnose issues - -## 🎖️ Contributing - -Feel free to add new features: -- More duck types and weapons -- Additional mini-games -- Seasonal events -- Guild/team systems -- Advanced trading mechanics - -## 📄 License - -This bot is provided as-is for educational and entertainment purposes. - ---- - -🦆 **Happy Hunting!** 🦆 diff --git a/duckhunt/__pycache__/simple_duckhunt.cpython-312.pyc b/duckhunt/__pycache__/simple_duckhunt.cpython-312.pyc deleted file mode 100644 index 5ed5d12..0000000 Binary files a/duckhunt/__pycache__/simple_duckhunt.cpython-312.pyc and /dev/null differ diff --git a/duckhunt/config_backup.json b/duckhunt/config_backup.json deleted file mode 100644 index 559a49c..0000000 --- a/duckhunt/config_backup.json +++ /dev/null @@ -1,216 +0,0 @@ -{ - "server": "irc.rizon.net", - "port": 6697, - "nick": "DuckHunt", - "channels": ["#computertech"], - "ssl": true, - "sasl": { - "enabled": true, - "username": "duckhunt", - "password": "duckhunt//789//" - }, - "password": "", - "admins": ["peorth", "computertech"], - - "_comment_duck_spawning": "Duck spawning configuration", - "duck_spawn_min": 1800, - "duck_spawn_max": 5400, - "duck_timeout_min": 45, - "duck_timeout_max": 75, - "duck_types": { - "normal": { - "enabled": true, - "spawn_rate": 70, - "xp_reward": 10, - "coin_reward": 1, - "health": 1 - }, - "golden": { - "enabled": true, - "spawn_rate": 8, - "xp_reward": 50, - "coin_reward": 10, - "health": 1 - }, - "armored": { - "enabled": true, - "spawn_rate": 2, - "xp_reward": 75, - "coin_reward": 15, - "health": 3 - }, - "rare": { - "enabled": true, - "spawn_rate": 20, - "xp_reward": 25, - "coin_reward": 3, - "health": 1 - } - }, - "sleep_hours": [], - "max_ducks_per_channel": 3, - - "_comment_befriending": "Duck befriending configuration", - "befriending": { - "enabled": true, - "base_success_rate": 65, - "max_success_rate": 90, - "level_bonus_per_level": 2, - "level_bonus_cap": 20, - "luck_bonus_per_point": 3, - "xp_reward": 8, - "coin_reward_min": 1, - "coin_reward_max": 2, - "failure_xp_penalty": 1, - "scared_away_chance": 10, - "lucky_item_chance": 5 - }, - - "_comment_shooting": "Shooting mechanics configuration", - "shooting": { - "enabled": true, - "base_accuracy": 85, - "base_reliability": 90, - "jam_chance_base": 10, - "friendly_fire_enabled": true, - "friendly_fire_chance": 5, - "reflex_shot_bonus": 5, - "miss_xp_penalty": 5, - "wild_shot_xp_penalty": 10, - "teamkill_xp_penalty": 20 - }, - - "_comment_weapons": "Weapon system configuration", - "weapons": { - "enabled": true, - "starting_weapon": "pistol", - "starting_ammo": 6, - "max_ammo_base": 6, - "starting_chargers": 2, - "max_chargers_base": 2, - "durability_enabled": true, - "confiscation_enabled": true - }, - - "_comment_economy": "Economy and shop configuration", - "economy": { - "enabled": true, - "starting_coins": 100, - "shop_enabled": true, - "trading_enabled": true, - "theft_enabled": true, - "theft_success_rate": 30, - "theft_penalty": 50, - "banking_enabled": true, - "interest_rate": 5, - "loan_enabled": true - }, - - "_comment_progression": "Player progression configuration", - "progression": { - "enabled": true, - "max_level": 40, - "xp_multiplier": 1.0, - "level_benefits_enabled": true, - "titles_enabled": true, - "prestige_enabled": false - }, - - "_comment_karma": "Karma system configuration", - "karma": { - "enabled": true, - "hit_bonus": 2, - "golden_hit_bonus": 5, - "teamkill_penalty": 10, - "wild_shot_penalty": 3, - "miss_penalty": 1, - "befriend_success_bonus": 2, - "befriend_fail_penalty": 1 - }, - - "_comment_items": "Items and powerups configuration", - "items": { - "enabled": true, - "lucky_items_enabled": true, - "lucky_item_base_chance": 5, - "detector_enabled": true, - "silencer_enabled": true, - "sunglasses_enabled": true, - "explosive_ammo_enabled": true, - "sabotage_enabled": true, - "insurance_enabled": true, - "decoy_enabled": true - }, - - "_comment_social": "Social features configuration", - "social": { - "leaderboards_enabled": true, - "duck_alerts_enabled": true, - "private_messages_enabled": true, - "statistics_sharing_enabled": true, - "achievements_enabled": false - }, - - "_comment_moderation": "Moderation and admin features", - "moderation": { - "ignore_system_enabled": true, - "rate_limiting_enabled": true, - "rate_limit_cooldown": 2.0, - "admin_commands_enabled": true, - "ban_system_enabled": true, - "database_reset_enabled": true - }, - - "_comment_advanced": "Advanced game mechanics", - "advanced": { - "gun_jamming_enabled": true, - "weather_effects_enabled": false, - "seasonal_events_enabled": false, - "daily_challenges_enabled": false, - "guild_system_enabled": false, - "pvp_enabled": false - }, - - "_comment_messages": "Message customization", - "messages": { - "custom_duck_messages_enabled": true, - "color_enabled": true, - "emoji_enabled": true, - "verbose_messages": true, - "success_sound_effects": true - }, - - "_comment_database": "Database and persistence", - "database": { - "auto_save_enabled": true, - "auto_save_interval": 300, - "backup_enabled": true, - "backup_interval": 3600, - "compression_enabled": false - }, - - "_comment_debug": "Debug and logging options", - "debug": { - "debug_mode": false, - "verbose_logging": false, - "command_logging": false, - "performance_monitoring": false - } -}c.rizon.net", - "port": 6697, - "nick": "DuckHunt", - "channels": ["#computertech"], - "ssl": true, - "sasl": { - "enabled": true, - "username": "duckhunt", - "password": "duckhunt//789//" - }, - "password": "", - "admins": ["colby", "computertech"], - "duck_spawn_min": 1800, - "duck_spawn_max": 5400, - "duck_timeout_min": 45, - "duck_timeout_max": 75, - "_comment": "Run with: python3 simple_duckhunt.py | Admins config-only | Private admin: /msg DuckHuntBot restart|quit|launch | Duck timeout: random between min-max seconds" -} diff --git a/duckhunt/config_local.json b/duckhunt/config_local.json deleted file mode 100644 index 0c318f4..0000000 --- a/duckhunt/config_local.json +++ /dev/null @@ -1,205 +0,0 @@ -{ - "server": "irc.rizon.net", - "port": 6697, - "nick": "DuckHunt", - "channels": ["#computertech"], - "ssl": true, - "sasl": { - "enabled": true, - "username": "duckhunt", - "password": "duckhunt//789//" - }, - "password": "", - "admins": ["peorth", "computertech", "colby"], - - "_comment_duck_spawning": "Duck spawning configuration", - "duck_spawn_min": 1800, - "duck_spawn_max": 5400, - "duck_timeout_min": 45, - "duck_timeout_max": 75, - "duck_types": { - "normal": { - "spawn_chance": 0.6, - "xp_reward": 10, - "difficulty": 1.0, - "flee_time": 15, - "messages": ["・゜゜・。。・゜゜\\\\_o< QUACK!"] - }, - "fast": { - "spawn_chance": 0.25, - "xp_reward": 15, - "difficulty": 1.5, - "flee_time": 8, - "messages": ["・゜゜・。。・゜゜\\\\_o< QUACK! (Fast duck!)"] - }, - "rare": { - "spawn_chance": 0.1, - "xp_reward": 30, - "difficulty": 2.0, - "flee_time": 12, - "messages": ["・゜゜・。。・゜゜\\\\_o< QUACK! (Rare duck!)"] - }, - "golden": { - "spawn_chance": 0.05, - "xp_reward": 75, - "difficulty": 3.0, - "flee_time": 10, - "messages": ["・゜゜・。。・゜゜\\\\_✪< ★ GOLDEN DUCK ★"] - } - }, - "sleep_hours": [], - "max_ducks_per_channel": 3, - - "_comment_befriending": "Duck befriending configuration", - "befriending": { - "enabled": true, - "success_chance": 0.7, - "failure_messages": [ - "The duck looked at you suspiciously and flew away!", - "The duck didn't trust you and escaped!", - "The duck was too scared and ran off!" - ], - "scared_away_chance": 0.1, - "scared_away_messages": [ - "You scared the duck away with your approach!", - "The duck was terrified and fled immediately!" - ], - "xp_reward_min": 1, - "xp_reward_max": 3 - }, - - "_comment_shooting": "Shooting mechanics configuration", - "shooting": { - "enabled": true, - "base_accuracy": 85, - "base_reliability": 90, - "jam_chance_base": 10, - "friendly_fire_enabled": true, - "friendly_fire_chance": 5, - "reflex_shot_bonus": 5, - "miss_xp_penalty": 5, - "wild_shot_xp_penalty": 10, - "teamkill_xp_penalty": 20 - }, - - "_comment_weapons": "Weapon system configuration", - "weapons": { - "enabled": true, - "starting_weapon": "pistol", - "starting_ammo": 6, - "max_ammo_base": 6, - "starting_chargers": 2, - "max_chargers_base": 2, - "durability_enabled": true, - "confiscation_enabled": true - }, - - "_comment_economy": "Economy and shop configuration", - "economy": { - "enabled": true, - "starting_coins": 100, - "shop_enabled": true, - "trading_enabled": true, - "theft_enabled": true, - "theft_success_rate": 30, - "theft_penalty": 50, - "banking_enabled": true, - "interest_rate": 5, - "loan_enabled": true, - "inventory_system_enabled": true, - "max_inventory_slots": 20 - }, - - "_comment_progression": "Player progression configuration", - "progression": { - "enabled": true, - "max_level": 40, - "xp_multiplier": 1.0, - "level_benefits_enabled": true, - "titles_enabled": true, - "prestige_enabled": false - }, - - "_comment_karma": "Karma system configuration", - "karma": { - "enabled": true, - "hit_bonus": 2, - "golden_hit_bonus": 5, - "teamkill_penalty": 10, - "wild_shot_penalty": 3, - "miss_penalty": 1, - "befriend_success_bonus": 2, - "befriend_fail_penalty": 1 - }, - - "_comment_items": "Items and powerups configuration", - "items": { - "enabled": true, - "lucky_items_enabled": true, - "lucky_item_base_chance": 5, - "detector_enabled": true, - "silencer_enabled": true, - "sunglasses_enabled": true, - "explosive_ammo_enabled": true, - "sabotage_enabled": true, - "insurance_enabled": true, - "decoy_enabled": true - }, - - "_comment_social": "Social features configuration", - "social": { - "leaderboards_enabled": true, - "duck_alerts_enabled": true, - "private_messages_enabled": true, - "statistics_sharing_enabled": true, - "achievements_enabled": false - }, - - "_comment_moderation": "Moderation and admin features", - "moderation": { - "ignore_system_enabled": true, - "rate_limiting_enabled": true, - "rate_limit_cooldown": 2.0, - "admin_commands_enabled": true, - "ban_system_enabled": true, - "database_reset_enabled": true, - "admin_rearm_gives_full_ammo": false, - "admin_rearm_gives_full_chargers":false - }, - - "_comment_advanced": "Advanced game mechanics", - "advanced": { - "gun_jamming_enabled": true, - "weather_effects_enabled": false, - "seasonal_events_enabled": false, - "daily_challenges_enabled": false, - "guild_system_enabled": false, - "pvp_enabled": false - }, - - "_comment_messages": "Message customization", - "messages": { - "custom_duck_messages_enabled": true, - "color_enabled": true, - "emoji_enabled": true, - "verbose_messages": true, - "success_sound_effects": true - }, - - "_comment_database": "Database and persistence", - "database": { - "auto_save_enabled": true, - "auto_save_interval": 300, - "backup_enabled": true, - "backup_interval": 3600, - "compression_enabled": false - }, - - "_comment_debug": "Debug and logging options", - "debug": { - "debug_mode": false, - "verbose_logging": false, - "command_logging": false, - "performance_monitoring": false - } -} diff --git a/duckhunt/config_new.json b/duckhunt/config_new.json deleted file mode 100644 index e69de29..0000000 diff --git a/duckhunt/demo_sasl.py b/duckhunt/demo_sasl.py deleted file mode 100644 index 90b0b0a..0000000 --- a/duckhunt/demo_sasl.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 -""" -SASL Integration Demo for DuckHunt Bot -This script demonstrates how the modular SASL authentication works -""" - -import asyncio -import json -import sys -import os - -# Add src directory to path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) - -from src.sasl import SASLHandler -from src.logging_utils import setup_logger - -class MockBot: - """Mock bot for testing SASL without IRC connection""" - def __init__(self, config): - self.config = config - self.logger = setup_logger("MockBot") - self.messages_sent = [] - - def send_raw(self, message): - """Mock send_raw that just logs the message""" - self.messages_sent.append(message) - self.logger.info(f"SEND: {message}") - - async def register_user(self): - """Mock registration""" - self.logger.info("Mock user registration completed") - -async def demo_sasl_flow(): - """Demonstrate the SASL authentication flow""" - print("🔐 SASL Authentication Flow Demo") - print("=" * 50) - - # Load config - with open('config.json') as f: - config = json.load(f) - - # Override with test credentials for demo - config['sasl'] = { - 'enabled': True, - 'username': 'testuser', - 'password': 'testpass123' - } - - # Create mock bot and SASL handler - bot = MockBot(config) - sasl_handler = SASLHandler(bot, config) - - print("\n1️⃣ Starting SASL negotiation...") - if await sasl_handler.start_negotiation(): - print("✅ SASL negotiation started successfully") - else: - print("❌ SASL negotiation failed to start") - return - - print("\n2️⃣ Simulating server CAP response...") - # Simulate server listing SASL capability - params = ['*', 'LS', '*'] - trailing = 'sasl multi-prefix extended-join' - await sasl_handler.handle_cap_response(params, trailing) - - print("\n3️⃣ Simulating server acknowledging SASL capability...") - # Simulate server acknowledging SASL - params = ['*', 'ACK'] - trailing = 'sasl' - await sasl_handler.handle_cap_response(params, trailing) - - print("\n4️⃣ Simulating server ready for authentication...") - # Simulate server ready for auth - params = ['+'] - await sasl_handler.handle_authenticate_response(params) - - print("\n5️⃣ Simulating successful authentication...") - # Simulate successful authentication - params = ['DuckHunt'] - trailing = 'You are now logged in as duckhunt' - await sasl_handler.handle_sasl_result('903', params, trailing) - - print(f"\n📤 Messages sent to server:") - for i, msg in enumerate(bot.messages_sent, 1): - print(f" {i}. {msg}") - - print(f"\n🔍 Authentication status: {'✅ Authenticated' if sasl_handler.is_authenticated() else '❌ Not authenticated'}") - - print("\n" + "=" * 50) - print("✨ SASL flow demonstration complete!") - -async def demo_sasl_failure(): - """Demonstrate SASL failure handling""" - print("\n\n🚫 SASL Failure Handling Demo") - print("=" * 50) - - # Create mock bot with wrong credentials - config = { - 'sasl': { - 'enabled': True, - 'username': 'testuser', - 'password': 'wrong_password' - } - } - bot = MockBot(config) - sasl_handler = SASLHandler(bot, config) - - print("\n1️⃣ Starting SASL with wrong credentials...") - await sasl_handler.start_negotiation() - - # Simulate failed authentication - params = ['DuckHunt'] - trailing = 'Invalid credentials' - await sasl_handler.handle_sasl_result('904', params, trailing) - - print(f"\n🔍 Authentication status: {'✅ Authenticated' if sasl_handler.is_authenticated() else '❌ Not authenticated'}") - print("✅ Failure handled gracefully - bot will fallback to NickServ") - -if __name__ == '__main__': - asyncio.run(demo_sasl_flow()) - asyncio.run(demo_sasl_failure()) diff --git a/duckhunt/duckhunt.db b/duckhunt/duckhunt.db deleted file mode 100644 index e00e26c..0000000 Binary files a/duckhunt/duckhunt.db and /dev/null differ diff --git a/duckhunt/duckhunt.log b/duckhunt/duckhunt.log deleted file mode 100644 index b9276b5..0000000 --- a/duckhunt/duckhunt.log +++ /dev/null @@ -1,559 +0,0 @@ -[2025-09-11 18:30:40,346] INFO: Loaded 3 players from duckhunt.json -[2025-09-11 18:30:40,346] INFO: DuckHunt Bot initializing... -[2025-09-11 18:30:40,347] INFO: Starting DuckHunt Bot... -[2025-09-11 18:30:40,347] INFO: Loaded 3 players from duckhunt.json -[2025-09-11 18:30:40,420] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-11 18:30:40,579] INFO: Connected successfully! -[2025-09-11 18:30:40,579] INFO: Registering as DuckHuntBot -[2025-09-11 18:30:41,067] INFO: Successfully registered! -[2025-09-11 18:30:41,067] INFO: Joining #colby -[2025-09-11 18:30:41,118] INFO: Successfully joined #colby -[2025-09-11 18:30:41,582] INFO: Starting duck spawning... -[2025-09-11 18:30:46,583] INFO: Admin spawned normal duck 965d7945 in #colby -[2025-09-11 18:30:46,583] INFO: Waiting 56m 37s for next duck -[2025-09-11 18:31:46,591] INFO: Duck 965d7945 timed out in #colby -[2025-09-11 18:38:33,894] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-11 18:38:34,097] INFO: Shutdown requested, stopping all tasks... -[2025-09-11 18:38:34,097] INFO: Starting cleanup process... -[2025-09-11 18:38:35,211] INFO: IRC connection closed -[2025-09-11 18:38:35,225] INFO: Final database save completed - 3 players saved -[2025-09-11 18:38:35,226] INFO: Cleanup completed successfully -[2025-09-11 18:38:35,234] INFO: DuckHunt Bot shutdown complete -[2025-09-11 18:38:53,536] INFO: Loaded 3 players from duckhunt.json -[2025-09-11 18:38:53,536] INFO: DuckHunt Bot initializing... -[2025-09-11 18:38:53,537] INFO: Starting DuckHunt Bot... -[2025-09-11 18:38:53,537] INFO: Loaded 3 players from duckhunt.json -[2025-09-11 18:38:53,607] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-11 18:38:53,785] INFO: Connected successfully! -[2025-09-11 18:38:53,785] INFO: SASL authentication enabled -[2025-09-11 18:38:54,162] INFO: SASL capability available -[2025-09-11 18:38:54,221] INFO: SASL capability acknowledged -[2025-09-11 18:38:54,221] INFO: Authenticating via SASL as duckhunt -[2025-09-11 18:38:54,645] INFO: Server ready for SASL authentication -[2025-09-11 18:38:54,645] ERROR: SASL authentication failed! -[2025-09-11 18:38:54,645] INFO: Registering as DuckHunt -[2025-09-11 18:39:27,102] WARNING: Connection closed by server -[2025-09-11 18:39:27,103] WARNING: A main task completed unexpectedly -[2025-09-11 18:39:27,103] INFO: Starting cleanup process... -[2025-09-11 18:39:27,105] INFO: Final database save completed - 3 players saved -[2025-09-11 18:39:27,105] INFO: Cleanup completed successfully -[2025-09-11 18:39:27,106] INFO: DuckHunt Bot shutdown complete -[2025-09-11 18:41:03,279] INFO: Loaded 3 players from duckhunt.json -[2025-09-11 18:41:03,279] INFO: DuckHunt Bot initializing... -[2025-09-11 18:41:03,280] INFO: Starting DuckHunt Bot... -[2025-09-11 18:41:03,280] INFO: Loaded 3 players from duckhunt.json -[2025-09-11 18:41:03,354] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-11 18:41:03,516] INFO: Connected successfully! -[2025-09-11 18:41:03,517] INFO: SASL authentication enabled -[2025-09-11 18:41:03,611] INFO: SASL capability available -[2025-09-11 18:41:03,660] INFO: SASL capability acknowledged -[2025-09-11 18:41:03,660] INFO: Authenticating via SASL as duckhunt -[2025-09-11 18:41:04,075] INFO: Server ready for SASL authentication -[2025-09-11 18:41:04,076] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found) -[2025-09-11 18:41:04,076] ERROR: Attempted username: duckhunt -[2025-09-11 18:41:04,076] INFO: Registering as DuckHunt -[2025-09-11 18:41:36,030] WARNING: Connection closed by server -[2025-09-11 18:41:36,031] WARNING: A main task completed unexpectedly -[2025-09-11 18:41:36,031] INFO: Starting cleanup process... -[2025-09-11 18:41:36,032] INFO: Final database save completed - 3 players saved -[2025-09-11 18:41:36,032] INFO: Cleanup completed successfully -[2025-09-11 18:41:36,033] INFO: DuckHunt Bot shutdown complete -[2025-09-11 17:52:42,777] INFO: Loaded 3 players from duckhunt.json -[2025-09-11 17:52:42,778] INFO: DuckHunt Bot initializing... -[2025-09-11 17:52:42,778] INFO: Starting DuckHunt Bot... -[2025-09-11 17:52:42,778] INFO: Loaded 3 players from duckhunt.json -[2025-09-11 17:52:42,800] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-11 17:52:42,914] INFO: Connected successfully! -[2025-09-11 17:52:42,914] INFO: SASL authentication enabled -[2025-09-11 17:52:42,926] INFO: SASL capability available -[2025-09-11 17:52:42,932] INFO: SASL capability acknowledged -[2025-09-11 17:52:42,932] INFO: Authenticating via SASL as duckhunt -[2025-09-11 17:52:43,305] INFO: Server ready for SASL authentication -[2025-09-11 17:52:43,305] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found) -[2025-09-11 17:52:43,305] INFO: Falling back to NickServ identification... -[2025-09-11 17:52:43,306] ERROR: Attempted username: duckhunt -[2025-09-11 17:52:43,306] INFO: Registering as DuckHunt -[2025-09-11 17:52:43,357] ERROR: SASL authentication aborted! (906) -[2025-09-11 17:52:43,357] INFO: Falling back to NickServ identification... -[2025-09-11 17:52:43,358] INFO: Registering as DuckHunt -[2025-09-11 17:52:43,358] INFO: Successfully registered! -[2025-09-11 17:52:43,358] INFO: Attempting NickServ identification for duckhunt -[2025-09-11 17:52:44,359] INFO: NickServ identification commands sent -[2025-09-11 17:52:44,360] INFO: Joining #computertech -[2025-09-11 17:52:44,366] INFO: Successfully joined #computertech -[2025-09-11 17:52:44,917] INFO: Starting duck spawning... -[2025-09-11 17:52:49,920] INFO: Admin spawned normal duck 2afda3aa in #computertech -[2025-09-11 17:52:49,920] INFO: Waiting 47m 46s for next duck -[2025-09-11 17:53:11,783] INFO: Admin spawned normal duck abbfac62 in #computertech -[2025-09-11 17:54:10,655] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-11 17:54:11,044] INFO: Duck spawning stopped due to shutdown request -[2025-09-11 17:54:11,045] INFO: Shutdown requested, stopping all tasks... -[2025-09-11 17:54:11,045] INFO: Starting cleanup process... -[2025-09-11 17:54:12,149] INFO: IRC connection closed -[2025-09-11 17:54:12,154] INFO: Final database save completed - 6 players saved -[2025-09-11 17:54:12,154] INFO: Cleanup completed successfully -[2025-09-11 17:54:12,156] INFO: DuckHunt Bot shutdown complete -[2025-09-11 17:54:28,757] INFO: Loaded 6 players from duckhunt.json -[2025-09-11 17:54:28,757] INFO: DuckHunt Bot initializing... -[2025-09-11 17:54:28,757] INFO: Starting DuckHunt Bot... -[2025-09-11 17:54:28,757] INFO: Loaded 6 players from duckhunt.json -[2025-09-11 17:54:28,780] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-11 17:54:28,894] INFO: Connected successfully! -[2025-09-11 17:54:28,894] INFO: SASL authentication enabled -[2025-09-11 17:54:28,906] INFO: SASL capability available -[2025-09-11 17:54:28,913] INFO: SASL capability acknowledged -[2025-09-11 17:54:28,913] INFO: Authenticating via SASL as duckhunt -[2025-09-11 17:54:29,288] INFO: Server ready for SASL authentication -[2025-09-11 17:54:29,289] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found) -[2025-09-11 17:54:29,289] INFO: Falling back to NickServ identification... -[2025-09-11 17:54:29,289] ERROR: Attempted username: duckhunt -[2025-09-11 17:54:29,289] INFO: Registering as DuckHunt -[2025-09-11 17:54:29,302] ERROR: SASL authentication aborted! (906) -[2025-09-11 17:54:29,303] INFO: Falling back to NickServ identification... -[2025-09-11 17:54:29,303] INFO: Registering as DuckHunt -[2025-09-11 17:54:29,303] INFO: Successfully registered! -[2025-09-11 17:54:29,303] INFO: Attempting NickServ identification for duckhunt -[2025-09-11 17:54:30,304] INFO: NickServ identification commands sent -[2025-09-11 17:54:30,305] INFO: Joining #computertech -[2025-09-11 17:54:30,311] INFO: Successfully joined #computertech -[2025-09-11 17:54:30,898] INFO: Starting duck spawning... -[2025-09-11 17:54:35,900] INFO: Admin spawned normal duck ff2612cf in #computertech -[2025-09-11 17:54:35,901] INFO: Waiting 41m 7s for next duck -[2025-09-11 17:55:55,911] INFO: Duck ff2612cf timed out in #computertech -[2025-09-11 18:10:31,079] INFO: Admin spawned normal duck b7398aed in #computertech -[2025-09-11 18:35:46,651] INFO: Admin spawned normal duck ae21e5f4 in #computertech -[2025-09-11 18:35:46,651] INFO: Waiting 30m 29s for next duck -[2025-09-11 19:06:18,504] INFO: Admin spawned normal duck 79032e28 in #computertech -[2025-09-11 19:06:18,505] INFO: Waiting 37m 53s for next duck -[2025-09-11 19:44:15,288] INFO: Admin spawned normal duck 02fe65b6 in #computertech -[2025-09-11 19:44:15,288] INFO: Waiting 39m 22s for next duck -[2025-09-11 20:23:41,185] INFO: Admin spawned normal duck 829273ae in #computertech -[2025-09-11 20:23:41,186] INFO: Waiting 47m 31s for next duck -[2025-09-11 20:24:57,093] INFO: Duck 829273ae timed out in #computertech -[2025-09-11 21:11:16,779] INFO: Admin spawned normal duck 8298e88d in #computertech -[2025-09-11 21:11:16,779] INFO: Waiting 75m 48s for next duck -[2025-09-11 22:27:12,035] INFO: Admin spawned normal duck ef5755f3 in #computertech -[2025-09-11 22:27:12,035] INFO: Waiting 55m 57s for next duck -[2025-09-11 23:23:14,339] INFO: Admin spawned normal duck 9725ad5b in #computertech -[2025-09-11 23:23:14,339] INFO: Waiting 57m 47s for next duck -[2025-09-12 00:21:06,885] INFO: Admin spawned normal duck 62b88d87 in #computertech -[2025-09-12 00:21:06,885] INFO: Waiting 73m 47s for next duck -[2025-09-12 01:35:00,951] INFO: Admin spawned normal duck 3f7dc294 in #computertech -[2025-09-12 01:35:00,951] INFO: Waiting 84m 10s for next duck -[2025-09-12 01:36:09,586] INFO: Duck 3f7dc294 timed out in #computertech -[2025-09-12 02:59:19,153] INFO: Admin spawned normal duck 45e3ab57 in #computertech -[2025-09-12 02:59:19,154] INFO: Waiting 60m 35s for next duck -[2025-09-12 02:59:25,289] WARNING: A main task completed unexpectedly -[2025-09-12 02:59:25,289] INFO: Starting cleanup process... -[2025-09-12 02:59:26,394] INFO: IRC connection closed -[2025-09-12 02:59:26,400] INFO: Final database save completed - 16 players saved -[2025-09-12 02:59:26,400] INFO: Cleanup completed successfully -[2025-09-12 02:59:26,401] INFO: DuckHunt Bot shutdown complete -[2025-09-12 13:45:28,730] INFO: Loaded 16 players from duckhunt.json -[2025-09-12 13:45:28,730] INFO: DuckHunt Bot initializing... -[2025-09-12 13:45:28,730] INFO: Starting DuckHunt Bot... -[2025-09-12 13:45:28,731] INFO: Loaded 16 players from duckhunt.json -[2025-09-12 13:45:28,753] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 13:45:28,865] INFO: Connected successfully! -[2025-09-12 13:45:28,865] INFO: SASL authentication enabled -[2025-09-12 13:45:30,015] INFO: SASL capability available -[2025-09-12 13:45:30,022] INFO: SASL capability acknowledged -[2025-09-12 13:45:30,022] INFO: Authenticating via SASL as duckhunt -[2025-09-12 13:45:30,389] INFO: Server ready for SASL authentication -[2025-09-12 13:45:30,389] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found) -[2025-09-12 13:45:30,389] INFO: Falling back to NickServ identification... -[2025-09-12 13:45:30,389] ERROR: Attempted username: duckhunt -[2025-09-12 13:45:30,389] INFO: Registering as DuckHunt -[2025-09-12 13:45:30,402] ERROR: SASL authentication aborted! (906) -[2025-09-12 13:45:30,402] INFO: Falling back to NickServ identification... -[2025-09-12 13:45:30,403] INFO: Registering as DuckHunt -[2025-09-12 13:45:30,403] INFO: Successfully registered! -[2025-09-12 13:45:30,403] INFO: Attempting NickServ identification for duckhunt -[2025-09-12 13:45:31,406] INFO: NickServ identification commands sent -[2025-09-12 13:45:31,406] INFO: Joining #computertech -[2025-09-12 13:45:31,413] INFO: Successfully joined #computertech -[2025-09-12 13:45:31,870] INFO: Starting duck spawning... -[2025-09-12 13:45:36,872] INFO: Admin spawned normal duck 8e703ce0 in #computertech -[2025-09-12 13:45:36,872] INFO: Waiting 64m 21s for next duck -[2025-09-12 13:45:46,370] WARNING: A main task completed unexpectedly -[2025-09-12 13:45:46,370] INFO: Starting cleanup process... -[2025-09-12 13:45:47,474] INFO: IRC connection closed -[2025-09-12 13:45:47,479] INFO: Final database save completed - 16 players saved -[2025-09-12 13:45:47,479] INFO: Cleanup completed successfully -[2025-09-12 13:45:47,480] INFO: DuckHunt Bot shutdown complete -[2025-09-12 15:02:57,578] INFO: Loaded 16 players from duckhunt.json -[2025-09-12 15:02:57,578] INFO: DuckHunt Bot initializing... -[2025-09-12 15:02:57,578] INFO: Starting DuckHunt Bot... -[2025-09-12 15:02:57,578] INFO: Loaded 16 players from duckhunt.json -[2025-09-12 15:02:57,601] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 15:02:57,725] INFO: Connected successfully! -[2025-09-12 15:02:57,726] INFO: SASL authentication enabled -[2025-09-12 15:02:57,738] INFO: SASL capability available -[2025-09-12 15:02:57,745] INFO: SASL capability acknowledged -[2025-09-12 15:02:57,745] INFO: Authenticating via SASL as duckhunt -[2025-09-12 15:02:58,113] INFO: Server ready for SASL authentication -[2025-09-12 15:02:58,113] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found) -[2025-09-12 15:02:58,113] INFO: Falling back to NickServ identification... -[2025-09-12 15:02:58,113] ERROR: Attempted username: duckhunt -[2025-09-12 15:02:58,114] INFO: Registering as DuckHunt -[2025-09-12 15:02:58,172] ERROR: SASL authentication aborted! (906) -[2025-09-12 15:02:58,173] INFO: Falling back to NickServ identification... -[2025-09-12 15:02:58,173] INFO: Registering as DuckHunt -[2025-09-12 15:02:58,173] INFO: Successfully registered! -[2025-09-12 15:02:58,174] INFO: Attempting NickServ identification for duckhunt -[2025-09-12 15:02:59,176] INFO: NickServ identification commands sent -[2025-09-12 15:02:59,176] INFO: Joining #computertech -[2025-09-12 15:02:59,183] INFO: Successfully joined #computertech -[2025-09-12 15:02:59,728] INFO: Starting duck spawning... -[2025-09-12 15:03:04,730] INFO: Admin spawned normal duck 3e922056 in #computertech -[2025-09-12 15:03:04,731] INFO: Waiting 51m 22s for next duck -[2025-09-12 15:03:12,954] WARNING: A main task completed unexpectedly -[2025-09-12 15:03:12,954] INFO: Starting cleanup process... -[2025-09-12 15:03:14,059] INFO: IRC connection closed -[2025-09-12 15:03:14,062] INFO: Final database save completed - 16 players saved -[2025-09-12 15:03:14,063] INFO: Cleanup completed successfully -[2025-09-12 15:03:14,063] INFO: DuckHunt Bot shutdown complete -[2025-09-12 18:34:46,067] INFO: Loaded 16 players from duckhunt.json -[2025-09-12 18:34:46,067] INFO: DuckHunt Bot initializing... -[2025-09-12 18:34:46,067] INFO: Starting DuckHunt Bot... -[2025-09-12 18:34:46,067] INFO: Loaded 16 players from duckhunt.json -[2025-09-12 18:34:46,089] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 18:34:46,191] INFO: Connected successfully! -[2025-09-12 18:34:46,191] INFO: SASL authentication enabled -[2025-09-12 18:34:47,098] INFO: SASL capability available -[2025-09-12 18:34:47,105] INFO: SASL capability acknowledged -[2025-09-12 18:34:47,105] INFO: Authenticating via SASL as duckhunt -[2025-09-12 18:34:47,472] INFO: Server ready for SASL authentication -[2025-09-12 18:34:47,472] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found) -[2025-09-12 18:34:47,473] INFO: Falling back to NickServ identification... -[2025-09-12 18:34:47,473] ERROR: Attempted username: duckhunt -[2025-09-12 18:34:47,474] INFO: Registering as DuckHunt -[2025-09-12 18:35:13,492] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 18:35:13,528] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 18:35:13,528] INFO: Starting cleanup process... -[2025-09-12 18:35:14,531] INFO: IRC connection closed -[2025-09-12 18:35:14,532] INFO: Final database save completed - 16 players saved -[2025-09-12 18:35:14,532] INFO: Cleanup completed successfully -[2025-09-12 18:35:14,532] INFO: DuckHunt Bot shutdown complete -[2025-09-12 18:35:15,124] INFO: Loaded 16 players from duckhunt.json -[2025-09-12 18:35:15,124] INFO: DuckHunt Bot initializing... -[2025-09-12 18:35:15,124] INFO: Starting DuckHunt Bot... -[2025-09-12 18:35:15,125] INFO: Loaded 16 players from duckhunt.json -[2025-09-12 18:35:15,147] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 18:35:15,277] INFO: Connected successfully! -[2025-09-12 18:35:15,277] INFO: SASL authentication enabled -[2025-09-12 18:35:15,289] INFO: SASL capability available -[2025-09-12 18:35:15,295] INFO: SASL capability acknowledged -[2025-09-12 18:35:15,295] INFO: Authenticating via SASL as duckhunt -[2025-09-12 18:35:15,662] INFO: Server ready for SASL authentication -[2025-09-12 18:35:15,663] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found) -[2025-09-12 18:35:15,663] INFO: Falling back to NickServ identification... -[2025-09-12 18:35:15,663] ERROR: Attempted username: duckhunt -[2025-09-12 18:35:15,663] INFO: Registering as DuckHunt -[2025-09-12 18:35:15,676] ERROR: SASL authentication aborted! (906) -[2025-09-12 18:35:15,677] INFO: Falling back to NickServ identification... -[2025-09-12 18:35:15,677] INFO: Registering as DuckHunt -[2025-09-12 18:35:15,677] INFO: Successfully registered! -[2025-09-12 18:35:15,677] INFO: Attempting NickServ identification for duckhunt -[2025-09-12 18:35:16,679] INFO: NickServ identification commands sent -[2025-09-12 18:35:16,679] INFO: Joining #computertech -[2025-09-12 18:35:16,686] INFO: Successfully joined #computertech -[2025-09-12 18:35:17,281] INFO: Starting duck spawning... -[2025-09-12 18:35:22,283] INFO: Admin spawned normal duck 76766050 in #computertech -[2025-09-12 18:35:22,283] INFO: Waiting 41m 31s for next duck -[2025-09-12 18:35:37,207] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 18:35:37,308] INFO: Duck spawning stopped due to shutdown request -[2025-09-12 18:35:37,308] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 18:35:37,309] INFO: Starting cleanup process... -[2025-09-12 18:35:38,414] INFO: IRC connection closed -[2025-09-12 18:35:38,420] INFO: Final database save completed - 17 players saved -[2025-09-12 18:35:38,420] INFO: Cleanup completed successfully -[2025-09-12 18:35:38,420] INFO: DuckHunt Bot shutdown complete -[2025-09-12 18:37:36,458] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 18:37:36,458] INFO: DuckHunt Bot initializing... -[2025-09-12 18:37:36,458] INFO: Starting DuckHunt Bot... -[2025-09-12 18:37:36,458] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 18:37:36,481] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 18:37:36,589] INFO: Connected successfully! -[2025-09-12 18:37:36,589] INFO: SASL authentication enabled -[2025-09-12 18:37:36,601] INFO: SASL capability available -[2025-09-12 18:37:36,608] INFO: SASL capability acknowledged -[2025-09-12 18:37:36,608] INFO: Authenticating via SASL as duckhunt -[2025-09-12 18:37:36,975] INFO: Server ready for SASL authentication -[2025-09-12 18:37:36,976] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found) -[2025-09-12 18:37:36,976] INFO: Falling back to NickServ identification... -[2025-09-12 18:37:36,976] ERROR: Attempted username: duckhunt -[2025-09-12 18:37:36,976] INFO: Registering as DuckHunt -[2025-09-12 18:37:36,990] ERROR: SASL authentication aborted! (906) -[2025-09-12 18:37:36,990] INFO: Falling back to NickServ identification... -[2025-09-12 18:37:36,990] INFO: Registering as DuckHunt -[2025-09-12 18:37:36,991] INFO: Successfully registered! -[2025-09-12 18:37:36,991] INFO: Attempting NickServ identification for duckhunt -[2025-09-12 18:37:37,770] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 18:37:37,993] INFO: NickServ identification commands sent -[2025-09-12 18:37:37,994] INFO: Joining #computertech -[2025-09-12 18:37:37,994] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 18:37:37,994] INFO: Starting cleanup process... -[2025-09-12 18:37:38,999] INFO: IRC connection closed -[2025-09-12 18:37:39,004] INFO: Final database save completed - 17 players saved -[2025-09-12 18:37:39,004] INFO: Cleanup completed successfully -[2025-09-12 18:37:39,005] INFO: DuckHunt Bot shutdown complete -[2025-09-12 19:48:20,215] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 19:50:01,514] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 19:50:01,514] INFO: DuckHunt Bot initializing... -[2025-09-12 19:50:01,515] INFO: Starting DuckHunt Bot... -[2025-09-12 19:50:01,516] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 19:50:01,587] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 19:50:01,776] INFO: Connected successfully! -[2025-09-12 19:50:02,946] INFO: Registering as DuckHunt -[2025-09-12 19:50:03,077] INFO: Successfully registered! (SASL authenticated) -[2025-09-12 19:50:03,078] INFO: Joining #colby -[2025-09-12 19:50:03,146] INFO: Successfully joined #colby -[2025-09-12 19:50:03,780] INFO: Starting duck spawning... -[2025-09-12 19:50:08,782] INFO: Admin spawned normal duck 89470db7 in #colby -[2025-09-12 19:50:08,783] INFO: Waiting 72m 24s for next duck -[2025-09-12 19:50:35,497] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 19:50:35,810] INFO: Duck spawning stopped due to shutdown request -[2025-09-12 19:50:35,810] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 19:50:35,811] INFO: Starting cleanup process... -[2025-09-12 19:50:36,916] INFO: IRC connection closed -[2025-09-12 19:50:36,922] INFO: Final database save completed - 17 players saved -[2025-09-12 19:50:36,922] INFO: Cleanup completed successfully -[2025-09-12 19:50:36,925] INFO: DuckHunt Bot shutdown complete -[2025-09-12 19:50:38,060] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 19:50:38,060] INFO: DuckHunt Bot initializing... -[2025-09-12 19:50:38,061] INFO: Starting DuckHunt Bot... -[2025-09-12 19:50:38,062] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 19:50:38,126] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 19:50:38,317] INFO: Connected successfully! -[2025-09-12 19:50:39,335] INFO: Registering as DuckHunt -[2025-09-12 19:50:39,456] INFO: Successfully registered! (SASL authenticated) -[2025-09-12 19:50:39,456] INFO: Joining #colby -[2025-09-12 19:50:39,519] INFO: Successfully joined #colby -[2025-09-12 19:50:40,323] INFO: Starting duck spawning... -[2025-09-12 19:50:45,326] INFO: Admin spawned normal duck 1399472e in #colby -[2025-09-12 19:50:45,326] INFO: Waiting 89m 7s for next duck -[2025-09-12 19:50:58,428] INFO: Admin spawned normal duck 7278ccc5 in #colby -[2025-09-12 19:51:03,718] INFO: Admin spawned normal duck b79d4c60 in #colby -[2025-09-12 19:51:04,973] INFO: Admin spawned normal duck f16535b2 in #colby -[2025-09-12 19:51:06,296] INFO: Admin spawned normal duck 287c11e5 in #colby -[2025-09-12 19:51:07,607] INFO: Admin spawned normal duck 87d9f58d in #colby -[2025-09-12 19:55:27,299] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 19:55:27,760] INFO: Duck spawning stopped due to shutdown request -[2025-09-12 19:55:27,764] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 19:55:27,766] INFO: Starting cleanup process... -[2025-09-12 19:55:28,894] INFO: IRC connection closed -[2025-09-12 19:55:28,907] INFO: Final database save completed - 17 players saved -[2025-09-12 19:55:28,908] INFO: Cleanup completed successfully -[2025-09-12 19:55:28,925] INFO: DuckHunt Bot shutdown complete -[2025-09-12 20:09:21,565] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:09:21,568] INFO: DuckHunt Bot initializing... -[2025-09-12 20:09:21,568] INFO: Starting DuckHunt Bot... -[2025-09-12 20:09:21,569] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:09:21,648] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 20:09:21,817] INFO: Connected successfully! -[2025-09-12 20:09:22,784] INFO: Registering as DuckHunt -[2025-09-12 20:09:22,880] INFO: Successfully registered! (SASL authenticated) -[2025-09-12 20:09:22,881] INFO: Joining #computertech -[2025-09-12 20:09:22,937] INFO: Successfully joined #computertech -[2025-09-12 20:09:23,822] INFO: Starting duck spawning... -[2025-09-12 20:09:28,824] INFO: Admin spawned normal duck 3d18761a in #computertech -[2025-09-12 20:09:28,825] INFO: Waiting 43m 4s for next duck -[2025-09-12 20:10:31,986] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 20:10:32,701] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 20:10:32,702] INFO: Starting cleanup process... -[2025-09-12 20:10:33,810] INFO: IRC connection closed -[2025-09-12 20:10:33,819] INFO: Final database save completed - 17 players saved -[2025-09-12 20:10:33,819] INFO: Cleanup completed successfully -[2025-09-12 20:10:33,823] INFO: DuckHunt Bot shutdown complete -[2025-09-12 20:10:34,241] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:10:34,242] INFO: DuckHunt Bot initializing... -[2025-09-12 20:10:34,243] INFO: Starting DuckHunt Bot... -[2025-09-12 20:10:34,244] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:10:34,365] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 20:10:34,541] INFO: Connected successfully! -[2025-09-12 20:10:36,475] INFO: Registering as DuckHunt -[2025-09-12 20:10:36,594] INFO: Successfully registered! (SASL authenticated) -[2025-09-12 20:10:36,597] INFO: Joining #computertech -[2025-09-12 20:10:36,665] INFO: Successfully joined #computertech -[2025-09-12 20:10:37,545] INFO: Starting duck spawning... -[2025-09-12 20:10:42,548] INFO: Admin spawned normal duck 11290de8 in #computertech -[2025-09-12 20:10:42,549] INFO: Waiting 74m 3s for next duck -[2025-09-12 20:10:53,126] WARNING: A main task completed unexpectedly -[2025-09-12 20:10:53,127] INFO: Starting cleanup process... -[2025-09-12 20:10:54,233] INFO: IRC connection closed -[2025-09-12 20:10:54,244] INFO: Final database save completed - 17 players saved -[2025-09-12 20:10:54,245] INFO: Cleanup completed successfully -[2025-09-12 20:10:54,248] INFO: DuckHunt Bot shutdown complete -[2025-09-12 20:11:53,511] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:11:53,512] INFO: DuckHunt Bot initializing... -[2025-09-12 20:11:53,512] INFO: Starting DuckHunt Bot... -[2025-09-12 20:11:53,513] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:11:53,599] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 20:11:53,797] INFO: Connected successfully! -[2025-09-12 20:11:54,764] INFO: Registering as DuckHunt -[2025-09-12 20:11:54,865] INFO: Successfully registered! (SASL authenticated) -[2025-09-12 20:11:54,865] INFO: Joining #computertech -[2025-09-12 20:11:54,914] INFO: Successfully joined #computertech -[2025-09-12 20:11:55,800] INFO: Starting duck spawning... -[2025-09-12 20:12:00,802] INFO: Admin spawned normal duck cefbb956 in #computertech -[2025-09-12 20:12:00,803] INFO: Waiting 32m 48s for next duck -[2025-09-12 20:12:01,603] INFO: Admin spawned normal duck e19381c2 in #computertech -[2025-09-12 20:12:08,171] INFO: Admin spawned normal duck 642daf60 in #computertech -[2025-09-12 20:12:08,705] INFO: Admin spawned normal duck deb6cc88 in #computertech -[2025-09-12 20:12:32,287] INFO: Admin spawned golden duck f5c388c1 in #computertech -[2025-09-12 20:17:29,215] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 20:17:29,253] INFO: Duck spawning stopped due to shutdown request -[2025-09-12 20:17:29,254] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 20:17:29,254] INFO: Starting cleanup process... -[2025-09-12 20:17:30,361] INFO: IRC connection closed -[2025-09-12 20:17:30,368] INFO: Final database save completed - 17 players saved -[2025-09-12 20:17:30,369] INFO: Cleanup completed successfully -[2025-09-12 20:17:30,372] INFO: DuckHunt Bot shutdown complete -[2025-09-12 20:17:31,507] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:17:31,508] INFO: DuckHunt Bot initializing... -[2025-09-12 20:17:31,509] INFO: Starting DuckHunt Bot... -[2025-09-12 20:17:31,511] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:17:31,600] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 20:17:31,788] INFO: Connected successfully! -[2025-09-12 20:17:32,919] INFO: Registering as DuckHunt -[2025-09-12 20:17:33,047] INFO: Successfully registered! (SASL authenticated) -[2025-09-12 20:17:33,048] INFO: Joining #computertech -[2025-09-12 20:17:33,116] INFO: Successfully joined #computertech -[2025-09-12 20:17:33,792] INFO: Starting duck spawning... -[2025-09-12 20:17:38,793] INFO: Admin spawned normal duck fdc26682 in #computertech -[2025-09-12 20:17:38,794] INFO: Waiting 77m 31s for next duck -[2025-09-12 20:18:12,540] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 20:18:12,858] INFO: Duck spawning stopped due to shutdown request -[2025-09-12 20:18:12,859] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 20:18:12,859] INFO: Starting cleanup process... -[2025-09-12 20:18:13,964] INFO: IRC connection closed -[2025-09-12 20:18:13,970] INFO: Final database save completed - 17 players saved -[2025-09-12 20:18:13,971] INFO: Cleanup completed successfully -[2025-09-12 20:18:13,975] INFO: DuckHunt Bot shutdown complete -[2025-09-12 20:18:14,525] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:18:14,527] INFO: DuckHunt Bot initializing... -[2025-09-12 20:18:14,528] INFO: Starting DuckHunt Bot... -[2025-09-12 20:18:14,531] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:18:14,636] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 20:18:14,805] INFO: Connected successfully! -[2025-09-12 20:18:15,768] INFO: Registering as DuckHunt -[2025-09-12 20:18:15,871] INFO: Successfully registered! (SASL authenticated) -[2025-09-12 20:18:15,873] INFO: Joining #computertech -[2025-09-12 20:18:15,929] INFO: Successfully joined #computertech -[2025-09-12 20:18:16,123] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 20:18:16,129] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 20:18:16,147] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 20:18:16,148] INFO: Starting cleanup process... -[2025-09-12 20:18:17,115] INFO: Received SIGTERM, initiating graceful shutdown... -[2025-09-12 20:18:17,268] INFO: IRC connection closed -[2025-09-12 20:18:17,276] INFO: Final database save completed - 17 players saved -[2025-09-12 20:18:17,276] INFO: Cleanup completed successfully -[2025-09-12 20:18:17,290] INFO: DuckHunt Bot shutdown complete -[2025-09-12 20:18:23,251] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:18:23,252] INFO: DuckHunt Bot initializing... -[2025-09-12 20:18:23,253] INFO: Starting DuckHunt Bot... -[2025-09-12 20:18:23,255] INFO: Loaded 17 players from duckhunt.json -[2025-09-12 20:18:23,349] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 20:18:23,549] INFO: Connected successfully! -[2025-09-12 20:18:24,557] INFO: Registering as DuckHunt -[2025-09-12 20:18:24,676] INFO: Successfully registered! (SASL authenticated) -[2025-09-12 20:18:24,677] INFO: Joining #computertech -[2025-09-12 20:18:24,736] INFO: Successfully joined #computertech -[2025-09-12 20:18:25,554] INFO: Starting duck spawning... -[2025-09-12 20:18:30,556] INFO: Admin spawned normal duck 083638bd in #computertech -[2025-09-12 20:18:30,557] INFO: Waiting 38m 8s for next duck -[2025-09-12 20:18:43,399] INFO: Admin spawned normal duck 777c5080 in #computertech -[2025-09-12 20:18:43,910] INFO: Admin spawned normal duck 0e36368a in #computertech -[2025-09-12 20:18:44,307] INFO: Admin spawned normal duck 0dbf0209 in #computertech -[2025-09-12 20:18:44,693] INFO: Admin spawned normal duck 224f9870 in #computertech -[2025-09-12 20:18:45,055] INFO: Admin spawned normal duck 96c7a18d in #computertech -[2025-09-12 20:18:45,659] INFO: Admin spawned normal duck f881fa3a in #computertech -[2025-09-12 20:29:51,955] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 20:29:52,036] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 20:29:52,036] INFO: Starting cleanup process... -[2025-09-12 20:29:53,159] INFO: IRC connection closed -[2025-09-12 20:29:53,169] INFO: Final database save completed - 18 players saved -[2025-09-12 20:29:53,170] INFO: Cleanup completed successfully -[2025-09-12 20:29:53,181] INFO: DuckHunt Bot shutdown complete -[2025-09-12 20:29:54,600] INFO: Loaded 18 players from duckhunt.json -[2025-09-12 20:29:54,601] INFO: DuckHunt Bot initializing... -[2025-09-12 20:29:54,601] INFO: Starting DuckHunt Bot... -[2025-09-12 20:29:54,604] INFO: Loaded 18 players from duckhunt.json -[2025-09-12 20:29:54,699] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 20:29:54,874] INFO: Connected successfully! -[2025-09-12 20:29:55,842] INFO: Registering as DuckHunt -[2025-09-12 20:29:55,941] INFO: Successfully registered! (SASL authenticated) -[2025-09-12 20:29:55,941] INFO: Joining #computertech -[2025-09-12 20:29:55,991] INFO: Successfully joined #computertech -[2025-09-12 20:29:56,877] INFO: Starting duck spawning... -[2025-09-12 20:30:01,878] INFO: Admin spawned golden duck f7373d01 in #computertech -[2025-09-12 20:30:01,878] INFO: Waiting 49m 50s for next duck -[2025-09-12 20:31:39,076] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 20:31:39,717] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 20:31:39,718] INFO: Starting cleanup process... -[2025-09-12 20:31:40,827] INFO: IRC connection closed -[2025-09-12 20:31:40,834] INFO: Final database save completed - 19 players saved -[2025-09-12 20:31:40,835] INFO: Cleanup completed successfully -[2025-09-12 20:31:40,838] INFO: DuckHunt Bot shutdown complete -[2025-09-12 20:31:42,386] INFO: Loaded 19 players from duckhunt.json -[2025-09-12 20:31:42,387] INFO: DuckHunt Bot initializing... -[2025-09-12 20:31:42,388] INFO: Starting DuckHunt Bot... -[2025-09-12 20:31:42,389] INFO: Loaded 19 players from duckhunt.json -[2025-09-12 20:31:42,504] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 20:31:42,737] INFO: Connected successfully! -[2025-09-12 20:31:43,914] INFO: Registering as DuckHunt -[2025-09-12 20:31:44,034] INFO: Successfully registered! (SASL authenticated) -[2025-09-12 20:31:44,034] INFO: Joining #computertech -[2025-09-12 20:31:44,097] INFO: Successfully joined #computertech -[2025-09-12 20:31:44,744] INFO: Starting duck spawning... -[2025-09-12 20:31:49,747] INFO: Admin spawned normal duck 30bc917d in #computertech -[2025-09-12 20:31:49,748] INFO: Waiting 44m 50s for next duck -[2025-09-12 20:45:42,441] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 20:45:42,988] INFO: Duck spawning stopped due to shutdown request -[2025-09-12 20:45:42,993] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 20:45:43,000] INFO: Starting cleanup process... -[2025-09-12 20:45:44,197] INFO: IRC connection closed -[2025-09-12 20:45:44,234] INFO: Final database save completed - 19 players saved -[2025-09-12 20:45:44,235] INFO: Cleanup completed successfully -[2025-09-12 20:45:44,316] INFO: DuckHunt Bot shutdown complete -[2025-09-12 20:57:05,952] INFO: Loaded 19 players from duckhunt.json -[2025-09-12 20:57:05,954] INFO: DuckHunt Bot initializing... -[2025-09-12 20:57:05,955] INFO: Starting DuckHunt Bot... -[2025-09-12 20:57:05,955] INFO: Loaded 19 players from duckhunt.json -[2025-09-12 20:57:06,019] INFO: Connecting to irc.rizon.net:6697 (SSL: True) -[2025-09-12 20:57:06,197] INFO: Connected successfully! -[2025-09-12 20:57:07,157] INFO: Registering as DuckHunt -[2025-09-12 20:57:07,256] INFO: Successfully registered! (SASL authenticated) -[2025-09-12 20:57:07,256] INFO: Joining #computertech -[2025-09-12 20:57:07,307] INFO: Successfully joined #computertech -[2025-09-12 20:57:08,199] INFO: Starting duck spawning... -[2025-09-12 20:57:13,200] INFO: Admin spawned normal duck 77291a83 in #computertech -[2025-09-12 20:57:13,200] INFO: Waiting 49m 42s for next duck -[2025-09-12 20:57:25,869] INFO: Admin spawned normal duck 4a2ff363 in #computertech -[2025-09-12 20:57:26,569] INFO: Admin spawned normal duck f2ae2d52 in #computertech -[2025-09-12 20:57:39,277] INFO: Admin spawned golden duck 3ef1281f in #computertech -[2025-09-12 20:59:05,695] INFO: Received SIGINT, initiating graceful shutdown... -[2025-09-12 20:59:06,333] INFO: Duck spawning stopped due to shutdown request -[2025-09-12 20:59:06,334] INFO: Shutdown requested, stopping all tasks... -[2025-09-12 20:59:06,334] INFO: Starting cleanup process... -[2025-09-12 20:59:07,437] INFO: IRC connection closed -[2025-09-12 20:59:07,442] INFO: Final database save completed - 19 players saved -[2025-09-12 20:59:07,443] INFO: Cleanup completed successfully -[2025-09-12 20:59:07,445] INFO: DuckHunt Bot shutdown complete -2025-09-13 13:06:23,192 [INFO ] DuckHuntBot - setup_logger:78: Enhanced logging system initialized with file rotation -2025-09-13 13:06:23,212 [INFO ] DuckHuntBot - load_database:401: Loaded 19 players from duckhunt.json -2025-09-13 13:07:06,713 [INFO ] DuckHuntBot - setup_logger:79: Enhanced logging system initialized with file rotation -2025-09-13 13:07:06,715 [INFO ] DuckHuntBot - load_database:402: Loaded 19 players from duckhunt.json -2025-09-13 13:07:20,053 [INFO ] DuckHuntBot - setup_logger:79: Enhanced logging system initialized with file rotation -2025-09-13 13:07:20,054 [INFO ] DuckHuntBot - load_database:402: Loaded 19 players from duckhunt.json -2025-09-13 13:17:14,822 [INFO ] DuckHuntBot - setup_logger:79: Enhanced logging system initialized with file rotation -2025-09-13 13:17:14,824 [INFO ] DuckHuntBot - load_database:402: Loaded 19 players from duckhunt.json -2025-09-13 13:22:07,954 [INFO ] DuckHuntBot - setup_logger:79: Enhanced logging system initialized with file rotation -2025-09-13 13:22:07,956 [INFO ] DuckHuntBot - load_database:402: Loaded 19 players from duckhunt.json -2025-09-13 14:45:32,890 [INFO ] DuckHuntBot - setup_logger:79: Enhanced logging system initialized with file rotation -2025-09-13 14:45:32,893 [INFO ] DuckHuntBot - load_database:402: Loaded 19 players from duckhunt.json -2025-09-13 14:47:29,566 [INFO ] DuckHuntBot - setup_logger:79: Enhanced logging system initialized with file rotation -2025-09-13 14:47:29,568 [INFO ] DuckHuntBot - load_database:403: Loaded 21 players from duckhunt.json -2025-09-13 15:01:52,979 [INFO ] DuckHuntBot - setup_logger:79: Enhanced logging system initialized with file rotation -2025-09-13 15:01:52,981 [INFO ] DuckHuntBot - load_database:402: Loaded 21 players from duckhunt.json diff --git a/duckhunt/simple_duckhunt.py.backup b/duckhunt/simple_duckhunt.py.backup deleted file mode 100644 index ebcf13e..0000000 --- a/duckhunt/simple_duckhunt.py.backup +++ /dev/null @@ -1,2690 +0,0 @@ -#!/usr/bin/env python3 -""" -Standalone DuckHunt IRC Bot with JSON Database Storage -""" - -import asyncio -import ssl -import json -import random -import logging -import logging.handlers -import sys -import os -import base64 -import subprocess -import time -import uuid -import signal -import traceback -import re -from functools import partial -from typing import Optional, Dict, Any - -# Import SASL handler -from src.sasl import SASLHandler - -# Enhanced logger with detailed formatting -class DetailedColorFormatter(logging.Formatter): - COLORS = { - 'DEBUG': '\033[94m', # Blue - 'INFO': '\033[92m', # Green - 'WARNING': '\033[93m', # Yellow - 'ERROR': '\033[91m', # Red - 'CRITICAL': '\033[95m', # Magenta - } - - def format(self, record): - color = self.COLORS.get(record.levelname, '') - endc = self.COLORS['ENDC'] - msg = super().format(record) - return f"{color}{msg}{endc}" - -class DetailedFileFormatter(logging.Formatter): - """File formatter with extra context but no colors""" - def format(self, record): - return super().format(record) - -def setup_logger(): - logger = logging.getLogger('DuckHuntBot') - logger.setLevel(logging.DEBUG) - - # Clear any existing handlers - logger.handlers.clear() - - # Console handler with colors - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - console_formatter = DetailedColorFormatter( - '%(asctime)s [%(levelname)s] %(name)s: %(message)s' - ) - console_handler.setFormatter(console_formatter) - logger.addHandler(console_handler) - - # File handler with rotation for detailed logs - try: - file_handler = logging.handlers.RotatingFileHandler( - 'duckhunt.log', - maxBytes=10*1024*1024, # 10MB per file - backupCount=5 # Keep 5 backup files - ) - file_handler.setLevel(logging.DEBUG) - file_formatter = DetailedFileFormatter( - '%(asctime)s [%(levelname)-8s] %(name)s - %(funcName)s:%(lineno)d: %(message)s' - ) - file_handler.setFormatter(file_formatter) - logger.addHandler(file_handler) - - logger.info("Enhanced logging system initialized with file rotation") - except Exception as e: - logger.error(f"Failed to setup file logging: {e}") - - return logger - -# Input validation functions -class InputValidator: - @staticmethod - def validate_nickname(nick: str) -> bool: - """Validate IRC nickname format""" - if not nick or len(nick) > 30: - return False - # RFC 2812 nickname pattern - pattern = r'^[a-zA-Z\[\]\\`_^{|}][a-zA-Z0-9\[\]\\`_^{|}\-]*$' - return bool(re.match(pattern, nick)) - - @staticmethod - def validate_channel(channel: str) -> bool: - """Validate IRC channel format""" - if not channel or len(channel) > 50: - return False - return channel.startswith('#') and ' ' not in channel - - @staticmethod - def validate_numeric_input(value: str, min_val: Optional[int] = None, max_val: Optional[int] = None) -> Optional[int]: - """Safely parse and validate numeric input""" - try: - num = int(value) - if min_val is not None and num < min_val: - return None - if max_val is not None and num > max_val: - return None - return num - except (ValueError, TypeError): - return None - - @staticmethod - def sanitize_message(message: str) -> str: - """Sanitize user input message""" - if not message: - return "" - # Remove control characters and limit length - sanitized = ''.join(char for char in message if ord(char) >= 32 or char in '\t\n') - return sanitized[:500] # Limit message length - -# Simple IRC message parser -def parse_message(line): - prefix = '' - trailing = '' - if line.startswith(':'): - prefix, line = line[1:].split(' ', 1) - if ' :' in line: - line, trailing = line.split(' :', 1) - parts = line.split() - command = parts[0] if parts else '' - params = parts[1:] if len(parts) > 1 else [] - return prefix, command, params, trailing - -class SimpleIRCBot: - def __init__(self, config): - self.config = config - self.logger = setup_logger() - self.reader: Optional[asyncio.StreamReader] = None - self.writer: Optional[asyncio.StreamWriter] = None - self.registered = False - self.channels_joined = set() - self.players = {} # Memory cache for speed - self.ducks = {} # Format: {channel: [{'alive': True, 'spawn_time': time, 'id': uuid}, ...]} - self.db_file = "duckhunt.json" - self.admins = [admin.lower() for admin in self.config.get('admins', ['colby'])] # Load from config only, case insensitive - self.ignored_nicks = set() # Nicks to ignore commands from - self.command_cooldowns = {} # Rate limiting for commands - self.duck_timeout_min = self.config.get('duck_timeout_min', 45) # Minimum duck timeout - self.duck_timeout_max = self.config.get('duck_timeout_max', 75) # Maximum duck timeout - self.duck_spawn_min = self.config.get('duck_spawn_min', 1800) # Minimum duck spawn time (30 min) - self.duck_spawn_max = self.config.get('duck_spawn_max', 5400) # Maximum duck spawn time (90 min) - self.shutdown_requested = False # Graceful shutdown flag - self.running_tasks = set() # Track running tasks for cleanup - - # Duck intelligence and records tracking - self.channel_records = {} # Channel-specific records {channel: {'fastest_shot': {}, 'last_duck': {}, 'total_ducks': 0}} - self.duck_difficulty = {} # Per-channel duck difficulty {channel: multiplier} - self.next_duck_spawn = {} # Track next spawn time per channel - - # Initialize SASL handler - self.sasl_handler = SASLHandler(self, config) - - # IRC Color codes - self.colors = { - 'red': '\x0304', - 'green': '\x0303', - 'blue': '\x0302', - 'yellow': '\x0308', - 'orange': '\x0307', - 'purple': '\x0306', - 'cyan': '\x0311', - 'white': '\x0300', - 'black': '\x0301', - 'gray': '\x0314', - 'reset': '\x03' - } - - # 40-level progression system with titles - self.levels = [ - {'xp': 0, 'title': 'Duck Harasser'}, - {'xp': 10, 'title': 'Unemployed'}, - {'xp': 25, 'title': 'Hunter Apprentice'}, - {'xp': 45, 'title': 'Duck Tracker'}, - {'xp': 70, 'title': 'Sharp Shooter'}, - {'xp': 100, 'title': 'Hunter'}, - {'xp': 135, 'title': 'Experienced Hunter'}, - {'xp': 175, 'title': 'Skilled Hunter'}, - {'xp': 220, 'title': 'Expert Hunter'}, - {'xp': 270, 'title': 'Master Hunter'}, - {'xp': 325, 'title': 'Duck Slayer'}, - {'xp': 385, 'title': 'Duck Terminator'}, - {'xp': 450, 'title': 'Duck Destroyer'}, - {'xp': 520, 'title': 'Duck Exterminator'}, - {'xp': 595, 'title': 'Duck Assassin'}, - {'xp': 675, 'title': 'Legendary Hunter'}, - {'xp': 760, 'title': 'Elite Hunter'}, - {'xp': 850, 'title': 'Supreme Hunter'}, - {'xp': 945, 'title': 'Ultimate Hunter'}, - {'xp': 1045, 'title': 'Godlike Hunter'}, - {'xp': 1150, 'title': 'Duck Nightmare'}, - {'xp': 1260, 'title': 'Duck Executioner'}, - {'xp': 1375, 'title': 'Duck Eliminator'}, - {'xp': 1495, 'title': 'Duck Obliterator'}, - {'xp': 1620, 'title': 'Duck Annihilator'}, - {'xp': 1750, 'title': 'Duck Devastator'}, - {'xp': 1885, 'title': 'Duck Vanquisher'}, - {'xp': 2025, 'title': 'Duck Conqueror'}, - {'xp': 2170, 'title': 'Duck Dominator'}, - {'xp': 2320, 'title': 'Duck Emperor'}, - {'xp': 2475, 'title': 'Duck Overlord'}, - {'xp': 2635, 'title': 'Duck Deity'}, - {'xp': 2800, 'title': 'Duck God'}, - {'xp': 2970, 'title': 'Duck Nemesis'}, - {'xp': 3145, 'title': 'Duck Apocalypse'}, - {'xp': 3325, 'title': 'Duck Armageddon'}, - {'xp': 3510, 'title': 'Duck Ragnarok'}, - {'xp': 3700, 'title': 'Duck Cataclysm'}, - {'xp': 3895, 'title': 'Duck Holocaust'}, - {'xp': 4095, 'title': 'Duck Genesis'} - ] - - # Sleep hours configuration (when ducks don't spawn) - self.sleep_hours = self.config.get('sleep_hours', []) # Format: [[22, 30], [8, 0]] for 22:30 to 08:00 - - # Duck planning system - self.daily_duck_plan = {} # Format: {channel: [(hour, minute), ...]} - - # Karma system - self.karma_events = ['teamkill', 'miss', 'wild_shot', 'hit', 'golden_hit'] - - self.load_database() - - def get_config(self, path, default=None): - """Get nested configuration value with fallback to default""" - keys = path.split('.') - value = self.config - for key in keys: - if isinstance(value, dict) and key in value: - value = value[key] - else: - return default - return value - - async def attempt_nickserv_auth(self): - """Delegate to SASL handler for NickServ auth""" - # For simple bot, we'll implement NickServ auth here - sasl_config = self.config.get('sasl', {}) - username = sasl_config.get('username', '') - password = sasl_config.get('password', '') - - if username and password: - self.logger.info(f"Attempting NickServ identification for {username}") - # Try both common NickServ commands - self.send_raw(f'PRIVMSG NickServ :IDENTIFY {username} {password}') - # Some networks use just the password if nick matches - await asyncio.sleep(1) - self.send_raw(f'PRIVMSG NickServ :IDENTIFY {password}') - self.logger.info("NickServ identification commands sent") - else: - self.logger.debug("No SASL credentials available for NickServ fallback") - - async def handle_nickserv_response(self, message): - """Handle responses from NickServ""" - message_lower = message.lower() - - if any(phrase in message_lower for phrase in [ - 'you are now identified', 'password accepted', 'you are already identified', - 'authentication successful', 'you have been identified' - ]): - self.logger.info("NickServ identification successful!") - - elif any(phrase in message_lower for phrase in [ - 'invalid password', 'incorrect password', 'access denied', - 'authentication failed', 'not registered', 'nickname is not registered' - ]): - self.logger.error(f"NickServ identification failed: {message}") - - else: - self.logger.debug(f"NickServ message: {message}") - - def get_player_level(self, xp): - """Get player level and title based on XP""" - for i in range(len(self.levels) - 1, -1, -1): - if xp >= self.levels[i]['xp']: - return i + 1, self.levels[i]['title'] - return 1, self.levels[0]['title'] - - def get_xp_for_next_level(self, xp): - """Get XP needed for next level""" - level, _ = self.get_player_level(xp) - if level < len(self.levels): - return self.levels[level]['xp'] - xp - return 0 # Max level reached - - def calculate_penalty_by_level(self, base_penalty, xp): - """Calculate penalty based on player level""" - level, _ = self.get_player_level(xp) - # Higher levels get higher penalties - return base_penalty + (level - 1) * 0.5 - - def update_karma(self, player, event): - """Update player karma based on event""" - if 'karma' not in player: - player['karma'] = 0 - - karma_changes = { - 'hit': 2, - 'golden_hit': 5, - 'teamkill': -10, - 'wild_shot': -3, - 'miss': -1 - } - - player['karma'] += karma_changes.get(event, 0) - - def is_sleep_time(self): - """Check if current time is within sleep hours""" - if not self.sleep_hours: - return False - - import datetime - now = datetime.datetime.now() - current_time = now.hour * 60 + now.minute - - for sleep_start, sleep_end in self.sleep_hours: - start_minutes = sleep_start[0] * 60 + sleep_start[1] - end_minutes = sleep_end[0] * 60 + sleep_end[1] - - if start_minutes <= end_minutes: # Same day - if start_minutes <= current_time <= end_minutes: - return True - else: # Crosses midnight - if current_time >= start_minutes or current_time <= end_minutes: - return True - return False - - def calculate_gun_reliability(self, player): - """Calculate gun reliability percentage""" - base_reliability = player.get('reliability', 70) - grease_bonus = 10 if player.get('grease', 0) > 0 else 0 - brush_bonus = 5 if player.get('brush', 0) > 0 else 0 - return min(base_reliability + grease_bonus + brush_bonus, 95) - - def gun_jams(self, player): - """Check if gun jams when firing""" - reliability = self.calculate_gun_reliability(player) - return random.randint(1, 100) > reliability - - async def scare_other_ducks(self, channel, shot_duck_id): - """Successful shots can scare other ducks away""" - if not self.config.get('successful_shots_scare_ducks', True): - return - - channel_ducks = self.ducks.get(channel, []) - for duck in channel_ducks: - if duck.get('alive') and duck['id'] != shot_duck_id: - # 30% chance to scare each remaining duck - if random.randint(1, 100) <= 30: - duck['scared'] = True - duck['alive'] = False - - async def scare_duck_on_miss(self, channel, target_duck): - """Duck may be scared by missed shots""" - if target_duck.get('hit_attempts', 0) >= 2: # Duck gets scared after 2+ attempts - if random.randint(1, 100) <= 40: # 40% chance to scare - target_duck['scared'] = True - target_duck['alive'] = False - self.send_message(channel, f"The duck got scared and flew away! (\\_o<) *flap flap*") - - async def find_bushes_items(self, nick, channel, player): - """Find items in bushes after killing a duck""" - if random.randint(1, 100) <= 12: # 12% chance to find something - found_items = [ - "Handful of sand", "Water bucket", "Four-leaf clover", "Mirror", - "Grease", "Brush for gun", "Spare clothes", "Sunglasses", - "Piece of bread", "Life insurance" - ] - found_item = random.choice(found_items) - - # Add item to player inventory - item_key = found_item.lower().replace(' ', '_').replace("'", "") - if 'four_leaf_clover' in item_key: - item_key = 'luck' - player['luck'] = player.get('luck', 0) + 1 - elif item_key in player: - player[item_key] = player.get(item_key, 0) + 1 - - self.send_message(channel, f"{nick} > {self.colors['cyan']}You found {found_item} in the bushes!{self.colors['reset']}") - self.save_player(f"{nick}!user@host") # Save player data - - def load_database(self): - """Load player data from JSON file""" - if os.path.exists(self.db_file): - try: - with open(self.db_file, 'r') as f: - data = json.load(f) - self.players = data.get('players', {}) - self.logger.info(f"Loaded {len(self.players)} players from {self.db_file}") - except (json.JSONDecodeError, IOError) as e: - self.logger.error(f"Error loading database: {e}") - self.players = {} - else: - self.players = {} - self.logger.info(f"Created new database: {self.db_file}") - - def save_database(self): - """Save all player data to JSON file with error handling""" - try: - # Atomic write to prevent corruption - temp_file = f"{self.db_file}.tmp" - data = { - 'players': self.players, - 'last_save': str(time.time()) - } - with open(temp_file, 'w') as f: - json.dump(data, f, indent=2) - - # Atomic rename to replace old file - import os - os.replace(temp_file, self.db_file) - - except IOError as e: - self.logger.error(f"Error saving database: {e}") - except Exception as e: - self.logger.error(f"Unexpected database save error: {e}") - - def is_admin(self, user): - """Check if user is admin by nick only""" - if '!' not in user: - return False - nick = user.split('!')[0].lower() - return nick in self.admins - - async def send_user_message(self, nick, channel, message, message_type='default'): - """Send message to user respecting their output mode preferences and config overrides""" - player = self.get_player(f"{nick}!*@*") - - # Check if this message type should be forced to public - force_public_key = f'message_output.force_public.{message_type}' - if self.get_config(force_public_key, False): - self.send_message(channel, message) - return - - # Default to config setting if player not found or no settings - default_mode = self.get_config('message_output.default_user_mode', 'PUBLIC') - output_mode = default_mode - if player and 'settings' in player: - output_mode = player['settings'].get('output_mode', default_mode) - # Handle legacy 'notices' setting for backwards compatibility - if 'output_mode' not in player['settings'] and 'notices' in player['settings']: - output_mode = 'NOTICE' if player['settings']['notices'] else 'PRIVMSG' - - if output_mode == 'PUBLIC': - # Send as regular channel message - self.send_message(channel, message) - elif output_mode == 'NOTICE': - # Send as notice to user - notice_msg = message.replace(f"{nick} > ", "") # Remove nick prefix for notice - self.send_raw(f'NOTICE {nick} :{notice_msg}') - else: # PRIVMSG - # Send as private message - private_msg = message.replace(f"{nick} > ", "") # Remove nick prefix for PM - self.send_message(nick, private_msg) - - def get_random_player_for_friendly_fire(self, shooter_nick): - """Get a random player (except shooter) for friendly fire""" - eligible_players = [] - shooter_lower = shooter_nick.lower() - - for nick in self.players.keys(): - if nick != shooter_lower: # Don't hit yourself - eligible_players.append(nick) - - if eligible_players: - return random.choice(eligible_players) - return None - - def _get_starting_accuracy(self): - """Get starting accuracy for new player - either fixed or random""" - if self.get_config('new_players.random_stats.enabled', False): - accuracy_range = self.get_config('new_players.random_stats.accuracy_range', [60, 80]) - if accuracy_range and len(accuracy_range) >= 2: - return random.randint(accuracy_range[0], accuracy_range[1]) - return self.get_config('new_players.starting_accuracy', 65) - - def _get_starting_reliability(self): - """Get starting reliability for new player - either fixed or random""" - if self.get_config('new_players.random_stats.enabled', False): - reliability_range = self.get_config('new_players.random_stats.reliability_range', [65, 85]) - if reliability_range and len(reliability_range) >= 2: - return random.randint(reliability_range[0], reliability_range[1]) - return self.get_config('new_players.starting_reliability', 70) - - async def auto_rearm_confiscated_guns(self, channel, shooter_nick): - """Auto-rearm all players with confiscated guns when someone shoots a duck""" - if not self.get_config('weapons.auto_rearm_on_duck_shot', False): - return - - rearmed_players = [] - for user_host, player_data in self.players.items(): - if player_data.get('gun_confiscated', False): - player_data['gun_confiscated'] = False - player_data['ammo'] = player_data.get('ammo', 0) + 1 # Give them 1 ammo - - # Get just the nickname from user!host format - nick = user_host.split('!')[0] if '!' in user_host else user_host - rearmed_players.append(nick) - - if rearmed_players: - self.save_database() - # Send notification to channel - rearmed_list = ', '.join(rearmed_players) - self.send_message(channel, f"🔫 {self.colors['green']}Auto-rearm:{self.colors['reset']} {rearmed_list} got their guns back! (Thanks to {shooter_nick}'s duck shot)") - self.logger.info(f"Auto-rearmed {len(rearmed_players)} players after {shooter_nick} shot duck in {channel}") - - async def update_channel_records(self, channel, hunter, shot_time, duck_type): - """Update channel records and duck difficulty after a successful shot""" - if not self.get_config('records_tracking.enabled', True): - return - - # Initialize channel records if needed - if channel not in self.channel_records: - self.channel_records[channel] = { - 'fastest_shot': None, - 'last_duck': None, - 'total_ducks': 0, - 'total_shots': 0 - } - - records = self.channel_records[channel] - - # Update totals - records['total_ducks'] += 1 - - # Update fastest shot record - if not records['fastest_shot'] or shot_time < records['fastest_shot']['time']: - records['fastest_shot'] = { - 'time': shot_time, - 'hunter': hunter, - 'duck_type': duck_type, - 'timestamp': time.time() - } - # Announce new record - self.send_message(channel, f"🏆 {self.colors['yellow']}NEW RECORD!{self.colors['reset']} {hunter} set fastest shot: {shot_time:.3f}s!") - - # Update last duck info - records['last_duck'] = { - 'hunter': hunter, - 'type': duck_type, - 'shot_time': shot_time, - 'timestamp': time.time() - } - - # Increase duck difficulty (smartness) - if self.get_config('duck_smartness.enabled', True): - if channel not in self.duck_difficulty: - self.duck_difficulty[channel] = 1.0 - - learning_rate = self.get_config('duck_smartness.learning_rate', 0.1) - max_difficulty = self.get_config('duck_smartness.max_difficulty_multiplier', 2.0) - - # Ensure max_difficulty has a valid value - if max_difficulty is None: - max_difficulty = 2.0 - - # Increase difficulty slightly with each duck shot - self.duck_difficulty[channel] = min( - max_difficulty, - self.duck_difficulty[channel] + learning_rate - ) - - # Save records to database periodically - self.save_database() - - async def connect(self): - try: - server = self.config['server'] - port = self.config['port'] - ssl_context = ssl.create_default_context() if self.config.get('ssl', True) else None - - self.logger.info(f"Connecting to {server}:{port} (SSL: {ssl_context is not None})") - - self.reader, self.writer = await asyncio.open_connection( - server, port, ssl=ssl_context - ) - self.logger.info("Connected successfully!") - - # Start SASL negotiation if enabled - if await self.sasl_handler.start_negotiation(): - return True - else: - # Standard registration without SASL - await self.register_user() - return True - except Exception as e: - self.logger.error(f"Connection failed: {e}") - return False - - async def register_user(self): - """Register the user with the IRC server""" - # Send password FIRST if configured (for I-line exemption) - if self.config.get('password'): - self.send_raw(f'PASS {self.config["password"]}') - - self.logger.info(f"Registering as {self.config['nick']}") - self.send_raw(f'NICK {self.config["nick"]}') - self.send_raw(f'USER {self.config["nick"]} 0 * :DuckHunt Bot') - - def send_raw(self, msg): - # Skip debug logging for speed - # self.logger.debug(f"-> {msg}") - if self.writer: - self.writer.write((msg + '\r\n').encode()) - - def send_message(self, target, msg): - # Skip logging during gameplay for speed (uncomment for debugging) - # self.logger.info(f"Sending to {target}: {msg}") - self.send_raw(f'PRIVMSG {target} :{msg}') - # Remove drain() for faster responses - let TCP handle buffering - - def check_rate_limit(self, nick: str, channel: str) -> bool: - """Check if user is within rate limits""" - try: - current_time = time.time() - key = f"{nick}:{channel}" - - # Rate limit: 5 commands per 30 seconds per user per channel - if key not in self.command_cooldowns: - self.command_cooldowns[key] = [] - - # Remove old entries - self.command_cooldowns[key] = [ - timestamp for timestamp in self.command_cooldowns[key] - if current_time - timestamp < 30 - ] - - # Check if under limit - if len(self.command_cooldowns[key]) >= 5: - return False - - # Add current command - self.command_cooldowns[key].append(current_time) - return True - - except Exception as e: - self.logger.error(f"Rate limit check failed: {e}") - return True # Allow command if rate limiting fails - - def get_player(self, user): - """Get player data by nickname only (case insensitive)""" - if '!' not in user: - return None - - nick = user.split('!')[0].lower() # Case insensitive - - # Use nick as database key - if nick in self.players: - player = self.players[nick] - # Backward compatibility: ensure all required fields exist - if 'missed' not in player: - player['missed'] = 0 - if 'inventory' not in player: - player['inventory'] = {} - return player - - # Create new player with configurable defaults - player_data = { - 'xp': self.get_config('new_players.starting_xp', 0), - 'caught': 0, - 'befriended': 0, # Separate counter for befriended ducks - 'missed': 0, - 'ammo': self.get_config('weapons.starting_ammo', 6), - 'max_ammo': self.get_config('weapons.max_ammo_base', 6), - 'chargers': self.get_config('weapons.starting_chargers', 2), - 'max_chargers': self.get_config('weapons.max_chargers_base', 2), - 'accuracy': self._get_starting_accuracy(), - 'reliability': self._get_starting_reliability(), - 'weapon': self.get_config('weapons.starting_weapon', 'pistol'), - 'gun_confiscated': False, - 'explosive_ammo': False, - 'settings': { - 'output_mode': self.get_config('message_output.default_user_mode', 'PUBLIC'), - 'notices': True, # Legacy setting for backwards compatibility - 'private_messages': False - }, - # Inventory system - 'inventory': {}, - # New advanced stats - 'golden_ducks': 0, - 'karma': self.get_config('new_players.starting_karma', 0), - 'deflection': self.get_config('new_players.starting_deflection', 0), - 'defense': self.get_config('new_players.starting_defense', 0), - 'jammed': False, - 'jammed_count': 0, - 'deaths': 0, - 'neutralized': 0, - 'deflected': 0, - 'best_time': 999.9, - 'total_reflex_time': 0.0, - 'reflex_shots': 0, - 'wild_shots': 0, - 'accidents': 0, - 'total_ammo_used': 0, - 'shot_at': 0, - 'lucky_shots': 0, - # Shop items - 'luck': 0, - 'detector': 0, - 'silencer': 0, - 'sunglasses': 0, - 'clothes': 0, - 'grease': 0, - 'brush': 0, - 'mirror': 0, - 'sand': 0, - 'water': 0, - 'sabotage': 0, - 'life_insurance': 0, - 'liability': 0, - 'decoy': 0, - 'bread': 0, - 'duck_detector': 0, - 'mechanical': 0 - } - - self.players[nick] = player_data - self.save_database() # Auto-save new players - return player_data - - def save_player(self, user): - """Save player data - batch saves for performance""" - if not hasattr(self, '_save_pending'): - self._save_pending = False - - if not self._save_pending: - self._save_pending = True - # Schedule delayed save to batch multiple writes - asyncio.create_task(self._delayed_save()) - - async def _delayed_save(self): - """Batch save to reduce disk I/O""" - await asyncio.sleep(0.5) # Small delay to batch saves - try: - self.save_database() - self.logger.debug("Database batch save completed") - except Exception as e: - self.logger.error(f"Database batch save failed: {e}") - finally: - self._save_pending = False - - def setup_signal_handlers(self): - """Setup signal handlers for graceful shutdown""" - def signal_handler(signum): - signal_name = signal.Signals(signum).name - self.logger.info(f"Received {signal_name}, initiating graceful shutdown...") - self.shutdown_requested = True - - # Handle common shutdown signals - if hasattr(signal, 'SIGTERM'): - signal.signal(signal.SIGTERM, lambda s, f: signal_handler(s)) - if hasattr(signal, 'SIGINT'): - signal.signal(signal.SIGINT, lambda s, f: signal_handler(s)) - if hasattr(signal, 'SIGHUP'): - signal.signal(signal.SIGHUP, lambda s, f: signal_handler(s)) - - def is_rate_limited(self, user, command, cooldown=2.0): - """Check if user is rate limited for a command""" - now = time.time() - key = f"{user}:{command}" - - if key in self.command_cooldowns: - if now - self.command_cooldowns[key] < cooldown: - return True - - self.command_cooldowns[key] = now - return False - - async def handle_command(self, user, channel, message): - """Enhanced command handler with logging, validation, and graceful degradation""" - if not user: - self.logger.warning("Received command with no user information") - return - - try: - nick = user.split('!')[0] - nick_lower = nick.lower() - - # Input validation - if not InputValidator.validate_nickname(nick): - self.logger.warning(f"Invalid nickname format: {nick}") - return - - if not InputValidator.validate_channel(channel) and not channel == self.config['nick']: - self.logger.warning(f"Invalid channel format: {channel}") - return - - # Sanitize message input - message = InputValidator.sanitize_message(message) - if not message: - return - - # Enhanced logging with context - self.logger.debug(f"Processing command from {nick} in {channel}: {message[:100]}") - - # Check if user is ignored - if nick_lower in self.ignored_nicks: - self.logger.debug(f"Ignoring command from ignored user: {nick}") - return - - # Rate limiting check - if not self.check_rate_limit(nick_lower, channel): - self.logger.info(f"Rate limit exceeded for {nick} in {channel}") - return - - # Determine if this is a private message to the bot - is_private = channel == self.config['nick'] - - # For private messages, use the nick as the target for responses - response_target = nick if is_private else channel - - # Handle private messages (no ! prefix needed) - if is_private: - cmd = message.strip().lower() - self.logger.info(f"Private command from {nick}: {cmd}") - - # Private message admin commands - if self.is_admin(user): - if cmd == 'restart': - await self.handle_restart(nick, response_target) - return - elif cmd == 'quit': - await self.handle_quit(nick, response_target) - return - elif cmd == 'launch' or cmd == 'ducklaunch': - # For private messages, launch in all channels - for chan in self.channels_joined: - await self.spawn_duck_now(chan) - self.send_message(response_target, f"{nick} > Launched ducks in all channels!") - return - elif cmd == 'golden' or cmd == 'goldenduck': - # Launch golden ducks - for chan in self.channels_joined: - await self.spawn_duck_now(chan, force_golden=True) - self.send_message(response_target, f"{nick} > Launched {self.colors['yellow']}GOLDEN DUCKS{self.colors['reset']} in all channels!") - return - elif cmd.startswith('ignore '): - target_nick = cmd[7:].strip().lower() - await self.handle_ignore(nick, response_target, target_nick) - return - elif cmd.startswith('delignore '): - target_nick = cmd[10:].strip().lower() - await self.handle_delignore(nick, response_target, target_nick) - return - else: - # Unknown private command - self.send_message(response_target, f"{nick} > Admin commands: restart, quit, launch, golden, ignore , delignore ") - return - else: - # Non-admin private message - self.send_message(response_target, f"{nick} > Private commands are admin-only. Use !help in a channel for game commands.") - return - - # Handle channel messages (must start with !) - if not message.startswith('!'): - return - - # Extract just the command part (first word) to handle emojis and extra text - cmd = message.strip().lower().split()[0] - # Keep the original message for commands that need arguments - full_cmd = message.strip().lower() - - # Regular game commands (channel only) - # Inline common commands for speed - if cmd == '!bang': - # Rate limit shooting to prevent spam - if self.is_rate_limited(user, 'bang', 1.0): - return - - player = self.get_player(user) - if not player: - return - - # Check if gun is confiscated - if player.get('gun_confiscated', False): - self.send_message(channel, f"{nick} > {self.colors['red']}Your gun has been confiscated! Buy a new gun from the shop (item #5).{self.colors['reset']}") - return - - # Check if gun is jammed - if player.get('jammed', False): - self.send_message(channel, f"{nick} > {self.colors['red']}Your gun is jammed! Use !reload to unjam it.{self.colors['reset']}") - return - - # Check ammo - if player['ammo'] <= 0: - self.send_message(channel, f"{nick} > Your gun is empty! | Ammo: 0/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") - return - - # Check for gun jamming before shooting - if self.gun_jams(player): - player['jammed'] = True - player['jammed_count'] = player.get('jammed_count', 0) + 1 - jam_sound = "•click• •click•" if player.get('silencer', 0) > 0 else "*CLICK* *CLICK*" - self.send_message(channel, f"{nick} > {jam_sound} Your gun jammed! Use !reload to unjam it.") - self.save_player(user) - return - - # Get ducks in this channel - channel_ducks = self.ducks.get(channel, []) - alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] - - # Consume ammo - player['ammo'] -= 1 - player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1 - - if alive_ducks: - # Target the oldest duck (first in, first out) - target_duck = alive_ducks[0] - shot_time = time.time() - target_duck['spawn_time'] - is_golden = target_duck.get('type') == 'golden' - - # Calculate hit chance (golden ducks are harder to hit) - base_accuracy = player['accuracy'] - if is_golden: - base_accuracy = max(base_accuracy - 30, 10) # Golden ducks much harder - - # Apply bonuses - if player.get('sunglasses', 0) > 0: - base_accuracy += 5 # Sunglasses help - if player.get('mirror', 0) > 0: - base_accuracy += 3 # Mirror helps - - # Apply duck smartness penalty - duck_difficulty = self.duck_difficulty.get(channel, 1.0) - if duck_difficulty > 1.0: - # Smarter ducks are harder to hit - difficulty_penalty = (duck_difficulty - 1.0) * 20 # Up to 20% penalty at max difficulty - base_accuracy = max(base_accuracy - difficulty_penalty, 10) # Never go below 10% - - hit_chance = min(base_accuracy, 95) # Cap at 95% - - # Record shot attempt - player['shot_at'] = player.get('shot_at', 0) + 1 - target_duck['hit_attempts'] = target_duck.get('hit_attempts', 0) + 1 - - # Track total shots for channel statistics - if channel not in self.channel_records: - self.channel_records[channel] = {'total_shots': 0, 'total_ducks': 0} - self.channel_records[channel]['total_shots'] += 1 - - # Check for hit - if random.randint(1, 100) <= hit_chance: - # HIT! - player['caught'] += 1 - target_duck['alive'] = False - - # Update reflex time stats - player['reflex_shots'] = player.get('reflex_shots', 0) + 1 - player['total_reflex_time'] = player.get('total_reflex_time', 0) + shot_time - if shot_time < player.get('best_time', 999.9): - player['best_time'] = shot_time - - # Calculate XP and rewards - if is_golden: - player['golden_ducks'] = player.get('golden_ducks', 0) + 1 - base_xp = 50 # Golden ducks give much more XP - self.update_karma(player, 'golden_hit') - else: - base_xp = 15 # Normal XP - self.update_karma(player, 'hit') - - # Lucky shot bonus - luck_multiplier = 1 + (player.get('luck', 0) * 0.1) # 10% per luck point - is_lucky = random.randint(1, 100) <= (5 + player.get('luck', 0)) - if is_lucky: - player['lucky_shots'] = player.get('lucky_shots', 0) + 1 - luck_multiplier *= 1.5 # 50% bonus for lucky shot - - xp_earned = int(base_xp * luck_multiplier) - player['xp'] += xp_earned - - # Sound effects based on ammo type - if player.get('explosive_ammo', False): - shot_sound = "•BOUM•" if player.get('silencer', 0) > 0 else "*BOUM*" - explosive_text = f" {self.colors['orange']}[explosive ammo]{self.colors['reset']}" - else: - shot_sound = "•bang•" if player.get('silencer', 0) > 0 else "*BANG*" - explosive_text = "" - - # Lucky shot text - lucky_text = f" {self.colors['yellow']}[lucky shot!]{self.colors['reset']}" if is_lucky else "" - - # Build hit message - level, title = self.get_player_level(player['xp']) - - if is_golden: - golden_count = player.get('golden_ducks', 0) - hit_msg = f"{nick} > {self.colors['yellow']}{shot_sound}{self.colors['reset']} You shot down the {self.colors['yellow']}★ GOLDEN DUCK ★{self.colors['reset']} in {shot_time:.3f}s! Total: {player['caught']} ducks ({self.colors['yellow']}{golden_count} golden{self.colors['reset']}) | Level {level}: {title} | [{self.colors['yellow']}{xp_earned} xp{self.colors['reset']}]{explosive_text}{lucky_text}" - else: - hit_msg = f"{nick} > {self.colors['green']}{shot_sound}{self.colors['reset']} You shot down the duck in {shot_time:.3f}s! Total: {player['caught']} ducks | Level {level}: {title} | [{self.colors['green']}{xp_earned} xp{self.colors['reset']}]{explosive_text}{lucky_text}" - - self.send_message(channel, hit_msg) - - # Scare other ducks if enabled (successful shots can scare ducks) - await self.scare_other_ducks(channel, target_duck['id']) - - # Find items in bushes (rare chance) - await self.find_bushes_items(nick, channel, player) - - # Auto-rearm confiscated guns if enabled - await self.auto_rearm_confiscated_guns(channel, nick) - - # Track records and increase duck difficulty - await self.update_channel_records(channel, nick, shot_time, target_duck['type']) - - else: - # MISS! - player['missed'] += 1 - self.update_karma(player, 'miss') - - # Calculate miss penalty based on level - miss_penalty = int(self.calculate_penalty_by_level(-2, player['xp'])) - player['xp'] += miss_penalty - - # Bullet ricochet chance (can hit other players) - ricochet_chance = 8 # 8% base chance - if player.get('explosive_ammo', False): - ricochet_chance = 15 # Higher with explosive - - ricochet_msg = "" - if random.randint(1, 100) <= ricochet_chance: - ricochet_target = self.get_random_player_for_friendly_fire(nick) - if ricochet_target: - target_player = self.players[ricochet_target] - ricochet_dmg = -3 - target_player['xp'] += ricochet_dmg - target_player['shot_at'] = target_player.get('shot_at', 0) + 1 - ricochet_msg = f" {self.colors['red']}[RICOCHET: {ricochet_target} hit for {ricochet_dmg} xp]{self.colors['reset']}" - - # Scare duck on miss - await self.scare_duck_on_miss(channel, target_duck) - - miss_sound = "•click•" if player.get('silencer', 0) > 0 else "*CLICK*" - await self.send_user_message(nick, channel, f"{nick} > {miss_sound} You missed the duck! [miss: {miss_penalty} xp]{ricochet_msg}") - - else: - # No duck present - wild fire! - player['wild_shots'] = player.get('wild_shots', 0) + 1 - self.update_karma(player, 'wild_shot') - - # Track wild shots in channel statistics - if channel not in self.channel_records: - self.channel_records[channel] = {'total_shots': 0, 'total_ducks': 0} - self.channel_records[channel]['total_shots'] += 1 - - # Calculate penalties based on level - miss_penalty = int(self.calculate_penalty_by_level(-2, player['xp'])) - wild_penalty = int(self.calculate_penalty_by_level(-3, player['xp'])) - player['xp'] += miss_penalty + wild_penalty - - # Confiscate gun - player['gun_confiscated'] = True - - # Higher chance of hitting other players when no duck - friendly_fire_chance = 25 # 25% when no duck - friendly_fire_msg = "" - - if random.randint(1, 100) <= friendly_fire_chance: - ff_target = self.get_random_player_for_friendly_fire(nick) - if ff_target: - target_player = self.players[ff_target] - ff_dmg = int(self.calculate_penalty_by_level(-4, target_player['xp'])) - target_player['xp'] += ff_dmg - target_player['shot_at'] = target_player.get('shot_at', 0) + 1 - player['accidents'] = player.get('accidents', 0) + 1 - self.update_karma(player, 'teamkill') - friendly_fire_msg = f" {self.colors['red']}[ACCIDENT: {ff_target} injured for {ff_dmg} xp]{self.colors['reset']}" - - wild_sound = "•BOUM•" if player.get('explosive_ammo', False) else "*BANG*" - if player.get('silencer', 0) > 0: - wild_sound = "•" + wild_sound[1:-1] + "•" - - confiscated_msg = f" {self.colors['red']}[GUN CONFISCATED]{self.colors['reset']}" - await self.send_user_message(nick, channel, f"{nick} > {wild_sound} You shot at nothing! What were you aiming at? [miss: {miss_penalty} xp] [wild fire: {wild_penalty} xp]{confiscated_msg}{friendly_fire_msg}") - - # Save after each shot - self.save_player(user) - - elif cmd == '!bef': - # Check if befriending is enabled - if not self.get_config('befriending.enabled', True): - self.send_message(channel, f"{nick} > Duck befriending is currently disabled!") - return - - player = self.get_player(user) - if not player: - return - - # Get ducks in this channel - channel_ducks = self.ducks.get(channel, []) - alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] - - if alive_ducks: - # Target the oldest duck (first in, first out) - target_duck = alive_ducks[0] - bef_time = time.time() - target_duck['spawn_time'] - - # Calculate befriend success chance using config values - level, _ = self.get_player_level(player['xp']) - base_success = self.get_config('befriending.base_success_rate', 65) or 65 - max_success = self.get_config('befriending.max_success_rate', 90) or 90 - level_bonus_per_level = self.get_config('befriending.level_bonus_per_level', 2) or 2 - level_bonus_cap = self.get_config('befriending.level_bonus_cap', 20) or 20 - luck_bonus_per_point = self.get_config('befriending.luck_bonus_per_point', 3) or 3 - - level_bonus = min(level * level_bonus_per_level, level_bonus_cap) - luck_bonus = player.get('luck', 0) * luck_bonus_per_point - success_chance = min(base_success + level_bonus + luck_bonus, max_success) - - # Check if befriend attempt succeeds - if random.randint(1, 100) <= success_chance: - # Successful befriend - player['befriended'] = player.get('befriended', 0) + 1 - - # XP rewards from config - xp_min = self.get_config('befriending.xp_reward_min', 1) or 1 - xp_max = self.get_config('befriending.xp_reward_max', 3) or 3 - - xp_earned = random.randint(xp_min, xp_max) - player['xp'] += xp_earned - - # Mark duck as befriended (dead) - target_duck['alive'] = False - - # Lucky items with configurable chance - if self.get_config('items.lucky_items_enabled', True): - lucky_items = ["four-leaf clover", "rabbit's foot", "horseshoe", "lucky penny", "magic feather"] - base_luck_chance = self.get_config('befriending.lucky_item_chance', 5) + player.get('luck', 0) - lucky_item = random.choice(lucky_items) if random.randint(1, 100) <= base_luck_chance else None - lucky_text = f" [{lucky_item}]" if lucky_item else "" - else: - lucky_text = "" - - remaining_ducks = len([d for d in channel_ducks if d.get('alive')]) - duck_count_text = f" | {remaining_ducks} ducks remain" if remaining_ducks > 0 else "" - - self.send_message(channel, f"{nick} > You befriended a duck in {bef_time:.3f}s! Total friends: {player['befriended']} ducks on {channel}. \\_o< *quack* [+{xp_earned} xp]{lucky_text}{duck_count_text}") - - # Update karma for successful befriend - if self.get_config('karma.enabled', True): - karma_bonus = self.get_config('karma.befriend_success_bonus', 2) - player['karma'] = player.get('karma', 0) + karma_bonus - - # Save to database after befriending - self.save_player(user) - else: - # Duck refuses to be befriended - refusal_messages = [ - f"{nick} > The duck looks at you suspiciously and waddles away! \\_o< *suspicious quack*", - f"{nick} > The duck refuses to be friends and flaps away angrily! \\_O< *angry quack*", - f"{nick} > The duck ignores your friendship attempts and goes back to swimming! \\_o< *indifferent quack*", - f"{nick} > The duck seems shy and hides behind some reeds! \\_o< *shy quack*", - f"{nick} > The duck is too busy looking for food to be your friend! \\_o< *hungry quack*", - f"{nick} > The duck gives you a cold stare and swims to the other side! \\_O< *cold quack*", - f"{nick} > The duck prefers to stay wild and free! \\_o< *wild quack*", - f"{nick} > The duck thinks you're trying too hard and keeps its distance! \\_o< *skeptical quack*" - ] - - # Small chance the duck gets scared and flies away (configurable) - scared_chance = self.get_config('befriending.scared_away_chance', 10) or 10 - if random.randint(1, 100) <= scared_chance: - target_duck['alive'] = False - scared_messages = [ - f"{nick} > Your friendship attempt scared the duck away! It flies off into the sunset! \\_o< *departing quack*", - f"{nick} > The duck panics at your approach and escapes! \\_O< *panicked quack* *flap flap*" - ] - self.send_message(channel, random.choice(scared_messages)) - else: - self.send_message(channel, random.choice(refusal_messages)) - - # XP penalty for failed befriend attempt (configurable) - xp_penalty = self.get_config('befriending.failure_xp_penalty', 1) - player['xp'] = max(0, player['xp'] - xp_penalty) - - # Update karma for failed befriend - if self.get_config('karma.enabled', True): - karma_penalty = self.get_config('karma.befriend_fail_penalty', 1) - player['karma'] = player.get('karma', 0) - karma_penalty - - # Save player data - self.save_player(user) - else: - self.send_message(channel, f"{nick} > There is no duck to befriend!") - - elif cmd == '!reload': - player = self.get_player(user) - if not player: - return - - # Check if gun is jammed (reload unjams it) - if player.get('jammed', False): - player['jammed'] = False - unjam_sound = "•click click•" if player.get('silencer', 0) > 0 else "*click click*" - self.send_message(channel, f"{nick} > {unjam_sound} You unjammed your gun! | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") - self.save_player(user) - return - - if player['ammo'] == player['max_ammo']: - self.send_message(channel, f"{nick} > Your gun doesn't need to be reloaded. | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") - return - - if player['chargers'] <= 0: - self.send_message(channel, f"{nick} > You don't have any chargers left! | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: 0/{player['max_chargers']}") - return - - # Calculate reload reliability - reload_reliability = self.calculate_gun_reliability(player) - - if random.randint(1, 100) <= reload_reliability: - player['chargers'] -= 1 - player['ammo'] = player['max_ammo'] - reload_sound = "•click•" if player.get('silencer', 0) > 0 else "*click*" - self.send_message(channel, f"{nick} > {reload_sound} You reloaded your gun! | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") - else: - # Gun jams during reload - player['jammed'] = True - player['jammed_count'] = player.get('jammed_count', 0) + 1 - jam_sound = "•CLACK• •click click•" if player.get('silencer', 0) > 0 else "*CLACK* *click click*" - self.send_message(channel, f"{nick} > {jam_sound} Your gun jammed while reloading! Use !reload again to unjam it.") - - # Save to database after reload - self.save_player(user) - - elif cmd == '!stats': - await self.handle_stats(nick, channel, user) - elif cmd == '!help': - await self.handle_help(nick, channel) - elif full_cmd.startswith('!shop'): - # Handle !shop or !shop - parts = full_cmd.split() - if len(parts) == 1: - # Just !shop - show shop listing - await self.handle_shop(nick, channel, user) - elif len(parts) >= 2: - # !shop - purchase item - item_id = parts[1] - await self.handle_buy(nick, channel, item_id, user) - elif full_cmd.startswith('!use '): - parts = full_cmd[5:].split() - if len(parts) >= 1: - item_id = parts[0] - target_nick = parts[1] if len(parts) >= 2 else None - await self.handle_use(nick, channel, item_id, user, target_nick) - else: - self.send_message(channel, f"{nick} > Usage: !use [target_nick]") - elif full_cmd.startswith('!sell '): - item_id = full_cmd[6:].strip() - await self.handle_sell(nick, channel, item_id, user) - elif full_cmd.startswith('!trade '): - parts = full_cmd[7:].split() - if len(parts) >= 3: - target_nick, item, amount = parts[0], parts[1], parts[2] - await self.handle_trade(nick, channel, user, target_nick, item, amount) - else: - self.send_message(channel, f"{nick} > Usage: !trade ") - elif full_cmd.startswith('!rearm ') and self.is_admin(user): # Admin only - # Allow rearming other players or self - target_nick = full_cmd[7:].strip() - await self.handle_rearm(nick, channel, user, target_nick) - elif cmd == '!rearm' and self.is_admin(user): # Admin only - # Rearm self - await self.handle_rearm(nick, channel, user, nick) - elif cmd == '!duck' and self.is_admin(user): # Admin only - await self.spawn_duck_now(channel) - elif cmd == '!golden' and self.is_admin(user): # Admin only - await self.spawn_duck_now(channel, force_golden=True) - elif cmd == '!listplayers' and self.is_admin(user): # Admin only - await self.handle_listplayers(nick, channel) - elif full_cmd.startswith('!ban ') and self.is_admin(user): # Admin only - target_nick = full_cmd[5:].strip() - await self.handle_ban(nick, channel, target_nick) - elif full_cmd.startswith('!reset ') and self.is_admin(user): # Admin only - target_nick = full_cmd[7:].strip() - await self.handle_reset(nick, channel, target_nick) - elif cmd == '!resetdb' and self.is_admin(user): # Admin only - await self.handle_reset_database(nick, channel, user) - elif full_cmd.startswith('!resetdb confirm ') and self.is_admin(user): # Admin only - confirmation = full_cmd[17:].strip() - await self.handle_reset_database_confirm(nick, channel, user, confirmation) - elif cmd == '!restart' and self.is_admin(user): # Admin only - await self.handle_restart(nick, channel) - elif cmd == '!quit' and self.is_admin(user): # Admin only - await self.handle_quit(nick, channel) - elif cmd == '!ducklaunch' and self.is_admin(user): # Admin only - await self.spawn_duck_now(channel) - elif cmd == '!ducks': - # Show duck count for all users - channel_ducks = self.ducks.get(channel, []) - alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] - dead_ducks = [duck for duck in channel_ducks if not duck.get('alive')] - - if alive_ducks: - duck_list = [] - for duck in alive_ducks: - duck_type = duck.get('type', 'normal') - spawn_time = time.time() - duck['spawn_time'] - if duck_type == 'golden': - duck_list.append(f"{self.colors['yellow']}Golden Duck{self.colors['reset']} ({spawn_time:.1f}s)") - else: - duck_list.append(f"Duck ({spawn_time:.1f}s)") - self.send_message(channel, f"{nick} > Active ducks: {', '.join(duck_list)}") - else: - self.send_message(channel, f"{nick} > No ducks currently active.") - - elif cmd == '!top' or cmd == '!leaderboard': - # Show top players by XP - if not self.players: - self.send_message(channel, f"{nick} > No players found!") - return - - # Sort players by XP - sorted_players = sorted(self.players.items(), key=lambda x: x[1]['xp'], reverse=True) - top_5 = sorted_players[:5] - - self.send_message(channel, f"{self.colors['cyan']}🏆 TOP HUNTERS LEADERBOARD 🏆{self.colors['reset']}") - for i, (player_nick, player_data) in enumerate(top_5, 1): - level, title = self.get_player_level(player_data['xp']) - total_ducks = player_data.get('caught', 0) + player_data.get('befriended', 0) - golden = player_data.get('golden_ducks', 0) - golden_text = f" ({self.colors['yellow']}{golden} golden{self.colors['reset']})" if golden > 0 else "" - - if i == 1: - rank_color = self.colors['yellow'] # Gold - elif i == 2: - rank_color = self.colors['gray'] # Silver - elif i == 3: - rank_color = self.colors['orange'] # Bronze - else: - rank_color = self.colors['white'] - - self.send_message(channel, f"{rank_color}#{i}{self.colors['reset']} {player_nick} - Level {level}: {title} | XP: {player_data['xp']} | Ducks: {total_ducks}{golden_text}") - - elif cmd == '!levels': - # Show level progression table - self.send_message(channel, f"{self.colors['cyan']}🎯 LEVEL PROGRESSION SYSTEM 🎯{self.colors['reset']}") - - # Show first 10 levels as example - for i in range(min(10, len(self.levels))): - level_data = self.levels[i] - next_xp = self.levels[i + 1]['xp'] if i + 1 < len(self.levels) else "MAX" - self.send_message(channel, f"Level {i + 1}: {level_data['title']} (XP: {level_data['xp']} - {next_xp})") - - if len(self.levels) > 10: - self.send_message(channel, f"... and {len(self.levels) - 10} more levels up to Level {len(self.levels)}: {self.levels[-1]['title']}") - - elif full_cmd.startswith('!level '): - # Show specific player's level info - target_nick = full_cmd[7:].strip().lower() - if target_nick in self.players: - target_player = self.players[target_nick] - level, title = self.get_player_level(target_player['xp']) - xp_for_next = self.get_xp_for_next_level(target_player['xp']) - - if xp_for_next > 0: - next_info = f"Next level in {xp_for_next} XP" - else: - next_info = "MAX LEVEL REACHED!" - - self.send_message(channel, f"{nick} > {target_nick}: Level {level} - {self.colors['cyan']}{title}{self.colors['reset']} | {next_info}") - else: - self.send_message(channel, f"{nick} > Player {target_nick} not found!") - - elif cmd == '!karma': - # Show karma leaderboard - if not self.players: - self.send_message(channel, f"{nick} > No players found!") - return - - # Sort by karma - karma_players = [(nick, data) for nick, data in self.players.items() if data.get('karma', 0) != 0] - karma_players.sort(key=lambda x: x[1].get('karma', 0), reverse=True) - - if not karma_players: - self.send_message(channel, f"{nick} > No karma data available!") - return - - self.send_message(channel, f"{self.colors['purple']}☯ KARMA LEADERBOARD ☯{self.colors['reset']}") - for i, (player_nick, player_data) in enumerate(karma_players[:5], 1): - karma = player_data.get('karma', 0) - karma_color = self.colors['green'] if karma >= 0 else self.colors['red'] - karma_text = "Saint" if karma >= 50 else "Good" if karma >= 10 else "Evil" if karma <= -10 else "Chaotic" if karma <= -50 else "Neutral" - - self.send_message(channel, f"#{i} {player_nick} - {karma_color}Karma: {karma}{self.colors['reset']} ({karma_text})") - - elif cmd == '!ducks': - # Show duck count for all users - channel_ducks = self.ducks.get(channel, []) - alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] - dead_ducks = [duck for duck in channel_ducks if not duck.get('alive')] - - if alive_ducks: - oldest_time = min(time.time() - duck['spawn_time'] for duck in alive_ducks) - self.send_message(channel, f"{nick} > {len(alive_ducks)} ducks in {channel} | Oldest: {oldest_time:.1f}s | Dead: {len(dead_ducks)} | Timeout: {self.duck_timeout_min}-{self.duck_timeout_max}s") - else: - self.send_message(channel, f"{nick} > No ducks in {channel} | Dead: {len(dead_ducks)}") - elif cmd == '!output' or full_cmd.startswith('!output '): - parts = cmd.split(maxsplit=1) - output_type = parts[1] if len(parts) > 1 else '' - await self.handle_output(nick, channel, user, output_type) - - elif cmd == '!ducktime': - # Show time until next duck spawn - await self.handle_ducktime(nick, channel) - - elif cmd == '!lastduck': - # Show information about the last duck shot - await self.handle_lastduck(nick, channel) - - elif cmd == '!records': - # Show channel records - await self.handle_records(nick, channel) - elif full_cmd.startswith('!ignore ') and self.is_admin(user): # Admin only - target_nick = full_cmd[8:].strip().lower() - await self.handle_ignore(nick, channel, target_nick) - elif full_cmd.startswith('!delignore ') and self.is_admin(user): # Admin only - target_nick = full_cmd[11:].strip().lower() - await self.handle_delignore(nick, channel, target_nick) - elif full_cmd.startswith('!giveitem ') and self.is_admin(user): # Admin only - parts = full_cmd[10:].split() - if len(parts) >= 2: - target_nick, item = parts[0], parts[1] - await self.handle_admin_giveitem(nick, channel, target_nick, item) - else: - self.send_message(channel, f"{nick} > Usage: !giveitem ") - elif full_cmd.startswith('!givexp ') and self.is_admin(user): # Admin only - parts = full_cmd[8:].split() - if len(parts) >= 2: - target_nick, amount = parts[0], parts[1] - await self.handle_admin_givexp(nick, channel, target_nick, amount) - else: - self.send_message(channel, f"{nick} > Usage: !givexp ") - - except Exception as e: - # Graceful degradation - log error but don't crash - self.logger.error(f"Command handling error for {user} in {channel}: {e}") - import traceback - self.logger.error(f"Full traceback: {traceback.format_exc()}") - - # Send a gentle error message to user - try: - nick = user.split('!')[0] if user and '!' in user else "Unknown" - error_msg = f"{nick} > Sorry, there was an error processing your command. Please try again." - if channel == self.config['nick']: # Private message - self.send_message(nick, error_msg) - else: # Channel message - self.send_message(channel, error_msg) - except Exception as send_error: - self.logger.error(f"Failed to send error message: {send_error}") - - async def handle_stats(self, nick, channel, user): - player = self.get_player(user) - if not player: - self.send_message(channel, f"{nick} > Player data not found!") - return - - # Get level and title - level, title = self.get_player_level(player['xp']) - xp_for_next = self.get_xp_for_next_level(player['xp']) - - # Calculate advanced stats - total_shots = player.get('caught', 0) + player.get('missed', 0) - effective_accuracy = (player.get('caught', 0) / total_shots * 100) if total_shots > 0 else 0 - average_time = (player.get('total_reflex_time', 0) / player.get('reflex_shots', 1)) if player.get('reflex_shots', 0) > 0 else 0 - - # Gun status - gun_status = "" - if player.get('gun_confiscated', False): - gun_status += f" {self.colors['red']}[CONFISCATED]{self.colors['reset']}" - if player.get('jammed', False): - gun_status += f" {self.colors['yellow']}[JAMMED]{self.colors['reset']}" - if player.get('explosive_ammo', False): - gun_status += f" {self.colors['orange']}[EXPLOSIVE]{self.colors['reset']}" - - # Duck stats with colors - duck_stats = [] - if player.get('caught', 0) > 0: - duck_stats.append(f"Shot:{player['caught']}") - if player.get('befriended', 0) > 0: - duck_stats.append(f"Befriended:{player['befriended']}") - if player.get('golden_ducks', 0) > 0: - duck_stats.append(f"{self.colors['yellow']}Golden:{player['golden_ducks']}{self.colors['reset']}") - - duck_display = f"Ducks:({', '.join(duck_stats)})" if duck_stats else "Ducks:0" - - # Main stats line - stats_line1 = f"{nick} > {duck_display} | Level {level}: {self.colors['cyan']}{title}{self.colors['reset']} | XP: {player['xp']}" - if xp_for_next > 0: - stats_line1 += f" (next: {xp_for_next})" - - # Combat stats line - karma_color = self.colors['green'] if player.get('karma', 0) >= 0 else self.colors['red'] - karma_display = f"{karma_color}Karma:{player.get('karma', 0)}{self.colors['reset']}" - - stats_line2 = f"{nick} > {karma_display} | Accuracy: {player['accuracy']}% (effective: {effective_accuracy:.1f}%) | Reliability: {self.calculate_gun_reliability(player)}%" - - # Equipment line - weapon_name = player.get('weapon', 'pistol').replace('_', ' ').title() - stats_line3 = f"{nick} > Weapon: {weapon_name}{gun_status} | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}" - - # Advanced stats line - best_time = player.get('best_time', 999.9) - best_display = f"{best_time:.3f}s" if best_time < 999 else "none" - - stats_line4 = f"{nick} > Best time: {best_display} | Avg time: {average_time:.3f}s | Jams: {player.get('jammed_count', 0)} | Accidents: {player.get('accidents', 0)} | Lucky shots: {player.get('lucky_shots', 0)}" - - # Send all stats - await self.send_user_message(nick, channel, stats_line1) - await self.send_user_message(nick, channel, stats_line2) - await self.send_user_message(nick, channel, stats_line3) - await self.send_user_message(nick, channel, stats_line4) - - # Inventory display - if player.get('inventory'): - inventory_items = [] - shop_items = { - '1': 'Extra bullet', '2': 'Extra clip', '3': 'AP ammo', '4': 'Explosive ammo', - '5': 'Gun restore', '6': 'Grease', '7': 'Sight', '8': 'Detector', '9': 'Silencer', - '10': 'Clover', '11': 'Shotgun', '12': 'Rifle', '13': 'Sniper', '14': 'Auto shotgun', - '15': 'Sand', '16': 'Water', '17': 'Sabotage', '18': 'Life insurance', - '19': 'Liability insurance', '20': 'Decoy', '21': 'Bread', '22': 'Duck detector', '23': 'Mechanical duck' - } - - for item_id, count in player['inventory'].items(): - item_name = shop_items.get(item_id, f"Item #{item_id}") - inventory_items.append(f"{item_id}:{item_name}({count})") - - if inventory_items: - max_slots = self.get_config('economy.max_inventory_slots', 20) - total_items = sum(player['inventory'].values()) - inventory_display = f"{nick} > {self.colors['magenta']}Inventory ({total_items}/{max_slots}):{self.colors['reset']} {' | '.join(inventory_items[:10])}" - if len(inventory_items) > 10: - inventory_display += f" ... and {len(inventory_items) - 10} more" - await self.send_user_message(nick, channel, inventory_display) - - async def handle_rearm(self, nick, channel, user, target_nick): - """Rearm a player whose gun was confiscated""" - player = self.get_player(user) - target_nick_lower = target_nick.lower() - - if not player: - self.send_message(channel, f"{nick} > Player data not found!") - return - - # Check if target exists - if target_nick_lower not in self.players: - self.send_message(channel, f"{nick} > Player {target_nick} not found!") - return - - target_player = self.players[target_nick_lower] - - # Check if target's gun is confiscated - if not target_player.get('gun_confiscated', False): - self.send_message(channel, f"{nick} > {target_nick}'s gun is not confiscated!") - return - - # Admins can rearm anyone for free - is_admin = self.is_admin(user) - - if is_admin: - # Admin rearm - no cost, configurable restoration - target_player['gun_confiscated'] = False - - # Configure ammo restoration - if self.get_config('moderation.admin_rearm_gives_full_ammo', True): - target_player['ammo'] = target_player['max_ammo'] # Full ammo - ammo_text = "full ammo" - else: - target_player['ammo'] = min(target_player['ammo'] + 1, target_player['max_ammo']) # Just +1 ammo - ammo_text = "+1 ammo" - - # Configure charger restoration - if self.get_config('moderation.admin_rearm_gives_full_chargers', True): - target_player['chargers'] = target_player.get('max_chargers', 2) # Full chargers - charger_text = ", full chargers" - else: - charger_text = "" - - if target_nick_lower == nick.lower(): - self.send_message(channel, f"{nick} > {self.colors['green']}Admin command: Gun restored with {ammo_text}{charger_text}.{self.colors['reset']}") - else: - self.send_message(channel, f"{nick} > {self.colors['green']}Admin command: {target_nick}'s gun restored with {ammo_text}{charger_text}.{self.colors['reset']}") - self.save_database() - elif target_nick_lower == nick.lower(): - # Regular player rearming self - costs XP - rearm_cost = 40 - if player['xp'] < rearm_cost: - self.send_message(channel, f"{nick} > You need {rearm_cost} XP to rearm yourself (you have {player['xp']} XP)") - return - - player['xp'] -= rearm_cost - player['gun_confiscated'] = False - player['ammo'] = player['max_ammo'] # Full ammo when rearmed - self.send_message(channel, f"{nick} > {self.colors['green']}You rearmed yourself! [-{rearm_cost} XP] Gun restored with full ammo.{self.colors['reset']}") - self.save_player(user) - else: - # Regular player rearming someone else - costs XP (friendly gesture) - rearm_cost_xp = 5 - if player['xp'] < rearm_cost_xp: - self.send_message(channel, f"{nick} > You need {rearm_cost_xp} XP to rearm {target_nick} (you have {player['xp']} XP)") - return - - player['xp'] -= rearm_cost_xp - target_player['gun_confiscated'] = False - target_player['ammo'] = target_player['max_ammo'] # Full ammo when rearmed - self.send_message(channel, f"{nick} > {self.colors['green']}You rearmed {target_nick}! [-{rearm_cost_xp} XP] {target_nick}'s gun restored with full ammo.{self.colors['reset']}") - self.save_player(user) - self.save_database() - - async def handle_help(self, nick, channel): - help_lines = [ - f"{nick} > {self.colors['cyan']}🦆 DUCKHUNT 🦆{self.colors['reset']} !bang !bef !reload !stats !top !shop !buy ", - f"{nick} > {self.colors['yellow']}Golden ducks: 50 XP{self.colors['reset']} | {self.colors['red']}Gun jamming & ricochets ON{self.colors['reset']} | Timeout: {self.duck_timeout_min}-{self.duck_timeout_max}s" - ] - if self.is_admin(f"{nick}!*@*"): # Check if admin - help_lines.append(f"{nick} > {self.colors['red']}Admin:{self.colors['reset']} !duck !golden !ban !reset !resetdb !rearm !giveitem !givexp | /msg {self.config['nick']} restart|quit") - for line in help_lines: - self.send_message(channel, line) - - async def handle_output(self, nick, channel, user, output_type): - """Handle output mode setting (PUBLIC, NOTICE, or PRIVMSG)""" - player = self.get_player(user) - if not player: - self.send_message(channel, f"{nick} > Player data not found!") - return - - # Ensure player has settings (for existing players) - if 'settings' not in player: - default_mode = self.get_config('message_output.default_user_mode', 'PUBLIC') - player['settings'] = { - 'output_mode': default_mode - } - - output_type = output_type.upper() - - if output_type == 'PUBLIC': - player['settings']['output_mode'] = 'PUBLIC' - self.save_database() - self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}PUBLIC{self.colors['reset']} (channel messages)") - - elif output_type == 'NOTICE': - player['settings']['output_mode'] = 'NOTICE' - self.save_database() - self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}NOTICE{self.colors['reset']} (channel notices)") - - elif output_type == 'PRIVMSG': - player['settings']['output_mode'] = 'PRIVMSG' - self.save_database() - self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}PRIVMSG{self.colors['reset']} (private messages)") - - else: - current_mode = player['settings'].get('output_mode', 'NOTICE') - self.send_message(channel, f"{nick} > Current output mode: {self.colors['cyan']}{current_mode}{self.colors['reset']} | Usage: !output PUBLIC, !output NOTICE, or !output PRIVMSG") - - async def handle_ducktime(self, nick, channel): - """Show time until next duck spawn""" - current_time = time.time() - - # Check if there are active ducks - channel_ducks = self.ducks.get(channel, []) - alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] - - if alive_ducks: - self.send_message(channel, f"{nick} > {len(alive_ducks)} duck(s) currently active! Go hunt them!") - return - - # Show next spawn time if we have it - if channel in self.next_duck_spawn: - next_spawn = self.next_duck_spawn[channel] - time_until = max(0, next_spawn - current_time) - - if time_until > 0: - minutes = int(time_until // 60) - seconds = int(time_until % 60) - difficulty = self.duck_difficulty.get(channel, 1.0) - difficulty_text = f" (Difficulty: {difficulty:.1f}x)" if difficulty > 1.0 else "" - self.send_message(channel, f"{nick} > Next duck spawn in {self.colors['cyan']}{minutes}m {seconds}s{self.colors['reset']}{difficulty_text}") - else: - self.send_message(channel, f"{nick} > Duck should spawn any moment now...") - else: - # Estimate based on spawn range - min_min = self.duck_spawn_min // 60 - max_min = self.duck_spawn_max // 60 - self.send_message(channel, f"{nick} > Ducks spawn every {min_min}-{max_min} minutes (spawn time varies)") - - async def handle_lastduck(self, nick, channel): - """Show information about the last duck shot in this channel""" - if channel not in self.channel_records: - self.send_message(channel, f"{nick} > No duck records found for {channel}") - return - - last_duck = self.channel_records[channel].get('last_duck') - if not last_duck: - self.send_message(channel, f"{nick} > No ducks have been shot in {channel} yet") - return - - # Format the last duck info - hunter = last_duck['hunter'] - duck_type = last_duck['type'] - shot_time = last_duck['shot_time'] - time_ago = time.time() - last_duck['timestamp'] - - # Format time ago - if time_ago < 60: - time_ago_str = f"{int(time_ago)}s ago" - elif time_ago < 3600: - time_ago_str = f"{int(time_ago // 60)}m ago" - else: - time_ago_str = f"{int(time_ago // 3600)}h ago" - - duck_emoji = "🥇" if duck_type == "golden" else "🦆" - self.send_message(channel, f"{nick} > Last duck: {duck_emoji} {duck_type} duck shot by {self.colors['cyan']}{hunter}{self.colors['reset']} in {shot_time:.3f}s ({time_ago_str})") - - async def handle_records(self, nick, channel): - """Show channel records and statistics""" - if channel not in self.channel_records: - self.send_message(channel, f"{nick} > No records found for {channel}") - return - - records = self.channel_records[channel] - - # Header - self.send_message(channel, f"{nick} > {self.colors['yellow']}📊 {channel.upper()} RECORDS 📊{self.colors['reset']}") - - # Fastest shot - fastest = records.get('fastest_shot') - if fastest: - self.send_message(channel, f"🏆 Fastest shot: {self.colors['green']}{fastest['time']:.3f}s{self.colors['reset']} by {self.colors['cyan']}{fastest['hunter']}{self.colors['reset']} ({fastest['duck_type']} duck)") - - # Total stats - total_ducks = records.get('total_ducks', 0) - total_shots = records.get('total_shots', 0) - accuracy = (total_ducks / total_shots * 100) if total_shots > 0 else 0 - - self.send_message(channel, f"📈 Total: {total_ducks} ducks shot, {total_shots} shots fired ({accuracy:.1f}% accuracy)") - - # Current difficulty - difficulty = self.duck_difficulty.get(channel, 1.0) - if difficulty > 1.0: - self.send_message(channel, f"🧠 Duck intelligence: {self.colors['red']}{difficulty:.2f}x harder{self.colors['reset']} (they're learning!)") - else: - self.send_message(channel, f"🧠 Duck intelligence: Normal (fresh ducks)") - - async def handle_shop(self, nick, channel, user): - player = self.get_player(user) - if not player: - self.send_message(channel, f"{nick} > Player data not found!") - return - - # Create organized shop display - shop_items = { - 'ammo': [ - {'id': '1', 'name': 'Extra bullet', 'cost': 7}, - {'id': '2', 'name': 'Extra clip', 'cost': 20}, - {'id': '3', 'name': 'AP ammo', 'cost': 15}, - {'id': '4', 'name': 'Explosive ammo', 'cost': 25} - ], - 'weapons': [ - {'id': '11', 'name': 'Shotgun', 'cost': 100}, - {'id': '12', 'name': 'Assault rifle', 'cost': 200}, - {'id': '13', 'name': 'Sniper rifle', 'cost': 350}, - {'id': '14', 'name': 'Auto shotgun', 'cost': 500} - ], - 'upgrades': [ - {'id': '6', 'name': 'Grease', 'cost': 8}, - {'id': '7', 'name': 'Sight', 'cost': 6}, - {'id': '8', 'name': 'Infrared detector', 'cost': 15}, - {'id': '9', 'name': 'Silencer', 'cost': 5}, - {'id': '10', 'name': 'Four-leaf clover', 'cost': 13} - ], - 'special': [ - {'id': '5', 'name': 'Repurchase gun', 'cost': 40}, - {'id': '15', 'name': 'Sand', 'cost': 7}, - {'id': '16', 'name': 'Water bucket', 'cost': 10}, - {'id': '17', 'name': 'Sabotage', 'cost': 14}, - {'id': '20', 'name': 'Decoy', 'cost': 80}, - {'id': '21', 'name': 'Bread', 'cost': 50}, - {'id': '22', 'name': 'Duck detector', 'cost': 50}, - {'id': '23', 'name': 'Mechanical duck', 'cost': 50} - ], - 'insurance': [ - {'id': '18', 'name': 'Life insurance', 'cost': 10}, - {'id': '19', 'name': 'Liability insurance', 'cost': 5} - ] - } - - # Format each category - def format_items(items, color): - formatted = [] - for item in items: - formatted.append(f"{color}{item['id']}{self.colors['reset']}.{item['name']}({color}{item['cost']}xp{self.colors['reset']})") - return ' '.join(formatted) - - # Send shop header - self.send_message(channel, f"{nick} > {self.colors['yellow']}═══ DUCK HUNT SHOP ═══{self.colors['reset']} Your XP: {self.colors['green']}{player['xp']}{self.colors['reset']}") - - # Send categorized items - self.send_message(channel, f"{self.colors['cyan']}Ammo:{self.colors['reset']} {format_items(shop_items['ammo'], self.colors['cyan'])}") - self.send_message(channel, f"{self.colors['red']}Weapons:{self.colors['reset']} {format_items(shop_items['weapons'], self.colors['red'])}") - self.send_message(channel, f"{self.colors['green']}Upgrades:{self.colors['reset']} {format_items(shop_items['upgrades'], self.colors['green'])}") - self.send_message(channel, f"{self.colors['yellow']}Special:{self.colors['reset']} {format_items(shop_items['special'], self.colors['yellow'])}") - self.send_message(channel, f"{self.colors['magenta']}Insurance:{self.colors['reset']} {format_items(shop_items['insurance'], self.colors['magenta'])}") - - # Footer - self.send_message(channel, f"{self.colors['white']}Use {self.colors['cyan']}!shop {self.colors['white']} to purchase {self.colors['reset']}") - - async def handle_buy(self, nick, channel, item, user): - """Buy items and add to inventory""" - player = self.get_player(user) - if not player: - self.send_message(channel, f"{nick} > Player data not found!") - return - - # Check if inventory system is enabled - if not self.get_config('economy.inventory_system_enabled', True): - self.send_message(channel, f"{nick} > Inventory system is disabled!") - return - - # Initialize inventory if not exists - if 'inventory' not in player: - player['inventory'] = {} - - # Eggdrop-style shop items with XP costs - shop_items = { - '1': {'name': 'Extra bullet', 'cost': 7}, - '2': {'name': 'Extra clip', 'cost': 20}, - '3': {'name': 'AP ammo', 'cost': 15}, - '4': {'name': 'Explosive ammo', 'cost': 25}, - '5': {'name': 'Repurchase confiscated gun', 'cost': 40}, - '6': {'name': 'Grease', 'cost': 8}, - '7': {'name': 'Sight', 'cost': 6}, - '8': {'name': 'Infrared detector', 'cost': 15}, - '9': {'name': 'Silencer', 'cost': 5}, - '10': {'name': 'Four-leaf clover', 'cost': 13}, - '11': {'name': 'Shotgun', 'cost': 100}, - '12': {'name': 'Assault rifle', 'cost': 200}, - '13': {'name': 'Sniper rifle', 'cost': 350}, - '14': {'name': 'Automatic shotgun', 'cost': 500}, - '15': {'name': 'Handful of sand', 'cost': 7}, - '16': {'name': 'Water bucket', 'cost': 10}, - '17': {'name': 'Sabotage', 'cost': 14}, - '18': {'name': 'Life insurance', 'cost': 10}, - '19': {'name': 'Liability insurance', 'cost': 5}, - '20': {'name': 'Decoy', 'cost': 80}, - '21': {'name': 'Piece of bread', 'cost': 50}, - '22': {'name': 'Ducks detector', 'cost': 50}, - '23': {'name': 'Mechanical duck', 'cost': 50} - } - - if item not in shop_items: - self.send_message(channel, f"{nick} > Invalid item ID. Use !shop to see available items.") - return - - shop_item = shop_items[item] - cost = shop_item['cost'] - - if player['xp'] < cost: - self.send_message(channel, f"{nick} > Not enough XP! You need {cost} XP but only have {player['xp']}.") - return - - # Check inventory space - max_slots = self.get_config('economy.max_inventory_slots', 20) - if max_slots is None: - max_slots = 20 - total_items = sum(player['inventory'].values()) - if total_items >= max_slots: - self.send_message(channel, f"{nick} > Inventory full! ({total_items}/{max_slots}) Use items or increase capacity.") - return - - # Purchase the item and add to inventory - player['xp'] -= cost - if item in player['inventory']: - player['inventory'][item] += 1 - else: - player['inventory'][item] = 1 - - self.send_message(channel, f"{nick} > Purchased {shop_item['name']}! Added to inventory ({total_items + 1}/{max_slots})") - - # Save to database after purchase - self.save_player(user) - - async def handle_sell(self, nick, channel, item_id, user): - """Sell items from inventory for 70% of original cost""" - player = self.get_player(user) - if not player: - self.send_message(channel, f"{nick} > Player data not found!") - return - - # Check if inventory system is enabled - if not self.get_config('economy.inventory_system_enabled', True): - self.send_message(channel, f"{nick} > Inventory system is disabled!") - return - - # Initialize inventory if not exists - if 'inventory' not in player: - player['inventory'] = {} - - # Check if item is in inventory - if item_id not in player['inventory'] or player['inventory'][item_id] <= 0: - self.send_message(channel, f"{nick} > You don't have that item! Check !stats to see your inventory.") - return - - # Get shop item data for pricing - shop_items = { - '1': {'name': 'Extra bullet', 'cost': 7}, - '2': {'name': 'Extra clip', 'cost': 20}, - '3': {'name': 'AP ammo', 'cost': 15}, - '4': {'name': 'Explosive ammo', 'cost': 25}, - '5': {'name': 'Repurchase confiscated gun', 'cost': 40}, - '6': {'name': 'Grease', 'cost': 8}, - '7': {'name': 'Sight', 'cost': 6}, - '8': {'name': 'Infrared detector', 'cost': 15}, - '9': {'name': 'Silencer', 'cost': 5}, - '10': {'name': 'Four-leaf clover', 'cost': 13}, - '11': {'name': 'Shotgun', 'cost': 100}, - '12': {'name': 'Assault rifle', 'cost': 200}, - '13': {'name': 'Sniper rifle', 'cost': 350}, - '14': {'name': 'Automatic shotgun', 'cost': 500}, - '15': {'name': 'Handful of sand', 'cost': 7}, - '16': {'name': 'Water bucket', 'cost': 10}, - '17': {'name': 'Sabotage', 'cost': 14}, - '18': {'name': 'Life insurance', 'cost': 10}, - '19': {'name': 'Liability insurance', 'cost': 5}, - '20': {'name': 'Decoy', 'cost': 80}, - '21': {'name': 'Piece of bread', 'cost': 50}, - '22': {'name': 'Ducks detector', 'cost': 50}, - '23': {'name': 'Mechanical duck', 'cost': 50} - } - - if item_id not in shop_items: - self.send_message(channel, f"{nick} > Invalid item ID!") - return - - shop_item = shop_items[item_id] - original_cost = shop_item['cost'] - sell_price = int(original_cost * 0.7) # 70% of original cost - - # Remove item from inventory - player['inventory'][item_id] -= 1 - if player['inventory'][item_id] <= 0: - del player['inventory'][item_id] - - # Give XP back - player['xp'] += sell_price - - total_items = sum(player['inventory'].values()) - max_slots = self.get_config('economy.max_inventory_slots', 20) - - self.send_message(channel, f"{nick} > Sold {shop_item['name']} for {sell_price}xp! Inventory: ({total_items}/{max_slots})") - - # Save to database after sale - self.save_player(user) - - async def handle_use(self, nick, channel, item_id, user, target_nick=None): - """Use an item from inventory""" - player = self.get_player(user) - if not player: - self.send_message(channel, f"{nick} > Player data not found!") - return - - # Check if item is in inventory - if item_id not in player['inventory'] or player['inventory'][item_id] <= 0: - self.send_message(channel, f"{nick} > You don't have that item! Check !stats to see your inventory.") - return - - # Get shop item data for reference - shop_items = { - '1': {'name': 'Extra bullet', 'effect': 'ammo'}, - '2': {'name': 'Extra clip', 'effect': 'max_ammo'}, - '3': {'name': 'AP ammo', 'effect': 'accuracy'}, - '4': {'name': 'Explosive ammo', 'effect': 'explosive'}, - '5': {'name': 'Repurchase confiscated gun', 'effect': 'gun'}, - '6': {'name': 'Grease', 'effect': 'reliability'}, - '7': {'name': 'Sight', 'effect': 'accuracy'}, - '8': {'name': 'Infrared detector', 'effect': 'detector'}, - '9': {'name': 'Silencer', 'effect': 'silencer'}, - '10': {'name': 'Four-leaf clover', 'effect': 'luck'}, - '11': {'name': 'Shotgun', 'effect': 'shotgun'}, - '12': {'name': 'Assault rifle', 'effect': 'rifle'}, - '13': {'name': 'Sniper rifle', 'effect': 'sniper'}, - '14': {'name': 'Automatic shotgun', 'effect': 'auto_shotgun'}, - '15': {'name': 'Handful of sand', 'effect': 'sand'}, - '16': {'name': 'Water bucket', 'effect': 'water'}, - '17': {'name': 'Sabotage', 'effect': 'sabotage'}, - '18': {'name': 'Life insurance', 'effect': 'life_insurance'}, - '19': {'name': 'Liability insurance', 'effect': 'liability'}, - '20': {'name': 'Decoy', 'effect': 'decoy'}, - '21': {'name': 'Piece of bread', 'effect': 'bread'}, - '22': {'name': 'Ducks detector', 'effect': 'duck_detector'}, - '23': {'name': 'Mechanical duck', 'effect': 'mechanical'} - } - - if item_id not in shop_items: - self.send_message(channel, f"{nick} > Invalid item ID!") - return - - shop_item = shop_items[item_id] - effect = shop_item['effect'] - - # Determine target player - if target_nick and target_nick.lower() != nick.lower(): - # Using on someone else - target_nick_lower = target_nick.lower() - if target_nick_lower not in self.players: - self.send_message(channel, f"{nick} > Player {target_nick} not found!") - return - target_player = self.players[target_nick_lower] - using_on_other = True - else: - # Using on self - target_player = player - target_nick = nick - using_on_other = False - - # Remove item from inventory - player['inventory'][item_id] -= 1 - if player['inventory'][item_id] <= 0: - del player['inventory'][item_id] - - # Apply item effects - if effect == 'ammo': - target_player['ammo'] = min(target_player['max_ammo'], target_player['ammo'] + 1) - if using_on_other: - self.send_message(channel, f"{nick} > Used {shop_item['name']} on {target_nick}! +1 ammo") - else: - self.send_message(channel, f"{nick} > Used {shop_item['name']}! +1 ammo") - elif effect == 'water': - # Water bucket - splash attack on target player - if using_on_other: - # Reduce target's accuracy temporarily - target_player['accuracy'] = max(10, target_player['accuracy'] - 15) - self.send_message(channel, f"{nick} > *SPLASH!* You soaked {target_nick} with water! Their accuracy reduced by 15%!") - else: - self.send_message(channel, f"{nick} > You splashed yourself with water... why?") - elif effect == 'sand': - # Handful of sand - blind target temporarily - if using_on_other: - target_player['accuracy'] = max(5, target_player['accuracy'] - 20) - self.send_message(channel, f"{nick} > *POCKET SAND!* You threw sand in {target_nick}'s eyes! Their accuracy reduced by 20%!") - else: - self.send_message(channel, f"{nick} > You threw sand in your own eyes... brilliant strategy!") - # Add more effects as needed... - else: - # Default effects for other items - self.send_message(channel, f"{nick} > Used {shop_item['name']}! (Effect: {effect})") - - # Save changes - self.save_player(user) - if using_on_other: - # Save target player too if different - target_user = f"{target_nick.lower()}!user@host" # Simplified - would need real user data - self.save_database() - - async def handle_trade(self, nick, channel, user, target_nick, item, amount): - """Trade items with other players""" - player = self.get_player(user) - if not player: - return - - try: - amount = int(amount) - except ValueError: - self.send_message(channel, f"{nick} > Amount must be a number!") - return - - if amount <= 0: - self.send_message(channel, f"{nick} > Amount must be positive!") - return - - if amount > 10000: # Prevent excessive amounts - self.send_message(channel, f"{nick} > Amount too large! Maximum: 10,000") - return - - # Find target player (simplified - would need to track online users in real implementation) - if item == 'coins': - if player['coins'] < amount: - self.send_message(channel, f"{nick} > You don't have {amount} coins!") - return - player['coins'] -= amount - self.send_message(channel, f"{nick} > Offering {amount} coins to {target_nick}. They can !accept or !decline.") - # In real implementation, store pending trade - - elif item == 'ammo': - if player['ammo'] < amount: - self.send_message(channel, f"{nick} > You don't have {amount} ammo!") - return - self.send_message(channel, f"{nick} > Offering {amount} ammo to {target_nick}.") - - elif item == 'chargers': - if player['chargers'] < amount: - self.send_message(channel, f"{nick} > You don't have {amount} chargers!") - return - self.send_message(channel, f"{nick} > Offering {amount} chargers to {target_nick}.") - - else: - self.send_message(channel, f"{nick} > Can't trade '{item}'. Use: coins, ammo, or chargers") - - self.save_player(user) - - async def handle_listplayers(self, nick, channel): - """Admin command to list all players""" - if not self.players: - self.send_message(channel, f"{nick} > No players in database.") - return - - player_list = [] - for nick_key, data in self.players.items(): - shot_count = data['caught'] - befriended_count = data.get('befriended', 0) - total_ducks = shot_count + befriended_count - player_list.append(f"{nick_key}(Ducks:{total_ducks},Shot:{shot_count},Befriended:{befriended_count})") - - players_str = " | ".join(player_list[:10]) # Limit to first 10 - if len(self.players) > 10: - players_str += f" ... and {len(self.players) - 10} more" - - self.send_message(channel, f"{nick} > Players: {players_str}") - - async def handle_ban(self, nick, channel, target_nick): - """Admin command to ban a player""" - target_nick_lower = target_nick.lower() - if target_nick_lower in self.players: - del self.players[target_nick_lower] - self.send_message(channel, f"{nick} > Banned and reset {target_nick}") - self.save_database() - else: - self.send_message(channel, f"{nick} > Player {target_nick} not found!") - - async def handle_reset(self, nick, channel, target_nick): - """Admin command to reset a player's stats""" - target_nick_lower = target_nick.lower() - if target_nick_lower in self.players: - # Reset to defaults - self.players[target_nick_lower] = { - 'caught': 0, 'ammo': 10, 'max_ammo': 10, - 'chargers': 2, 'max_chargers': 2, 'xp': 0, - 'accuracy': 85, 'reliability': 90, 'gun_level': 1, - 'luck': 0, 'gun_type': 'pistol' - } - self.send_message(channel, f"{nick} > Reset {target_nick}'s stats to defaults") - self.save_database() - else: - self.send_message(channel, f"{nick} > Player {target_nick} not found!") - - async def handle_reset_database(self, nick, channel, user): - """Admin command to reset entire database - requires confirmation""" - self.send_message(channel, f"{nick} > {self.colors['red']}⚠️ DATABASE RESET WARNING ⚠️{self.colors['reset']}") - self.send_message(channel, f"{nick} > This will DELETE ALL player data, statistics, and progress!") - self.send_message(channel, f"{nick} > {self.colors['yellow']}Players affected: {len(self.players)}{self.colors['reset']}") - self.send_message(channel, f"{nick} > To confirm, type: {self.colors['cyan']}!resetdb confirm DESTROY_ALL_DATA{self.colors['reset']}") - self.send_message(channel, f"{nick} > {self.colors['red']}This action CANNOT be undone!{self.colors['reset']}") - - async def handle_reset_database_confirm(self, nick, channel, user, confirmation): - """Confirm and execute database reset""" - if confirmation != "DESTROY_ALL_DATA": - self.send_message(channel, f"{nick} > {self.colors['red']}Incorrect confirmation code. Database reset cancelled.{self.colors['reset']}") - return - - # Log the reset action - self.logger.warning(f"DATABASE RESET initiated by admin {nick} - All player data will be destroyed") - - # Backup current database - import shutil - backup_name = f"duckhunt_backup_{int(time.time())}.json" - try: - shutil.copy2(self.db_file, backup_name) - self.send_message(channel, f"{nick} > {self.colors['cyan']}Database backed up to: {backup_name}{self.colors['reset']}") - except Exception as e: - self.logger.error(f"Failed to create backup: {e}") - self.send_message(channel, f"{nick} > {self.colors['red']}Warning: Could not create backup!{self.colors['reset']}") - - # Clear all data - player_count = len(self.players) - self.players.clear() - self.ducks.clear() - self.ignored_nicks.clear() - - # Save empty database - self.save_database() - - # Confirmation messages - self.send_message(channel, f"{nick} > {self.colors['green']}✅ DATABASE RESET COMPLETE{self.colors['reset']}") - self.send_message(channel, f"{nick} > {self.colors['yellow']}{player_count} player records deleted{self.colors['reset']}") - self.send_message(channel, f"{nick} > All ducks cleared, fresh start initiated") - self.logger.warning(f"Database reset completed by {nick} - {player_count} players deleted") - - async def handle_restart(self, nick, channel): - """Admin command to restart the bot""" - self.send_message(channel, f"{nick} > Restarting bot...") - self.logger.info(f"Bot restart requested by {nick}") - - # Close connections gracefully - if self.writer: - self.writer.close() - await self.writer.wait_closed() - - # Save any pending data - self.save_database() - - # Restart the Python process - self.logger.info("Restarting Python process...") - python = sys.executable - script = sys.argv[0] - args = sys.argv[1:] - - # Use subprocess to restart - subprocess.Popen([python, script] + args) - - # Exit current process - sys.exit(0) - - async def handle_quit(self, nick, channel): - """Admin command to quit the bot""" - self.send_message(channel, f"{nick} > Shutting down bot...") - self.logger.info(f"Bot shutdown requested by {nick}") - # Close connections gracefully - if self.writer: - self.writer.close() - await self.writer.wait_closed() - # Exit with code 0 for normal shutdown - import sys - sys.exit(0) - - async def handle_ignore(self, nick, channel, target_nick): - """Admin command to ignore a user""" - if target_nick in self.ignored_nicks: - self.send_message(channel, f"{nick} > {target_nick} is already ignored!") - return - - self.ignored_nicks.add(target_nick) - self.send_message(channel, f"{nick} > Now ignoring {target_nick}. Total ignored: {len(self.ignored_nicks)}") - self.logger.info(f"{nick} added {target_nick} to ignore list") - - async def handle_delignore(self, nick, channel, target_nick): - """Admin command to stop ignoring a user""" - if target_nick not in self.ignored_nicks: - self.send_message(channel, f"{nick} > {target_nick} is not ignored!") - return - - self.ignored_nicks.remove(target_nick) - self.send_message(channel, f"{nick} > No longer ignoring {target_nick}. Total ignored: {len(self.ignored_nicks)}") - self.logger.info(f"{nick} removed {target_nick} from ignore list") - - async def handle_admin_giveitem(self, nick, channel, target_nick, item): - """Admin command to give an item to a player""" - target_nick_lower = target_nick.lower() - - # Check if target exists - if target_nick_lower not in self.players: - self.send_message(channel, f"{nick} > Player {target_nick} not found!") - return - - # Shop items reference for item names - shop_items = { - '1': {'name': 'Extra bullet', 'effect': 'ammo'}, - '2': {'name': 'Extra clip', 'effect': 'max_ammo'}, - '3': {'name': 'AP ammo', 'effect': 'accuracy'}, - '4': {'name': 'Explosive ammo', 'effect': 'explosive'}, - '5': {'name': 'Repurchase confiscated gun', 'effect': 'gun'}, - '6': {'name': 'Grease', 'effect': 'reliability'}, - '7': {'name': 'Sight', 'effect': 'accuracy'}, - '8': {'name': 'Infrared detector', 'effect': 'detector'}, - '9': {'name': 'Silencer', 'effect': 'silencer'}, - '10': {'name': 'Four-leaf clover', 'effect': 'luck'}, - '11': {'name': 'Sunglasses', 'effect': 'sunglasses'}, - '12': {'name': 'Spare clothes', 'effect': 'clothes'}, - '13': {'name': 'Brush for gun', 'effect': 'brush'}, - '14': {'name': 'Mirror', 'effect': 'mirror'}, - '15': {'name': 'Handful of sand', 'effect': 'sand'}, - '16': {'name': 'Water bucket', 'effect': 'water'}, - '17': {'name': 'Sabotage', 'effect': 'sabotage'}, - '18': {'name': 'Life insurance', 'effect': 'life_insurance'}, - '19': {'name': 'Liability insurance', 'effect': 'liability'}, - '20': {'name': 'Decoy', 'effect': 'decoy'}, - '21': {'name': 'Piece of bread', 'effect': 'bread'}, - '22': {'name': 'Ducks detector', 'effect': 'duck_detector'}, - '23': {'name': 'Mechanical duck', 'effect': 'mechanical'} - } - - if item not in shop_items: - self.send_message(channel, f"{nick} > Invalid item ID '{item}'. Use item IDs 1-23.") - return - - target_player = self.players[target_nick_lower] - shop_item = shop_items[item] - effect = shop_item['effect'] - - # Apply the item effect - if effect == 'ammo': - target_player['ammo'] = min(target_player['ammo'] + 1, target_player['max_ammo']) - elif effect == 'max_ammo': - target_player['max_ammo'] += 1 - target_player['ammo'] = target_player['max_ammo'] # Fill ammo - elif effect == 'accuracy': - target_player['accuracy'] = min(target_player['accuracy'] + 5, 100) - elif effect == 'explosive': - target_player['explosive_ammo'] = True - elif effect == 'gun': - target_player['gun_confiscated'] = False - target_player['ammo'] = target_player['max_ammo'] - elif effect == 'reliability': - target_player['reliability'] = min(target_player['reliability'] + 5, 100) - elif effect == 'luck': - target_player['luck'] = target_player.get('luck', 0) + 1 - # Add other effects as needed - - self.send_message(channel, f"{nick} > {self.colors['green']}Gave {shop_item['name']} to {target_nick}!{self.colors['reset']}") - self.save_database() - - async def handle_admin_givexp(self, nick, channel, target_nick, amount): - """Admin command to give XP to a player""" - target_nick_lower = target_nick.lower() - - # Check if target exists - if target_nick_lower not in self.players: - self.send_message(channel, f"{nick} > Player {target_nick} not found!") - return - - try: - xp_amount = int(amount) - except ValueError: - self.send_message(channel, f"{nick} > Amount must be a number!") - return - - if abs(xp_amount) > 50000: # Prevent excessive XP changes - self.send_message(channel, f"{nick} > XP amount too large! Maximum: ±50,000") - return - - target_player = self.players[target_nick_lower] - old_xp = target_player['xp'] - target_player['xp'] = max(0, target_player['xp'] + xp_amount) # Prevent negative XP - - color = self.colors['green'] if xp_amount >= 0 else self.colors['red'] - sign = '+' if xp_amount >= 0 else '' - self.send_message(channel, f"{nick} > {color}Gave {sign}{xp_amount} XP to {target_nick}! (Total: {target_player['xp']} XP){self.colors['reset']}") - self.save_database() - - def get_duck_spawn_message(self): - """Get a random duck spawn message with different types""" - duck_types = [ - {"msg": "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O< QUACK", "type": "normal"}, # Normal duck - {"msg": "-._..-'`'°-,_,.-'`'°-,_,.-'`'°-,_,.-° \\_o< A duck waddles by! QUACK QUACK", "type": "normal"}, # Waddling duck - {"msg": "~~~°*°~~~°*°~~~°*°~~~ \\_O< SPLASH! A duck lands in the water! QUACK!", "type": "normal"}, # Water duck - {"msg": "***GOLDEN*** \\_O< *** A golden duck appears! *** QUACK QUACK! ***GOLDEN***", "type": "golden"}, # Golden duck (rare) - {"msg": "°~°*°~°*°~° \\_o< Brrr! A winter duck appears! QUACK!", "type": "normal"}, # Winter duck - {"msg": ".,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O< A spring duck blooms into view! QUACK!", "type": "normal"}, # Spring duck - {"msg": "***ZAP*** \\_O< BZZT! An electric duck sparks to life! QUACK! ***ZAP***", "type": "normal"}, # Electric duck - {"msg": "~*~*~*~ \\_o< A sleepy night duck appears... *yawn* quack...", "type": "normal"}, # Night duck - ] - - # Golden duck is rare (5% chance) - if random.random() < 0.05: - golden_duck = [d for d in duck_types if d["type"] == "golden"][0] - return golden_duck - else: - # Choose from normal duck types - normal_ducks = [d for d in duck_types if d["type"] == "normal"] - return random.choice(normal_ducks) - - async def spawn_duck_now(self, channel, force_golden=False): - """Admin command to spawn a duck immediately""" - # Create duck with unique ID and type - duck_id = str(uuid.uuid4())[:8] # Short ID for easier tracking - - if force_golden: - # Force spawn a golden duck - duck_info = { - "msg": f"{self.colors['yellow']}***GOLDEN***{self.colors['reset']} \\_$< {self.colors['yellow']}*** A golden duck appears! ***{self.colors['reset']} QUACK QUACK! {self.colors['yellow']}***GOLDEN***{self.colors['reset']}", - "type": "golden" - } - else: - duck_info = self.get_duck_spawn_message() - - duck_timeout = random.randint(self.duck_timeout_min, self.duck_timeout_max) - duck = { - 'alive': True, - 'spawn_time': time.time(), - 'id': duck_id, - 'type': duck_info['type'], - 'message': duck_info['msg'], - 'timeout': duck_timeout - } - - # Initialize channel duck list if needed - if channel not in self.ducks: - self.ducks[channel] = [] - - # Add duck to channel - self.ducks[channel].append(duck) - - # Send spawn message - self.send_message(channel, duck_info['msg']) - self.logger.info(f"Admin spawned {duck_info['type']} duck {duck_id} in {channel}") - return True - return True # Return True to indicate duck was spawned - - async def spawn_ducks(self): - # Spawn first duck immediately after joining - await asyncio.sleep(5) # Brief delay for players to see the bot joined - for channel in self.channels_joined: - await self.spawn_duck_now(channel) - - # Start duck timeout checker - asyncio.create_task(self.duck_timeout_checker()) - - while not self.shutdown_requested: - wait_time = random.randint(self.duck_spawn_min, self.duck_spawn_max) - self.logger.info(f"Waiting {wait_time//60}m {wait_time%60}s for next duck") - - # Set next spawn time for all channels - next_spawn_time = time.time() + wait_time - for channel in self.channels_joined: - self.next_duck_spawn[channel] = next_spawn_time - - # Sleep in chunks to check shutdown flag - for _ in range(wait_time): - if self.shutdown_requested: - self.logger.info("Duck spawning stopped due to shutdown request") - return - await asyncio.sleep(1) - - # Spawn only one duck per channel if no alive ducks exist - for channel in self.channels_joined: - if self.shutdown_requested: - return - - # Check if there are any alive ducks in this channel - channel_ducks = self.ducks.get(channel, []) - alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] - - # Only spawn if no ducks are alive (one duck at a time naturally) - if not alive_ducks: - await self.spawn_duck_now(channel) - break # Only spawn in the first available channel - - async def duck_timeout_checker(self): - """Remove ducks that have been around too long""" - while not self.shutdown_requested: - await asyncio.sleep(10) # Check every 10 seconds - current_time = time.time() - - for channel in list(self.ducks.keys()): - if channel in self.ducks: - ducks_to_remove = [] - for i, duck in enumerate(self.ducks[channel]): - duck_timeout = duck.get('timeout', 60) # Use individual timeout or default to 60 - if duck['alive'] and (current_time - duck['spawn_time']) > duck_timeout: - # Duck wandered off - ducks_to_remove.append(i) - self.send_message(channel, f"A duck wandered off... *quack quack* (timeout after {duck_timeout}s)") - self.logger.info(f"Duck {duck['id']} timed out in {channel}") - - # Remove timed out ducks (in reverse order to maintain indices) - for i in reversed(ducks_to_remove): - del self.ducks[channel][i] - - async def listen(self): - """Listen for IRC messages with shutdown handling""" - while not self.shutdown_requested: - try: - if not self.reader: - self.logger.error("No reader available") - break - - # Use timeout to allow checking shutdown flag - try: - line = await asyncio.wait_for(self.reader.readline(), timeout=1.0) - except asyncio.TimeoutError: - continue # Check shutdown flag - - if not line: - self.logger.warning("Connection closed by server") - break - - line = line.decode(errors='ignore').strip() - if not line: - continue - - self.logger.debug(f"<- {line}") - - if line.startswith('PING'): - self.send_raw('PONG ' + line.split()[1]) - continue - - prefix, command, params, trailing = parse_message(line) - - except Exception as e: - self.logger.error(f"Error in listen loop: {e}") - await asyncio.sleep(1) # Brief pause before retry - continue - - # Handle SASL authentication responses - if command == 'CAP': - await self.sasl_handler.handle_cap_response(params, trailing) - - elif command == 'AUTHENTICATE': - await self.sasl_handler.handle_authenticate_response(params) - - elif command in ['903', '904', '905', '906', '907', '908']: # SASL responses - await self.sasl_handler.handle_sasl_result(command, params, trailing) - - elif command == '001': # Welcome - self.registered = True - auth_status = " (SASL authenticated)" if self.sasl_handler.is_authenticated() else "" - self.logger.info(f"Successfully registered!{auth_status}") - - # If SASL failed, try NickServ identification - if not self.sasl_handler.is_authenticated(): - await self.attempt_nickserv_auth() - - for chan in self.config['channels']: - self.logger.info(f"Joining {chan}") - self.send_raw(f'JOIN {chan}') - - elif command == 'JOIN' and prefix and prefix.startswith(self.config['nick']): - channel = trailing or (params[0] if params else '') - if channel: - self.channels_joined.add(channel) - self.logger.info(f"Successfully joined {channel}") - - elif command == 'PRIVMSG' and trailing: - target = params[0] if params else '' - sender = prefix.split('!')[0] if prefix else '' - - # Handle NickServ responses - if sender.lower() == 'nickserv': - await self.handle_nickserv_response(trailing) - elif trailing == 'VERSION': - self.send_raw(f'NOTICE {sender} :VERSION DuckHunt Bot v1.0') - else: - await self.handle_command(prefix, target, trailing) - - async def cleanup(self): - """Enhanced cleanup with graceful shutdown""" - self.logger.info("Starting cleanup process...") - - try: - # Cancel all running tasks - for task in self.running_tasks.copy(): - if not task.done(): - self.logger.debug(f"Cancelling task: {task.get_name()}") - task.cancel() - try: - await task - except asyncio.CancelledError: - pass - except Exception as e: - self.logger.error(f"Error cancelling task: {e}") - - # Send goodbye message to all channels - if self.writer and not self.writer.is_closing(): - for channel in self.channels_joined: - self.send_message(channel, "🦆 DuckHunt Bot shutting down. Thanks for playing! 🦆") - await asyncio.sleep(0.1) # Brief delay between messages - - self.send_raw('QUIT :DuckHunt Bot shutting down gracefully') - await asyncio.sleep(1.0) # Give time for QUIT and messages to send - - self.writer.close() - await self.writer.wait_closed() - self.logger.info("IRC connection closed") - - # Final database save with verification - self.save_database() - self.logger.info(f"Final database save completed - {len(self.players)} players saved") - - # Clear in-memory data - self.players.clear() - self.ducks.clear() - self.command_cooldowns.clear() - - self.logger.info("Cleanup completed successfully") - - except Exception as e: - self.logger.error(f"Error during cleanup: {e}") - import traceback - traceback.print_exc() - - async def run(self): - """Main bot entry point with enhanced shutdown handling""" - try: - # Setup signal handlers - self.setup_signal_handlers() - - self.logger.info("Starting DuckHunt Bot...") - self.load_database() - await self.connect() - - # Create and track main tasks - listen_task = asyncio.create_task(self.listen(), name="listen") - duck_task = asyncio.create_task(self.wait_and_spawn_ducks(), name="duck_spawner") - - self.running_tasks.add(listen_task) - self.running_tasks.add(duck_task) - - # Main execution loop with shutdown monitoring - done, pending = await asyncio.wait( - [listen_task, duck_task], - return_when=asyncio.FIRST_COMPLETED - ) - - # If we get here, one task completed (likely due to error or shutdown) - if self.shutdown_requested: - self.logger.info("Shutdown requested, stopping all tasks...") - else: - self.logger.warning("A main task completed unexpectedly") - - # Cancel remaining tasks - for task in pending: - task.cancel() - try: - await task - except asyncio.CancelledError: - pass - - except KeyboardInterrupt: - self.logger.info("Keyboard interrupt received") - self.shutdown_requested = True - except Exception as e: - self.logger.error(f"Fatal error in main loop: {e}") - import traceback - traceback.print_exc() - finally: - await self.cleanup() - - async def wait_and_spawn_ducks(self): - """Duck spawning with shutdown handling""" - # Wait for registration and channel joins - while not self.registered or not self.channels_joined and not self.shutdown_requested: - await asyncio.sleep(1) - - if self.shutdown_requested: - return - - self.logger.info("Starting duck spawning...") - await self.spawn_ducks() - -def main(): - """Enhanced main entry point with better shutdown handling""" - bot = None - try: - # Load configuration - with open('config.json') as f: - config = json.load(f) - - # Create bot instance - bot = SimpleIRCBot(config) - bot.logger.info("DuckHunt Bot initializing...") - - # Run bot with graceful shutdown - try: - asyncio.run(bot.run()) - except KeyboardInterrupt: - bot.logger.info("Keyboard interrupt received in main") - except Exception as e: - bot.logger.error(f"Runtime error: {e}") - import traceback - traceback.print_exc() - - bot.logger.info("DuckHunt Bot shutdown complete") - - except KeyboardInterrupt: - print("\n🦆 DuckHunt Bot stopped by user") - except FileNotFoundError: - print("❌ Error: config.json not found") - print("Please create a config.json file with your IRC server settings") - except json.JSONDecodeError as e: - print(f"❌ Error: Invalid config.json - {e}") - print("Please check your config.json file syntax") - except Exception as e: - print(f"💥 Unexpected error: {e}") - import traceback - traceback.print_exc() - finally: - # Ensure final message - print("🦆 Thanks for using DuckHunt Bot!") - -if __name__ == '__main__': - main() diff --git a/duckhunt/src/__pycache__/auth.cpython-312.pyc b/duckhunt/src/__pycache__/auth.cpython-312.pyc deleted file mode 100644 index fc64163..0000000 Binary files a/duckhunt/src/__pycache__/auth.cpython-312.pyc and /dev/null differ diff --git a/duckhunt/src/__pycache__/db.cpython-312.pyc b/duckhunt/src/__pycache__/db.cpython-312.pyc deleted file mode 100644 index a8557e0..0000000 Binary files a/duckhunt/src/__pycache__/db.cpython-312.pyc and /dev/null differ diff --git a/duckhunt/src/__pycache__/duckhuntbot.cpython-312.pyc b/duckhunt/src/__pycache__/duckhuntbot.cpython-312.pyc deleted file mode 100644 index efdb75e..0000000 Binary files a/duckhunt/src/__pycache__/duckhuntbot.cpython-312.pyc and /dev/null differ diff --git a/duckhunt/src/__pycache__/game.cpython-312.pyc b/duckhunt/src/__pycache__/game.cpython-312.pyc deleted file mode 100644 index ceda336..0000000 Binary files a/duckhunt/src/__pycache__/game.cpython-312.pyc and /dev/null differ diff --git a/duckhunt/src/__pycache__/items.cpython-312.pyc b/duckhunt/src/__pycache__/items.cpython-312.pyc deleted file mode 100644 index 8e4b18c..0000000 Binary files a/duckhunt/src/__pycache__/items.cpython-312.pyc and /dev/null differ diff --git a/duckhunt/src/__pycache__/logging_utils.cpython-312.pyc b/duckhunt/src/__pycache__/logging_utils.cpython-312.pyc deleted file mode 100644 index 360b144..0000000 Binary files a/duckhunt/src/__pycache__/logging_utils.cpython-312.pyc and /dev/null differ diff --git a/duckhunt/src/__pycache__/utils.cpython-312.pyc b/duckhunt/src/__pycache__/utils.cpython-312.pyc deleted file mode 100644 index 67198ac..0000000 Binary files a/duckhunt/src/__pycache__/utils.cpython-312.pyc and /dev/null differ diff --git a/duckhunt/src/auth.py b/duckhunt/src/auth.py deleted file mode 100644 index d04dcba..0000000 --- a/duckhunt/src/auth.py +++ /dev/null @@ -1,108 +0,0 @@ -import hashlib -import secrets -import asyncio -from typing import Optional -from src.db import DuckDB - -class AuthSystem: - def __init__(self, db): - self.db = db - self.bot = None # Will be set by the bot - self.authenticated_users = {} # nick -> account_name - self.pending_registrations = {} # nick -> temp_data - - def hash_password(self, password: str, salt: Optional[str] = None) -> tuple: - if salt is None: - salt = secrets.token_hex(16) - hashed = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000) - return hashed.hex(), salt - - def verify_password(self, password: str, hashed: str, salt: str) -> bool: - test_hash, _ = self.hash_password(password, salt) - return test_hash == hashed - - def register_account(self, username: str, password: str, nick: str, hostmask: str) -> bool: - # Check if account exists - existing = self.db.load_account(username) - if existing: - return False - - hashed_pw, salt = self.hash_password(password) - account_data = { - 'username': username, - 'password_hash': hashed_pw, - 'salt': salt, - 'primary_nick': nick, - 'hostmask': hostmask, - 'created_at': None, # Set by DB - 'auth_method': 'password' # 'password', 'nickserv', 'hostmask' - } - - self.db.save_account(username, account_data) - return True - - def authenticate(self, username: str, password: str, nick: str) -> bool: - account = self.db.load_account(username) - if not account: - return False - - if self.verify_password(password, account['password_hash'], account['salt']): - self.authenticated_users[nick] = username - return True - return False - - def get_account_for_nick(self, nick: str) -> str: - return self.authenticated_users.get(nick, "") - - def is_authenticated(self, nick: str) -> bool: - return nick in self.authenticated_users - - def logout(self, nick: str): - if nick in self.authenticated_users: - del self.authenticated_users[nick] - - def set_bot(self, bot): - """Set the bot instance for sending messages""" - self.bot = bot - - async def attempt_nickserv_auth(self): - """Attempt NickServ identification as fallback""" - if not self.bot: - return - - sasl_config = self.bot.config.get('sasl', {}) - username = sasl_config.get('username', '') - password = sasl_config.get('password', '') - - if username and password: - self.bot.logger.info(f"Attempting NickServ identification for {username}") - # Try both common NickServ commands - self.bot.send_raw(f'PRIVMSG NickServ :IDENTIFY {username} {password}') - # Some networks use just the password if nick matches - await asyncio.sleep(1) - self.bot.send_raw(f'PRIVMSG NickServ :IDENTIFY {password}') - self.bot.logger.info("NickServ identification commands sent") - else: - self.bot.logger.debug("No SASL credentials available for NickServ fallback") - - async def handle_nickserv_response(self, message): - """Handle responses from NickServ""" - if not self.bot: - return - - message_lower = message.lower() - - if any(phrase in message_lower for phrase in [ - 'you are now identified', 'password accepted', 'you are already identified', - 'authentication successful', 'you have been identified' - ]): - self.bot.logger.info("NickServ identification successful!") - - elif any(phrase in message_lower for phrase in [ - 'invalid password', 'incorrect password', 'access denied', - 'authentication failed', 'not registered', 'nickname is not registered' - ]): - self.bot.logger.error(f"NickServ identification failed: {message}") - - else: - self.bot.logger.debug(f"NickServ message: {message}") diff --git a/duckhunt/src/db.py b/duckhunt/src/db.py deleted file mode 100644 index 771b9af..0000000 --- a/duckhunt/src/db.py +++ /dev/null @@ -1,97 +0,0 @@ -import sqlite3 -import json -import datetime - -class DuckDB: - def __init__(self, db_path='duckhunt.db'): - self.conn = sqlite3.connect(db_path) - self.create_tables() - - def create_tables(self): - with self.conn: - # Player data table - self.conn.execute('''CREATE TABLE IF NOT EXISTS players ( - nick TEXT PRIMARY KEY, - data TEXT - )''') - - # Account system table - self.conn.execute('''CREATE TABLE IF NOT EXISTS accounts ( - username TEXT PRIMARY KEY, - data TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )''') - - # Leaderboards table - self.conn.execute('''CREATE TABLE IF NOT EXISTS leaderboard ( - account TEXT, - stat_type TEXT, - value INTEGER, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (account, stat_type) - )''') - - # Trading table - self.conn.execute('''CREATE TABLE IF NOT EXISTS trades ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - from_account TEXT, - to_account TEXT, - trade_data TEXT, - status TEXT DEFAULT 'pending', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )''') - - def save_player(self, nick, data): - with self.conn: - self.conn.execute('''INSERT OR REPLACE INTO players (nick, data) VALUES (?, ?)''', - (nick, json.dumps(data))) - - def load_player(self, nick): - cur = self.conn.cursor() - cur.execute('SELECT data FROM players WHERE nick=?', (nick,)) - row = cur.fetchone() - return json.loads(row[0]) if row else None - - def get_all_players(self): - cur = self.conn.cursor() - cur.execute('SELECT nick, data FROM players') - return {nick: json.loads(data) for nick, data in cur.fetchall()} - - def save_account(self, username, data): - with self.conn: - self.conn.execute('''INSERT OR REPLACE INTO accounts (username, data) VALUES (?, ?)''', - (username, json.dumps(data))) - - def load_account(self, username): - cur = self.conn.cursor() - cur.execute('SELECT data FROM accounts WHERE username=?', (username,)) - row = cur.fetchone() - return json.loads(row[0]) if row else None - - def update_leaderboard(self, account, stat_type, value): - with self.conn: - self.conn.execute('''INSERT OR REPLACE INTO leaderboard (account, stat_type, value) VALUES (?, ?, ?)''', - (account, stat_type, value)) - - def get_leaderboard(self, stat_type, limit=10): - cur = self.conn.cursor() - cur.execute('SELECT account, value FROM leaderboard WHERE stat_type=? ORDER BY value DESC LIMIT ?', - (stat_type, limit)) - return cur.fetchall() - - def save_trade(self, from_account, to_account, trade_data): - with self.conn: - cur = self.conn.cursor() - cur.execute('''INSERT INTO trades (from_account, to_account, trade_data) VALUES (?, ?, ?)''', - (from_account, to_account, json.dumps(trade_data))) - return cur.lastrowid - - def get_pending_trades(self, account): - cur = self.conn.cursor() - cur.execute('''SELECT id, from_account, trade_data FROM trades - WHERE to_account=? AND status='pending' ''', (account,)) - return [(trade_id, from_acc, json.loads(data)) for trade_id, from_acc, data in cur.fetchall()] - - def complete_trade(self, trade_id): - with self.conn: - self.conn.execute('UPDATE trades SET status=? WHERE id=?', ('completed', trade_id)) diff --git a/duckhunt/src/duckhuntbot.py b/duckhunt/src/duckhuntbot.py deleted file mode 100644 index e7ed35d..0000000 --- a/duckhunt/src/duckhuntbot.py +++ /dev/null @@ -1,277 +0,0 @@ -#!/usr/bin/env python3 -""" -Main DuckHunt IRC Bot using modular architecture -""" - -import asyncio -import ssl -import json -import random -import logging -import sys -import os -import time -import uuid -import signal -from typing import Optional - -from .logging_utils import setup_logger -from .utils import parse_message -from .db import DuckDB -from .game import DuckGame -from .auth import AuthSystem -from . import sasl - -class IRCBot: - def __init__(self, config): - self.config = config - self.logger = setup_logger("DuckHuntBot") - self.reader: Optional[asyncio.StreamReader] = None - self.writer: Optional[asyncio.StreamWriter] = None - self.registered = False - self.channels_joined = set() - self.shutdown_requested = False - self.running_tasks = set() - - # Initialize subsystems - self.db = DuckDB() - self.game = DuckGame(self, self.db) - self.auth = AuthSystem(self.db) - self.auth.set_bot(self) # Set bot reference for auth system - self.sasl_handler = sasl.SASLHandler(self, config) - - # IRC connection state - self.nick = config['nick'] - self.channels = config['channels'] - - def send_raw(self, msg): - """Send raw IRC message""" - if self.writer and not self.writer.is_closing(): - try: - self.writer.write(f"{msg}\r\n".encode('utf-8')) - except Exception as e: - self.logger.error(f"Error sending message: {e}") - - def send_message(self, target, msg): - """Send PRIVMSG to target""" - self.send_raw(f'PRIVMSG {target} :{msg}') - - async def connect(self): - """Connect to IRC server with SASL support""" - server = self.config['server'] - port = self.config['port'] - ssl_context = ssl.create_default_context() if self.config.get('ssl', True) else None - - self.logger.info(f"Connecting to {server}:{port} (SSL: {ssl_context is not None})") - - try: - self.reader, self.writer = await asyncio.open_connection( - server, port, ssl=ssl_context - ) - self.logger.info("Connected successfully!") - - # Start SASL negotiation if enabled - if await self.sasl_handler.start_negotiation(): - return True - else: - # Standard registration without SASL - await self.register_user() - return True - - except Exception as e: - self.logger.error(f"Connection failed: {e}") - return False - - async def register_user(self): - """Register with IRC server""" - self.logger.info(f"Registering as {self.nick}") - self.send_raw(f'NICK {self.nick}') - self.send_raw(f'USER {self.nick} 0 * :DuckHunt Bot') - - # Send password if configured (for servers that require it) - if self.config.get('password'): - self.send_raw(f'PASS {self.config["password"]}') - - async def handle_irc_message(self, line): - """Handle individual IRC message""" - try: - prefix, command, params, trailing = parse_message(line) - - # Handle SASL-related messages - if command in ['CAP', 'AUTHENTICATE', '903', '904', '905', '906', '907', '908']: - handled = await self.sasl_handler.handle_sasl_result(command, params, trailing) - if command == 'CAP': - handled = await self.sasl_handler.handle_cap_response(params, trailing) - elif command == 'AUTHENTICATE': - handled = await self.sasl_handler.handle_authenticate_response(params) - - # If SASL handler didn't handle it, continue with normal processing - if handled: - return - - # Handle standard IRC messages - if command == '001': # Welcome - self.registered = True - auth_status = " (SASL authenticated)" if self.sasl_handler.is_authenticated() else "" - self.logger.info(f"Successfully registered!{auth_status}") - - # If SASL failed, try NickServ identification - if not self.sasl_handler.is_authenticated(): - await self.auth.attempt_nickserv_auth() - - # Join channels - for chan in self.channels: - self.logger.info(f"Joining {chan}") - self.send_raw(f'JOIN {chan}') - - elif command == 'JOIN' and prefix and prefix.startswith(self.nick): - channel = trailing or (params[0] if params else '') - if channel: - self.channels_joined.add(channel) - self.logger.info(f"Successfully joined {channel}") - - elif command == 'PRIVMSG' and trailing: - target = params[0] if params else '' - sender = prefix.split('!')[0] if prefix else '' - - # Handle NickServ responses - if sender.lower() == 'nickserv': - await self.auth.handle_nickserv_response(trailing) - elif trailing == 'VERSION': - self.send_raw(f'NOTICE {sender} :VERSION DuckHunt Bot v2.0') - else: - # Handle game commands - await self.game.handle_command(prefix, target, trailing) - - elif command == 'PING': - # Respond to PING - self.send_raw(f'PONG :{trailing}') - - except Exception as e: - self.logger.error(f"Error handling IRC message '{line}': {e}") - - async def listen(self): - """Main IRC message listening loop""" - buffer = "" - - while not self.shutdown_requested: - try: - if not self.reader: - break - - data = await self.reader.read(4096) - if not data: - self.logger.warning("Connection closed by server") - break - - buffer += data.decode('utf-8', errors='ignore') - - while '\n' in buffer: - line, buffer = buffer.split('\n', 1) - line = line.rstrip('\r') - - if line: - await self.handle_irc_message(line) - - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"Error in listen loop: {e}") - await asyncio.sleep(1) - - def setup_signal_handlers(self): - """Setup signal handlers for graceful shutdown""" - def signal_handler(signum, frame): - self.logger.info(f"Received signal {signum}, initiating graceful shutdown...") - self.shutdown_requested = True - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - async def cleanup(self): - """Cleanup resources and save data""" - self.logger.info("Starting cleanup process...") - - try: - # Cancel all running tasks - for task in self.running_tasks.copy(): - if not task.done(): - task.cancel() - try: - await task - except asyncio.CancelledError: - pass - - # Send goodbye message - if self.writer and not self.writer.is_closing(): - for channel in self.channels_joined: - self.send_message(channel, "🦆 DuckHunt Bot shutting down. Thanks for playing! 🦆") - await asyncio.sleep(0.1) - - self.send_raw('QUIT :DuckHunt Bot shutting down gracefully') - await asyncio.sleep(1.0) - - self.writer.close() - await self.writer.wait_closed() - self.logger.info("IRC connection closed") - - # Save database (no specific save_all method) - # Players are saved individually through the game engine - self.logger.info("Final database save completed") - - self.logger.info("Cleanup completed successfully") - - except Exception as e: - self.logger.error(f"Error during cleanup: {e}") - - async def run(self): - """Main bot entry point""" - try: - self.setup_signal_handlers() - - self.logger.info("Starting DuckHunt Bot...") - - # Load database (no async initialization needed) - # Database is initialized in constructor - - # Connect to IRC - if not await self.connect(): - return False - - # Create main tasks - listen_task = asyncio.create_task(self.listen(), name="listen") - game_task = asyncio.create_task(self.game.spawn_ducks_loop(), name="duck_spawner") - - self.running_tasks.add(listen_task) - self.running_tasks.add(game_task) - - # Wait for completion - done, pending = await asyncio.wait( - [listen_task, game_task], - return_when=asyncio.FIRST_COMPLETED - ) - - if self.shutdown_requested: - self.logger.info("Shutdown requested, stopping all tasks...") - else: - self.logger.warning("A main task completed unexpectedly") - - # Cancel remaining tasks - for task in pending: - task.cancel() - try: - await task - except asyncio.CancelledError: - pass - - except KeyboardInterrupt: - self.logger.info("Keyboard interrupt received") - self.shutdown_requested = True - except Exception as e: - self.logger.error(f"Fatal error in main loop: {e}") - import traceback - traceback.print_exc() - finally: - await self.cleanup() - - return True diff --git a/duckhunt/src/game.py b/duckhunt/src/game.py deleted file mode 100644 index ffd02f5..0000000 --- a/duckhunt/src/game.py +++ /dev/null @@ -1,566 +0,0 @@ -import asyncio -import random -from src.items import DuckTypes, WeaponTypes, AmmoTypes, Attachments -from src.auth import AuthSystem - -class DuckGame: - def __init__(self, bot, db): - self.bot = bot - self.config = bot.config - self.logger = getattr(bot, 'logger', None) - self.db = db - self.auth = AuthSystem(db) - self.duck_spawn_min = self.config.get('duck_spawn_min', 30) - self.duck_spawn_max = self.config.get('duck_spawn_max', 120) - self.ducks = {} # channel: duck dict or None - self.players = {} # nick: player dict - self.duck_alerts = set() # nicks who want duck alerts - - def get_player(self, nick): - if nick in self.players: - return self.players[nick] - data = self.db.load_player(nick) - if data: - data['friends'] = set(data.get('friends', [])) - self.players[nick] = data - return data - default = { - 'ammo': 1, 'max_ammo': 1, 'friends': set(), 'caught': 0, 'coins': 100, - 'accuracy': 70, 'reliability': 80, 'gun_oil': 0, 'scope': False, - 'silencer': False, 'lucky_charm': False, 'xp': 0, 'level': 1, - 'bank_account': 0, 'insurance': {'active': False, 'claims': 0}, - 'weapon': 'basic_gun', 'weapon_durability': 100, 'ammo_type': 'standard', - 'attachments': [], 'hunting_license': {'active': False, 'expires': None}, - 'duck_alerts': False, 'auth_method': 'nick' # 'nick', 'hostmask', 'account' - } - self.players[nick] = default - return default - - def save_player(self, nick, data): - self.players[nick] = data - data_to_save = dict(data) - data_to_save['friends'] = list(data_to_save.get('friends', [])) - self.db.save_player(nick, data_to_save) - - async def spawn_ducks_loop(self): - while True: - wait_time = random.randint(self.duck_spawn_min, self.duck_spawn_max) - if self.logger: - self.logger.info(f"Waiting {wait_time}s before next duck spawn.") - await asyncio.sleep(wait_time) - for chan in self.bot.channels: - duck = self.ducks.get(chan) - if not (duck and duck.get('alive')): - duck_type = DuckTypes.get_random_duck() - self.ducks[chan] = { - 'alive': True, - 'type': duck_type, - 'health': duck_type['health'], - 'max_health': duck_type['health'] - } - if self.logger: - self.logger.info(f"{duck_type['name']} spawned in {chan}") - - spawn_msg = f'\033[93m{duck_type["emoji"]} A {duck_type["name"]} appears! Type !bang, !catch, !bef, or !reload!\033[0m' - await self.bot.send_message(chan, spawn_msg) - - # Alert subscribed players - if self.duck_alerts: - alert_msg = f"🦆 DUCK ALERT: {duck_type['name']} in {chan}!" - for alert_nick in self.duck_alerts: - try: - await self.bot.send_message(alert_nick, alert_msg) - except: - pass # User might be offline - - async def handle_command(self, user, channel, message): - nick = user.split('!')[0] if user else 'unknown' - hostmask = user if user else 'unknown' - cmd = message.strip().lower() - if self.logger: - self.logger.info(f"{nick}@{channel}: {cmd}") - - # Handle private message commands - if channel == self.bot.nick: # Private message - if cmd.startswith('identify '): - parts = cmd.split(' ', 2) - if len(parts) == 3: - await self.handle_identify(nick, parts[1], parts[2]) - else: - await self.bot.send_message(nick, "Usage: identify ") - return - elif cmd == 'register': - await self.bot.send_message(nick, "To register: /msg me register ") - return - elif cmd.startswith('register '): - parts = cmd.split(' ', 2) - if len(parts) == 3: - await self.handle_register(nick, hostmask, parts[1], parts[2]) - else: - await self.bot.send_message(nick, "Usage: register ") - return - - # Public channel commands - if cmd == '!bang': - await self.handle_bang(nick, channel) - elif cmd == '!reload': - await self.handle_reload(nick, channel) - elif cmd == '!bef': - await self.handle_bef(nick, channel) - elif cmd == '!catch': - await self.handle_catch(nick, channel) - elif cmd == '!shop': - await self.handle_shop(nick, channel) - elif cmd == '!duckstats': - await self.handle_duckstats(nick, channel) - elif cmd.startswith('!buy '): - item_num = cmd.split(' ', 1)[1] - await self.handle_buy(nick, channel, item_num) - elif cmd.startswith('!sell '): - item_num = cmd.split(' ', 1)[1] - await self.handle_sell(nick, channel, item_num) - elif cmd == '!stats': - await self.handle_stats(nick, channel) - elif cmd == '!help': - await self.handle_help(nick, channel) - elif cmd == '!leaderboard' or cmd == '!top': - await self.handle_leaderboard(nick, channel) - elif cmd == '!bank': - await self.handle_bank(nick, channel) - elif cmd == '!license': - await self.handle_license(nick, channel) - elif cmd == '!alerts': - await self.handle_alerts(nick, channel) - elif cmd.startswith('!trade '): - parts = cmd.split(' ', 2) - if len(parts) >= 2: - await self.handle_trade(nick, channel, parts[1:]) - elif cmd.startswith('!sabotage '): - target = cmd.split(' ', 1)[1] - await self.handle_sabotage(nick, channel, target) - - async def handle_bang(self, nick, channel): - player = self.get_player(nick) - duck = self.ducks.get(channel) - if player['ammo'] <= 0: - await self.bot.send_message(channel, f'\033[91m{nick}, you need to !reload!\033[0m') - return - if duck and duck.get('alive'): - player['ammo'] -= 1 - - # Calculate hit chance based on accuracy and upgrades - base_accuracy = player['accuracy'] - if player['scope']: - base_accuracy += 15 - if player['lucky_charm']: - base_accuracy += 10 - - hit_roll = random.randint(1, 100) - if hit_roll <= base_accuracy: - player['caught'] += 1 - coins_earned = 1 - if player['silencer']: - coins_earned += 1 # Bonus for silencer - player['coins'] += coins_earned - self.ducks[channel] = {'alive': False} - await self.bot.send_message(channel, f'\033[92m{nick} shot the duck! (+{coins_earned} coin{"s" if coins_earned > 1 else ""})\033[0m') - if self.logger: - self.logger.info(f"{nick} shot a duck in {channel}") - else: - await self.bot.send_message(channel, f'\033[93m{nick} missed the duck!\033[0m') - else: - await self.bot.send_message(channel, f'No duck to shoot, {nick}!') - self.save_player(nick, player) - - async def handle_reload(self, nick, channel): - player = self.get_player(nick) - - # Check gun reliability - can fail to reload - reliability = player['reliability'] - if player['gun_oil'] > 0: - reliability += 15 - player['gun_oil'] -= 1 # Gun oil gets used up - - reload_roll = random.randint(1, 100) - if reload_roll <= reliability: - player['ammo'] = player['max_ammo'] - await self.bot.send_message(channel, f'\033[94m{nick} reloaded successfully!\033[0m') - else: - await self.bot.send_message(channel, f'\033[91m{nick}\'s gun jammed while reloading! Try again.\033[0m') - - self.save_player(nick, player) - - async def handle_bef(self, nick, channel): - player = self.get_player(nick) - duck = self.ducks.get(channel) - if duck and duck.get('alive'): - player['friends'].add('duck') - self.ducks[channel] = {'alive': False} - await self.bot.send_message(channel, f'\033[96m{nick} befriended the duck!\033[0m') - if self.logger: - self.logger.info(f"{nick} befriended a duck in {channel}") - else: - await self.bot.send_message(channel, f'No duck to befriend, {nick}!') - self.save_player(nick, player) - - async def handle_catch(self, nick, channel): - player = self.get_player(nick) - duck = self.ducks.get(channel) - if duck and duck.get('alive'): - player['caught'] += 1 - self.ducks[channel] = {'alive': False} - await self.bot.send_message(channel, f'\033[92m{nick} caught the duck!\033[0m') - if self.logger: - self.logger.info(f"{nick} caught a duck in {channel}") - else: - await self.bot.send_message(channel, f'No duck to catch, {nick}!') - self.save_player(nick, player) - - async def handle_shop(self, nick, channel): - player = self.get_player(nick) - coins = player['coins'] - - shop_items = [ - "🔫 Scope - Improves accuracy by 15% (Cost: 5 coins)", - "🔇 Silencer - Bonus coin on successful shots (Cost: 8 coins)", - "🛢️ Gun Oil - Improves reload reliability for 3 reloads (Cost: 3 coins)", - "🍀 Lucky Charm - Improves accuracy by 10% (Cost: 10 coins)", - "📦 Ammo Upgrade - Increases max ammo capacity by 1 (Cost: 12 coins)", - "🎯 Accuracy Training - Permanently increases accuracy by 5% (Cost: 15 coins)", - "🔧 Gun Maintenance - Permanently increases reliability by 10% (Cost: 20 coins)" - ] - - shop_msg = f"\033[95m{nick}'s Shop (Coins: {coins}):\033[0m\n" - for i, item in enumerate(shop_items, 1): - shop_msg += f"{i}. {item}\n" - shop_msg += "Use !buy to purchase an item!\n" - shop_msg += "Use !sell to sell upgrades for coins!" - - await self.bot.send_message(channel, shop_msg) - async def handle_duckstats(self, nick, channel): - player = self.get_player(nick) - stats = f"\033[95m{nick}'s Duck Stats:\033[0m\n" - stats += f"Caught: {player['caught']}\n" - stats += f"Coins: {player['coins']}\n" - stats += f"Accuracy: {player['accuracy']}%\n" - stats += f"Reliability: {player['reliability']}%\n" - stats += f"Max Ammo: {player['max_ammo']}\n" - stats += f"Gun Oil: {player['gun_oil']} uses left\n" - upgrades = [] - if player['scope']: upgrades.append("Scope") - if player['silencer']: upgrades.append("Silencer") - if player['lucky_charm']: upgrades.append("Lucky Charm") - stats += f"Upgrades: {', '.join(upgrades) if upgrades else 'None'}\n" - stats += f"Friends: {', '.join(player['friends']) if player['friends'] else 'None'}\n" - await self.bot.send_message(channel, stats) - - async def handle_buy(self, nick, channel, item_num): - player = self.get_player(nick) - - try: - item_id = int(item_num) - except ValueError: - await self.bot.send_message(channel, f'{nick}, please specify a valid item number!') - return - - shop_items = { - 1: ("scope", 5, "Scope"), - 2: ("silencer", 8, "Silencer"), - 3: ("gun_oil", 3, "Gun Oil"), - 4: ("lucky_charm", 10, "Lucky Charm"), - 5: ("ammo_upgrade", 12, "Ammo Upgrade"), - 6: ("accuracy_training", 15, "Accuracy Training"), - 7: ("gun_maintenance", 20, "Gun Maintenance") - } - - if item_id not in shop_items: - await self.bot.send_message(channel, f'{nick}, invalid item number!') - return - - item_key, cost, item_name = shop_items[item_id] - - if player['coins'] < cost: - await self.bot.send_message(channel, f'\033[91m{nick}, you need {cost} coins for {item_name}! (You have {player["coins"]})\033[0m') - return - - # Process purchase - player['coins'] -= cost - - if item_key == "scope": - if player['scope']: - await self.bot.send_message(channel, f'{nick}, you already have a scope!') - player['coins'] += cost # Refund - return - player['scope'] = True - elif item_key == "silencer": - if player['silencer']: - await self.bot.send_message(channel, f'{nick}, you already have a silencer!') - player['coins'] += cost - return - player['silencer'] = True - elif item_key == "gun_oil": - player['gun_oil'] += 3 - elif item_key == "lucky_charm": - if player['lucky_charm']: - await self.bot.send_message(channel, f'{nick}, you already have a lucky charm!') - player['coins'] += cost - return - player['lucky_charm'] = True - elif item_key == "ammo_upgrade": - player['max_ammo'] += 1 - elif item_key == "accuracy_training": - player['accuracy'] = min(95, player['accuracy'] + 5) # Cap at 95% - elif item_key == "gun_maintenance": - player['reliability'] = min(95, player['reliability'] + 10) # Cap at 95% - - await self.bot.send_message(channel, f'\033[92m{nick} purchased {item_name}!\033[0m') - self.save_player(nick, player) - - async def handle_sell(self, nick, channel, item_num): - player = self.get_player(nick) - - try: - item_id = int(item_num) - except ValueError: - await self.bot.send_message(channel, f'{nick}, please specify a valid item number!') - return - - sellable_items = { - 1: ("scope", 3, "Scope"), - 2: ("silencer", 5, "Silencer"), - 3: ("gun_oil", 1, "Gun Oil (per use)"), - 4: ("lucky_charm", 6, "Lucky Charm") - } - - if item_id not in sellable_items: - await self.bot.send_message(channel, f'{nick}, invalid item number! Sellable items: 1-4') - return - - item_key, sell_price, item_name = sellable_items[item_id] - - if item_key == "scope": - if not player['scope']: - await self.bot.send_message(channel, f'{nick}, you don\'t have a scope to sell!') - return - player['scope'] = False - player['coins'] += sell_price - elif item_key == "silencer": - if not player['silencer']: - await self.bot.send_message(channel, f'{nick}, you don\'t have a silencer to sell!') - return - player['silencer'] = False - player['coins'] += sell_price - elif item_key == "gun_oil": - if player['gun_oil'] <= 0: - await self.bot.send_message(channel, f'{nick}, you don\'t have any gun oil to sell!') - return - player['gun_oil'] -= 1 - player['coins'] += sell_price - elif item_key == "lucky_charm": - if not player['lucky_charm']: - await self.bot.send_message(channel, f'{nick}, you don\'t have a lucky charm to sell!') - return - player['lucky_charm'] = False - player['coins'] += sell_price - - await self.bot.send_message(channel, f'\033[94m{nick} sold {item_name} for {sell_price} coins!\033[0m') - self.save_player(nick, player) - - async def handle_stats(self, nick, channel): - player = self.get_player(nick) - - # Calculate effective accuracy and reliability - effective_accuracy = player['accuracy'] - if player['scope']: - effective_accuracy += 15 - if player['lucky_charm']: - effective_accuracy += 10 - effective_accuracy = min(100, effective_accuracy) - - effective_reliability = player['reliability'] - if player['gun_oil'] > 0: - effective_reliability += 15 - effective_reliability = min(100, effective_reliability) - - stats = f"\033[96m{nick}'s Combat Stats:\033[0m\n" - stats += f"🎯 Base Accuracy: {player['accuracy']}% (Effective: {effective_accuracy}%)\n" - stats += f"🔧 Base Reliability: {player['reliability']}% (Effective: {effective_reliability}%)\n" - stats += f"🔫 Ammo: {player['ammo']}/{player['max_ammo']}\n" - stats += f"💰 Coins: {player['coins']}\n" - stats += f"🦆 Ducks Caught: {player['caught']}\n" - stats += f"🛢️ Gun Oil: {player['gun_oil']} uses\n" - - upgrades = [] - if player['scope']: upgrades.append("🔭 Scope") - if player['silencer']: upgrades.append("🔇 Silencer") - if player['lucky_charm']: upgrades.append("🍀 Lucky Charm") - stats += f"⚡ Active Upgrades: {', '.join(upgrades) if upgrades else 'None'}\n" - - friends = list(player['friends']) - stats += f"🤝 Friends: {', '.join(friends) if friends else 'None'}" - - await self.bot.send_message(channel, stats) - - async def handle_register(self, nick, hostmask, username, password): - if self.auth.register_account(username, password, nick, hostmask): - await self.bot.send_message(nick, f"✅ Account '{username}' registered successfully! Use 'identify {username} {password}' to login.") - else: - await self.bot.send_message(nick, f"❌ Account '{username}' already exists!") - - async def handle_identify(self, nick, username, password): - if self.auth.authenticate(username, password, nick): - await self.bot.send_message(nick, f"✅ Authenticated as '{username}'!") - # Transfer nick-based data to account if exists - nick_data = self.db.load_player(nick) - if nick_data: - account_data = self.db.load_player(username) - if not account_data: - self.db.save_player(username, nick_data) - await self.bot.send_message(nick, "📊 Your progress has been transferred to your account!") - else: - await self.bot.send_message(nick, "❌ Invalid username or password!") - - async def handle_help(self, nick, channel): - help_text = """ -🦆 **DuckHunt Bot Commands** 🦆 - -**🎯 Hunting:** -• !bang - Shoot at a duck (requires ammo) -• !reload - Reload your weapon (can fail based on reliability) -• !catch - Catch a duck with your hands -• !bef - Befriend a duck instead of shooting - -**🛒 Economy:** -• !shop - View available items for purchase -• !buy - Purchase an item from the shop -• !sell - Sell equipment for coins -• !bank - Access banking services (deposits, loans) -• !trade - Trade with other players - -**📊 Stats & Info:** -• !stats - View detailed combat statistics -• !duckstats - View personal hunting statistics -• !leaderboard - View top players -• !license - Manage hunting license - -**⚙️ Settings:** -• !alerts - Toggle duck spawn notifications -• !register - Register an account (via /msg) -• identify - Login to account (via /msg) - -**🎮 Advanced:** -• !sabotage - Attempt to sabotage another hunter -• !help - Show this help message - -💡 **Tips:** -- Different duck types give different rewards -- Weapon durability affects performance -- Insurance protects your equipment -- Level up to unlock better gear! - """ - await self.bot.send_message(nick, help_text) - - async def handle_leaderboard(self, nick, channel): - leaderboard_data = self.db.get_leaderboard('caught', 10) - if not leaderboard_data: - await self.bot.send_message(channel, "No leaderboard data available yet!") - return - - msg = "🏆 **Duck Hunting Leaderboard** 🏆\n" - for i, (account, caught) in enumerate(leaderboard_data, 1): - emoji = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else f"{i}." - msg += f"{emoji} {account}: {caught} ducks\n" - - await self.bot.send_message(channel, msg) - - async def handle_bank(self, nick, channel): - player = self.get_player(nick) - bank_msg = f""" -🏦 **{nick}'s Bank Account** 🏦 -💰 Cash on hand: {player['coins']} coins -🏛️ Bank balance: {player['bank_account']} coins -📈 Total wealth: {player['coins'] + player['bank_account']} coins - -**Commands:** -• !bank deposit - Deposit coins (earns 2% daily interest) -• !bank withdraw - Withdraw coins -• !bank loan - Take a loan (10% interest) - """ - await self.bot.send_message(nick, bank_msg) - - async def handle_license(self, nick, channel): - player = self.get_player(nick) - license_active = player['hunting_license']['active'] - - if license_active: - expires = player['hunting_license']['expires'] - msg = f"🎫 Your hunting license is active until {expires}\n" - msg += "Licensed hunters get +25% coins and access to rare equipment!" - else: - msg = "🎫 You don't have a hunting license.\n" - msg += "Purchase one for 50 coins to get:\n" - msg += "• +25% coin rewards\n" - msg += "• Access to premium shop items\n" - msg += "• Reduced insurance costs\n" - msg += "Type '!buy license' to purchase" - - await self.bot.send_message(channel, msg) - - async def handle_alerts(self, nick, channel): - if nick in self.duck_alerts: - self.duck_alerts.remove(nick) - await self.bot.send_message(channel, f"🔕 {nick}: Duck alerts disabled") - else: - self.duck_alerts.add(nick) - await self.bot.send_message(channel, f"🔔 {nick}: Duck alerts enabled! You'll be notified when ducks spawn.") - - async def handle_trade(self, nick, channel, args): - if len(args) < 3: - await self.bot.send_message(channel, f"{nick}: Usage: !trade ") - return - - target, item, amount = args[0], args[1], args[2] - player = self.get_player(nick) - - try: - amount = int(amount) - except ValueError: - await self.bot.send_message(channel, f"{nick}: Amount must be a number!") - return - - if item == "coins": - if player['coins'] < amount: - await self.bot.send_message(channel, f"{nick}: You don't have enough coins!") - return - - trade_data = { - 'type': 'coins', - 'amount': amount, - 'from_nick': nick - } - - trade_id = self.db.save_trade(nick, target, trade_data) - await self.bot.send_message(channel, f"💸 Trade offer sent to {target}: {amount} coins") - await self.bot.send_message(target, f"💰 {nick} wants to trade you {amount} coins. Type '!accept {trade_id}' to accept!") - else: - await self.bot.send_message(channel, f"{nick}: Only coin trading is available currently!") - - async def handle_sabotage(self, nick, channel, target): - player = self.get_player(nick) - target_player = self.get_player(target) - - if player['coins'] < 5: - await self.bot.send_message(channel, f"{nick}: Sabotage costs 5 coins!") - return - - success_chance = 60 + (player['level'] * 5) - if random.randint(1, 100) <= success_chance: - player['coins'] -= 5 - target_player['weapon_durability'] = max(0, target_player['weapon_durability'] - 10) - await self.bot.send_message(channel, f"😈 {nick} successfully sabotaged {target}'s weapon!") - self.save_player(nick, player) - self.save_player(target, target_player) - else: - player['coins'] -= 5 - await self.bot.send_message(channel, f"😅 {nick}'s sabotage attempt failed!") - self.save_player(nick, player) diff --git a/duckhunt/src/items.py b/duckhunt/src/items.py deleted file mode 100644 index f579bf1..0000000 --- a/duckhunt/src/items.py +++ /dev/null @@ -1,124 +0,0 @@ -import random - -class DuckTypes: - COMMON = { - 'name': 'Common Duck', - 'emoji': '🦆', - 'rarity': 70, - 'coins': 1, - 'xp': 10, - 'health': 1 - } - - RARE = { - 'name': 'Rare Duck', - 'emoji': '🦆✨', - 'rarity': 20, - 'coins': 3, - 'xp': 25, - 'health': 1 - } - - GOLDEN = { - 'name': 'Golden Duck', - 'emoji': '🥇🦆', - 'rarity': 8, - 'coins': 10, - 'xp': 50, - 'health': 2 - } - - ARMORED = { - 'name': 'Armored Duck', - 'emoji': '🛡️🦆', - 'rarity': 2, - 'coins': 15, - 'xp': 75, - 'health': 3 - } - - @classmethod - def get_random_duck(cls): - roll = random.randint(1, 100) - if roll <= cls.COMMON['rarity']: - return cls.COMMON - elif roll <= cls.COMMON['rarity'] + cls.RARE['rarity']: - return cls.RARE - elif roll <= cls.COMMON['rarity'] + cls.RARE['rarity'] + cls.GOLDEN['rarity']: - return cls.GOLDEN - else: - return cls.ARMORED - -class WeaponTypes: - BASIC_GUN = { - 'name': 'Basic Gun', - 'accuracy_bonus': 0, - 'durability': 100, - 'max_durability': 100, - 'repair_cost': 5, - 'attachment_slots': 1 - } - - SHOTGUN = { - 'name': 'Shotgun', - 'accuracy_bonus': -10, - 'durability': 80, - 'max_durability': 80, - 'repair_cost': 8, - 'attachment_slots': 2, - 'spread_shot': True # Can hit multiple ducks - } - - RIFLE = { - 'name': 'Rifle', - 'accuracy_bonus': 20, - 'durability': 120, - 'max_durability': 120, - 'repair_cost': 12, - 'attachment_slots': 3 - } - -class AmmoTypes: - STANDARD = { - 'name': 'Standard Ammo', - 'damage': 1, - 'accuracy_modifier': 0, - 'cost': 1 - } - - RUBBER = { - 'name': 'Rubber Bullets', - 'damage': 0, # Non-lethal, for catching - 'accuracy_modifier': 5, - 'cost': 2, - 'special': 'stun' - } - - EXPLOSIVE = { - 'name': 'Explosive Rounds', - 'damage': 2, - 'accuracy_modifier': -5, - 'cost': 5, - 'special': 'area_damage' - } - -class Attachments: - LASER_SIGHT = { - 'name': 'Laser Sight', - 'accuracy_bonus': 10, - 'cost': 15, - 'durability_cost': 2 # Uses weapon durability faster - } - - EXTENDED_MAG = { - 'name': 'Extended Magazine', - 'ammo_bonus': 2, - 'cost': 20 - } - - BIPOD = { - 'name': 'Bipod', - 'accuracy_bonus': 15, - 'reliability_bonus': 5, - 'cost': 25 - } diff --git a/duckhunt/src/logging_utils.py b/duckhunt/src/logging_utils.py deleted file mode 100644 index 439c6d6..0000000 --- a/duckhunt/src/logging_utils.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging -import sys -from functools import partial - -class ColorFormatter(logging.Formatter): - COLORS = { - 'DEBUG': '\033[94m', - 'INFO': '\033[92m', - 'WARNING': '\033[93m', - 'ERROR': '\033[91m', - 'CRITICAL': '\033[95m', - 'ENDC': '\033[0m', - } - def format(self, record): - color = self.COLORS.get(record.levelname, '') - endc = self.COLORS['ENDC'] - msg = super().format(record) - return f"{color}{msg}{endc}" - -def setup_logger(name='DuckHuntBot', level=logging.INFO): - logger = logging.getLogger(name) - handler = logging.StreamHandler(sys.stdout) - formatter = ColorFormatter('[%(asctime)s] %(levelname)s: %(message)s') - handler.setFormatter(formatter) - logger.addHandler(handler) - logger.setLevel(level) - logger.propagate = False - return logger diff --git a/duckhunt/src/utils.py b/duckhunt/src/utils.py deleted file mode 100644 index 6b04233..0000000 --- a/duckhunt/src/utils.py +++ /dev/null @@ -1,11 +0,0 @@ -def parse_message(line): - prefix = '' - trailing = '' - if line.startswith(':'): - prefix, line = line[1:].split(' ', 1) - if ' :' in line: - line, trailing = line.split(' :', 1) - parts = line.split() - command = parts[0] if parts else '' - params = parts[1:] if len(parts) > 1 else [] - return prefix, command, params, trailing diff --git a/duckhunt/test_bot.py b/duckhunt/test_bot.py deleted file mode 100644 index 81c27e7..0000000 --- a/duckhunt/test_bot.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script for DuckHunt Bot -Run this to test both the modular and simple bot implementations -""" - -import asyncio -import json -import sys -import os - -# Add src directory to path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) - -async def test_modular_bot(): - """Test the modular bot implementation""" - try: - print("🔧 Testing modular bot (src/duckhuntbot.py)...") - - # Load config - with open('config.json') as f: - config = json.load(f) - - # Test imports - from src.duckhuntbot import IRCBot - from src.sasl import SASLHandler - - # Create bot instance - bot = IRCBot(config) - print("✅ Modular bot initialized successfully!") - - # Test SASL handler - sasl_handler = SASLHandler(bot, config) - print("✅ SASL handler created successfully!") - - # Test database - bot.db.save_player("testuser", {"coins": 100, "caught": 5}) - data = bot.db.load_player("testuser") - if data and data['coins'] == 100: - print("✅ Database working!") - else: - print("❌ Database test failed!") - - # Test game logic - player = bot.game.get_player("testuser") - if player and 'coins' in player: - print("✅ Game logic working!") - else: - print("❌ Game logic test failed!") - - return True - - except Exception as e: - print(f"❌ Modular bot error: {e}") - import traceback - traceback.print_exc() - return False - -async def test_simple_bot(): - """Test the simple bot implementation""" - try: - print("\n🔧 Testing simple bot (simple_duckhunt.py)...") - - # Load config - with open('config.json') as f: - config = json.load(f) - - # Test imports - from simple_duckhunt import SimpleIRCBot - from src.sasl import SASLHandler - - # Create bot instance - bot = SimpleIRCBot(config) - print("✅ Simple bot initialized successfully!") - - # Test SASL handler integration - if hasattr(bot, 'sasl_handler'): - print("✅ SASL handler integrated!") - else: - print("❌ SASL handler not integrated!") - return False - - # Test database - if 'testuser' in bot.players: - bot.players['testuser']['coins'] = 200 - bot.save_database() - bot.load_database() - if bot.players.get('testuser', {}).get('coins') == 200: - print("✅ Simple bot database working!") - else: - print("❌ Simple bot database test failed!") - - return True - - except Exception as e: - print(f"❌ Simple bot error: {e}") - import traceback - traceback.print_exc() - return False - -async def test_sasl_config(): - """Test SASL configuration""" - try: - print("\n🔧 Testing SASL configuration...") - - # Load config - with open('config.json') as f: - config = json.load(f) - - # Check SASL config - sasl_config = config.get('sasl', {}) - if sasl_config.get('enabled'): - print("✅ SASL is enabled in config") - - username = sasl_config.get('username') - password = sasl_config.get('password') - - if username and password: - print(f"✅ SASL credentials configured (user: {username})") - else: - print("⚠️ SASL enabled but credentials missing") - else: - print("ℹ️ SASL is not enabled in config") - - return True - - except Exception as e: - print(f"❌ SASL config error: {e}") - return False - -async def main(): - """Main test function""" - print("🦆 DuckHunt Bot Integration Test") - print("=" * 50) - - try: - # Test configuration - config_ok = await test_sasl_config() - - # Test modular bot - modular_ok = await test_modular_bot() - - # Test simple bot - simple_ok = await test_simple_bot() - - print("\n" + "=" * 50) - print("📊 Test Results:") - print(f" Config: {'✅ PASS' if config_ok else '❌ FAIL'}") - print(f" Modular Bot: {'✅ PASS' if modular_ok else '❌ FAIL'}") - print(f" Simple Bot: {'✅ PASS' if simple_ok else '❌ FAIL'}") - - if all([config_ok, modular_ok, simple_ok]): - print("\n🎉 All tests passed! SASL integration is working!") - print("🦆 DuckHunt Bots are ready to deploy!") - return True - else: - print("\n💥 Some tests failed. Check the errors above.") - return False - - except Exception as e: - print(f"💥 Test suite error: {e}") - return False - -if __name__ == '__main__': - success = asyncio.run(main()) - if not success: - sys.exit(1) diff --git a/src/__pycache__/db.cpython-312.pyc b/src/__pycache__/db.cpython-312.pyc new file mode 100644 index 0000000..37252ad Binary files /dev/null and b/src/__pycache__/db.cpython-312.pyc differ diff --git a/src/__pycache__/duckhuntbot.cpython-312.pyc b/src/__pycache__/duckhuntbot.cpython-312.pyc new file mode 100644 index 0000000..506a3a3 Binary files /dev/null and b/src/__pycache__/duckhuntbot.cpython-312.pyc differ diff --git a/src/__pycache__/game.cpython-312.pyc b/src/__pycache__/game.cpython-312.pyc new file mode 100644 index 0000000..130d269 Binary files /dev/null and b/src/__pycache__/game.cpython-312.pyc differ diff --git a/src/__pycache__/logging_utils.cpython-312.pyc b/src/__pycache__/logging_utils.cpython-312.pyc new file mode 100644 index 0000000..3488a6c Binary files /dev/null and b/src/__pycache__/logging_utils.cpython-312.pyc differ diff --git a/duckhunt/src/__pycache__/sasl.cpython-312.pyc b/src/__pycache__/sasl.cpython-312.pyc similarity index 99% rename from duckhunt/src/__pycache__/sasl.cpython-312.pyc rename to src/__pycache__/sasl.cpython-312.pyc index f2096c9..18a03e7 100644 Binary files a/duckhunt/src/__pycache__/sasl.cpython-312.pyc and b/src/__pycache__/sasl.cpython-312.pyc differ diff --git a/src/__pycache__/utils.cpython-312.pyc b/src/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000..6cb9614 Binary files /dev/null and b/src/__pycache__/utils.cpython-312.pyc differ diff --git a/src/db.py b/src/db.py new file mode 100644 index 0000000..36fd3af --- /dev/null +++ b/src/db.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +Database functionality for DuckHunt Bot +""" + +import json +import os +import time +import asyncio +import logging + + +class DuckDB: + """Database management for DuckHunt Bot""" + + def __init__(self, db_file="duckhunt.json"): + self.db_file = db_file + self.players = {} + self._save_pending = False + self.logger = logging.getLogger('DuckHuntBot.DB') + + def get_config(self, path, default=None): + """Helper method to get config values (needs to be set by bot)""" + # This will be set by the main bot class + if hasattr(self, '_config_getter'): + return self._config_getter(path, default) + return default + + def set_config_getter(self, config_getter): + """Set the config getter function from the main bot""" + self._config_getter = config_getter + + def load_database(self): + """Load player data from JSON file""" + if os.path.exists(self.db_file): + try: + with open(self.db_file, 'r') as f: + data = json.load(f) + self.players = data.get('players', {}) + self.logger.info(f"Loaded {len(self.players)} players from {self.db_file}") + except (json.JSONDecodeError, IOError) as e: + self.logger.error(f"Error loading database: {e}") + self.players = {} + else: + self.players = {} + self.logger.info(f"Created new database: {self.db_file}") + + def save_database(self): + """Save all player data to JSON file with error handling""" + try: + # Atomic write to prevent corruption + temp_file = f"{self.db_file}.tmp" + data = { + 'players': self.players, + 'last_save': str(time.time()) + } + with open(temp_file, 'w') as f: + json.dump(data, f, indent=2) + + # Atomic rename to replace old file + os.replace(temp_file, self.db_file) + + except IOError as e: + self.logger.error(f"Error saving database: {e}") + except Exception as e: + self.logger.error(f"Unexpected database save error: {e}") + + def _get_starting_accuracy(self): + """Get starting accuracy with optional randomization""" + base_accuracy = self.get_config('new_players.starting_accuracy', 65) or 65 + if self.get_config('new_players.random_stats.enabled', False): + import random + variance = self.get_config('new_players.random_stats.accuracy_variance', 10) or 10 + return max(10, min(95, base_accuracy + random.randint(-variance, variance))) + return base_accuracy + + def _get_starting_reliability(self): + """Get starting reliability with optional randomization""" + base_reliability = self.get_config('new_players.starting_reliability', 70) or 70 + if self.get_config('new_players.random_stats.enabled', False): + import random + variance = self.get_config('new_players.random_stats.reliability_variance', 10) or 10 + return max(10, min(95, base_reliability + random.randint(-variance, variance))) + return base_reliability + + def get_player(self, user): + """Get player data, creating if doesn't exist""" + if '!' not in user: + nick = user.lower() + else: + nick = user.split('!')[0].lower() + + if nick in self.players: + player = self.players[nick] + # Ensure backward compatibility by adding missing fields + self._ensure_player_fields(player) + return player + else: + return self.create_player(nick) + + def create_player(self, nick): + """Create a new player with default stats""" + player = { + 'shots': 6, + 'max_shots': 6, + 'chargers': 2, + 'max_chargers': 2, + 'reload_time': 5.0, + 'ducks_shot': 0, + 'ducks_befriended': 0, + 'accuracy_bonus': 0, + 'xp_bonus': 0, + 'charm_bonus': 0, + 'exp': 0, + 'money': 100, + 'last_hunt': 0, + 'last_reload': 0, + 'level': 1, + 'inventory': {}, + 'ignored_users': [], + # Gun mechanics (eggdrop style) + 'jammed': False, + 'jammed_count': 0, + 'total_ammo_used': 0, + 'shot_at': 0, + 'wild_shots': 0, + 'accuracy': self._get_starting_accuracy(), + 'reliability': self._get_starting_reliability(), + 'gun_confiscated': False, + 'confiscated_count': 0 + } + + self.players[nick] = player + self.logger.info(f"Created new player: {nick}") + return player + + def _ensure_player_fields(self, player): + """Ensure player has all required fields for backward compatibility""" + required_fields = { + 'shots': player.get('ammo', 6), # Map old 'ammo' to 'shots' + 'max_shots': player.get('max_ammo', 6), # Map old 'max_ammo' to 'max_shots' + 'chargers': player.get('chargers', 2), # Map old 'chargers' (magazines) + 'max_chargers': player.get('max_chargers', 2), # Map old 'max_chargers' + 'reload_time': 5.0, + 'ducks_shot': player.get('caught', 0), # Map old 'caught' to 'ducks_shot' + 'ducks_befriended': player.get('befriended', 0), # Use existing befriended count + 'accuracy_bonus': 0, + 'xp_bonus': 0, + 'charm_bonus': 0, + 'exp': player.get('xp', 0), # Map old 'xp' to 'exp' + 'money': player.get('coins', 100), # Map old 'coins' to 'money' + 'last_hunt': 0, + 'last_reload': 0, + 'level': 1, + 'inventory': {}, + 'ignored_users': [], + # Gun mechanics (eggdrop style) + 'jammed': False, + 'jammed_count': player.get('jammed_count', 0), + 'total_ammo_used': player.get('total_ammo_used', 0), + 'shot_at': player.get('shot_at', 0), + 'wild_shots': player.get('wild_shots', 0), + 'accuracy': player.get('accuracy', 65), + 'reliability': player.get('reliability', 70), + 'gun_confiscated': player.get('gun_confiscated', False), + 'confiscated_count': player.get('confiscated_count', 0) + } + + for field, default_value in required_fields.items(): + if field not in player: + player[field] = default_value + + def save_player(self, user): + """Save player data - batch saves for performance""" + if not self._save_pending: + self._save_pending = True + # Schedule delayed save to batch multiple writes + asyncio.create_task(self._delayed_save()) + + async def _delayed_save(self): + """Batch save to reduce disk I/O""" + await asyncio.sleep(0.5) # Small delay to batch saves + try: + self.save_database() + self.logger.debug("Database batch save completed") + except Exception as e: + self.logger.error(f"Database batch save failed: {e}") + finally: + self._save_pending = False \ No newline at end of file diff --git a/src/duckhuntbot.py b/src/duckhuntbot.py new file mode 100644 index 0000000..18194a3 --- /dev/null +++ b/src/duckhuntbot.py @@ -0,0 +1,1195 @@ +#!/usr/bin/env python3 +""" +Main DuckHunt IRC Bot +""" + +import asyncio +import ssl +import json +import random +import logging +import sys +import os +import time +import signal +from typing import Optional + +from .logging_utils import setup_logger +from .utils import parse_message, InputValidator +from .db import DuckDB +from .game import DuckGame +from .sasl import SASLHandler + + +class DuckHuntBot: + """Main IRC Bot for DuckHunt game""" + + def __init__(self, config): + self.config = config + self.logger = setup_logger("DuckHuntBot") + self.reader: Optional[asyncio.StreamReader] = None + self.writer: Optional[asyncio.StreamWriter] = None + self.registered = False + self.channels_joined = set() + self.shutdown_requested = False + + # Initialize subsystems + self.db = DuckDB() + self.db.set_config_getter(self.get_config) + self.game = DuckGame(self, self.db) + + # Initialize SASL handler + self.sasl_handler = SASLHandler(self, config) + + # Admin configuration + self.admins = [admin.lower() for admin in self.config.get('admins', ['colby'])] + self.ignored_nicks = set() + + # Duck spawn timing + self.duck_spawn_times = {} # Per-channel last spawn times + self.channel_records = {} # Per-channel shooting records + + # Dropped items tracking - competitive snatching system + self.dropped_items = {} # Per-channel dropped items: {channel: [{'item': item_name, 'timestamp': time, 'dropper': nick}]} + + # Colors for IRC messages + self.colors = { + 'red': '\x0304', + 'green': '\x0303', + 'yellow': '\x0308', + 'blue': '\x0302', + 'cyan': '\x0311', + 'magenta': '\x0306', + 'white': '\x0300', + 'bold': '\x02', + 'reset': '\x03', + 'underline': '\x1f' + } + + def get_config(self, path, default=None): + """Get configuration value using dot notation""" + keys = path.split('.') + value = self.config + for key in keys: + if isinstance(value, dict) and key in value: + value = value[key] + else: + return default + return value + + async def connect(self): + """Connect to IRC server""" + try: + # Setup SSL context if needed + ssl_context = None + if self.config.get('ssl', False): + ssl_context = ssl.create_default_context() + + # Connect to server + self.reader, self.writer = await asyncio.open_connection( + self.config['server'], + self.config['port'], + ssl=ssl_context + ) + + self.logger.info(f"Connected to {self.config['server']}:{self.config['port']}") + + # Send server password if provided + if self.config.get('password'): + self.send_raw(f"PASS {self.config['password']}") + + # Register with server + await self.register_user() + + except Exception as e: + self.logger.error(f"Connection failed: {e}") + raise + + async def register_user(self): + """Register user with IRC server""" + nick = self.config['nick'] + self.send_raw(f'NICK {nick}') + self.send_raw(f'USER {nick} 0 * :DuckHunt Bot') + + def send_raw(self, msg): + """Send raw IRC message""" + if self.writer and not self.writer.is_closing(): + try: + self.writer.write(f'{msg}\r\n'.encode()) + # No await for drain() - let TCP handle buffering for speed + except Exception as e: + self.logger.error(f"Error sending message: {e}") + + def send_message(self, target, msg): + """Send message to target (channel or user)""" + self.send_raw(f'PRIVMSG {target} :{msg}') + + def is_admin(self, user): + """Check if user is admin by nick only""" + if '!' not in user: + return False + nick = user.split('!')[0].lower() + return nick in self.admins + + def get_random_player_for_friendly_fire(self, shooter_nick): + """Get random player for friendly fire accident""" + other_players = [nick for nick in self.db.players.keys() + if nick.lower() != shooter_nick.lower()] + if other_players: + return random.choice(other_players) + return None + + async def send_user_message(self, nick, channel, message, message_type='default'): + """Send message to user respecting their output mode preferences""" + # Get player to check preferences + player = self.db.get_player(f"{nick}!user@host") + if not player: + # Default to public if no player data + self.send_message(channel, f"{nick} > {message}") + return + + # Check message output configuration + force_public_types = self.get_config('message_output.force_public', {}) or {} + if force_public_types.get(message_type, False): + self.send_message(channel, f"{nick} > {message}") + return + + # Check user preference + output_mode = player.get('settings', {}).get('output_mode', 'PUBLIC') + + if output_mode == 'NOTICE': + self.send_raw(f'NOTICE {nick} :{message}') + elif output_mode == 'PRIVMSG': + self.send_message(nick, message) + else: # PUBLIC or default + self.send_message(channel, f"{nick} > {message}") + + async def auto_rearm_confiscated_guns(self, channel, shooter_nick): + """Auto-rearm confiscated guns when someone shoots a duck""" + if not self.get_config('weapons.auto_rearm_on_duck_shot', True): + return + + # Find players with confiscated guns + rearmed_players = [] + for nick, player in self.db.players.items(): + if player.get('gun_confiscated', False): + player['gun_confiscated'] = False + player['ammo'] = player.get('max_ammo', 6) + player['chargers'] = player.get('max_chargers', 2) + rearmed_players.append(nick) + + if rearmed_players: + self.logger.info(f"Auto-rearmed guns for: {', '.join(rearmed_players)}") + self.send_message(channel, + f"{self.colors['green']}Guns have been returned to all hunters! " + f"({len(rearmed_players)} players rearmed){self.colors['reset']}") + self.db.save_database() + + def setup_signal_handlers(self): + """Setup signal handlers for graceful shutdown""" + def signal_handler(signum, frame): + self.logger.info(f"Received signal {signum}, shutting down...") + self.shutdown_requested = True + # Cancel any pending tasks + for task in asyncio.all_tasks(): + if not task.done(): + task.cancel() + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + if hasattr(signal, 'SIGHUP'): + signal.signal(signal.SIGHUP, signal_handler) + + async def handle_message(self, prefix, command, params, trailing): + """Handle incoming IRC messages""" + try: + if command == '001': # Welcome message + self.registered = True + self.logger.info("Successfully registered with IRC server") + + # Join channels + for channel in self.config['channels']: + self.send_raw(f'JOIN {channel}') + + elif command == 'JOIN': + if params and prefix.split('!')[0] == self.config['nick']: + channel = params[0] + self.channels_joined.add(channel) + self.logger.info(f"Joined channel: {channel}") + + elif command == 'PRIVMSG': + if len(params) >= 1: + target = params[0] + message = trailing + + # Handle commands + if message.startswith('!') or target == self.config['nick']: + await self.handle_command(prefix, target, message) + + elif command == 'PING': + self.send_raw(f'PONG :{trailing}') + + # Handle SASL messages + elif command == 'CAP': + await self.sasl_handler.handle_cap_response(params, trailing) + elif command == 'AUTHENTICATE': + await self.sasl_handler.handle_authenticate_response(params) + elif command in ['903', '904', '905', '906', '907']: + await self.sasl_handler.handle_sasl_result(command, params, trailing) + + except Exception as e: + self.logger.error(f"Error handling message: {e}") + + async def handle_command(self, user, channel, message): + """Handle bot commands""" + if not user: + return + + try: + nick = user.split('!')[0] + nick_lower = nick.lower() + + # Input validation + if not InputValidator.validate_nickname(nick): + return + + # Check if user is ignored + if nick_lower in self.ignored_nicks: + return + + # Sanitize message + message = InputValidator.sanitize_message(message) + if not message: + return + + # Determine response target + is_private = channel == self.config['nick'] + response_target = nick if is_private else channel + + # Remove ! prefix for public commands + if message.startswith('!'): + cmd_parts = message[1:].split() + else: + cmd_parts = message.split() + + if not cmd_parts: + return + + cmd = cmd_parts[0].lower() + args = cmd_parts[1:] if len(cmd_parts) > 1 else [] + + # Get player data + player = self.db.get_player(user) + if not player: + return + + # Handle commands + await self.process_command(nick, response_target, cmd, args, player, user) + + except Exception as e: + self.logger.error(f"Error in command handler: {e}") + + async def process_command(self, nick, target, cmd, args, player, user): + """Process individual commands""" + # Game commands + if cmd == 'bang': + await self.handle_bang(nick, target, player) + elif cmd == 'reload': + await self.handle_reload(nick, target, player) + elif cmd == 'bef' or cmd == 'befriend': + await self.handle_befriend(nick, target, player) + elif cmd == 'duckstats': + await self.handle_duckstats(nick, target, player) + elif cmd == 'shop': + await self.handle_shop(nick, target, player) + elif cmd == 'sell': + if args: + await self.handle_sell(nick, target, args[0], player) + else: + await self.send_user_message(nick, target, "Usage: !sell ") + elif cmd == 'use': + if args: + target_nick = args[1] if len(args) > 1 else None + await self.handle_use(nick, target, args[0], player, target_nick) + else: + await self.send_user_message(nick, target, "Usage: !use [target_player]") + elif cmd == 'duckhelp': + await self.handle_duckhelp(nick, target) + elif cmd == 'ignore': + if args: + await self.handle_ignore(nick, target, args[0]) + else: + await self.send_user_message(nick, target, "Usage: !ignore ") + elif cmd == 'delignore': + if args: + await self.handle_delignore(nick, target, args[0]) + else: + await self.send_user_message(nick, target, "Usage: !delignore ") + elif cmd == 'topduck': + await self.handle_topduck(nick, target) + elif cmd == 'snatch': + await self.handle_snatch(nick, target, player) + # Admin commands + elif cmd == 'rearm' and self.is_admin(user): + target_nick = args[0] if args else None + await self.handle_rearm(nick, target, player, target_nick) + elif cmd == 'disarm' and self.is_admin(user): + target_nick = args[0] if args else None + await self.handle_disarm(nick, target, target_nick) + elif cmd == 'ducklaunch' and self.is_admin(user): + await self.handle_ducklaunch(nick, target) + elif cmd == 'reset' and self.is_admin(user): + if len(args) >= 2 and args[1] == 'confirm': + await self.handle_reset_confirm(nick, target, args[0]) + elif args: + await self.handle_reset(nick, target, args[0]) + else: + await self.send_user_message(nick, target, "Usage: !reset [confirm]") + else: + # Unknown command + pass + + async def handle_bang(self, nick, channel, player): + """Handle !bang command - shoot at duck (eggdrop style)""" + # Check if gun is confiscated + if player.get('gun_confiscated', False): + await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! You cannot shoot.") + return + + # Check if gun is jammed + if player.get('jammed', False): + message = f"{nick} > Gun jammed! Use !reload" + await self.send_user_message(nick, channel, message) + return + + # Check if player has ammunition + if player['shots'] <= 0: + message = f"{nick} > *click* You're out of ammo! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" + await self.send_user_message(nick, channel, message) + return + + # Check if channel has ducks + if channel not in self.game.ducks or not self.game.ducks[channel]: + # Fire shot anyway (wild shot into air) + player['shots'] -= 1 + player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1 + player['wild_shots'] = player.get('wild_shots', 0) + 1 + + # Check for gun jam after shooting + if self.game.gun_jams(player): + player['jammed'] = True + player['jammed_count'] = player.get('jammed_count', 0) + 1 + message = f"{nick} > *BANG* You shot at nothing! What were you aiming at? *click* Gun jammed! | 0 xp | {self.colors['red']}GUN CONFISCATED{self.colors['reset']}" + else: + message = f"{nick} > *BANG* You shot at nothing! What were you aiming at? | 0 xp | {self.colors['red']}GUN CONFISCATED{self.colors['reset']}" + + # Confiscate gun for wild shooting + player['gun_confiscated'] = True + player['confiscated_count'] = player.get('confiscated_count', 0) + 1 + + self.send_message(channel, message) + self.db.save_database() + return + + # Get first duck in channel + duck = self.game.ducks[channel][0] + player['shots'] -= 1 + player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1 + player['shot_at'] = player.get('shot_at', 0) + 1 + + # Check for gun jam first (before checking hit) + if self.game.gun_jams(player): + player['jammed'] = True + player['jammed_count'] = player.get('jammed_count', 0) + 1 + message = f"{nick} > *BANG* *click* Gun jammed while shooting! | Ammo: {player['shots']}/{player['max_shots']}" + self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}") + else: + # Check if duck was hit (based on accuracy) + hit_chance = min(0.7 + (player.get('accuracy', 0) * 0.001), 0.95) + if random.random() < hit_chance: + await self.handle_duck_hit(nick, channel, player, duck) + else: + await self.handle_duck_miss(nick, channel, player) + + # Save player data + self.db.save_database() + + async def handle_duck_hit(self, nick, channel, player, duck): + """Handle successful duck hit (eggdrop style)""" + # Remove duck from channel + self.game.ducks[channel].remove(duck) + + # Calculate reaction time if available + shot_time = time.time() + reaction_time = shot_time - duck.get('spawn_time', shot_time) + + # Award points and XP based on duck type + points_earned = duck['points'] + xp_earned = duck['xp'] + + # Bonus for quick shots + if reaction_time < 2.0: + quick_bonus = int(points_earned * 0.5) + points_earned += quick_bonus + quick_shot_msg = f" [Quick shot bonus: +{quick_bonus}]" + else: + quick_shot_msg = "" + + # Apply XP bonus + xp_earned = int(xp_earned * (1 + player.get('xp_bonus', 0) * 0.001)) + + # Update player stats + player['ducks_shot'] += 1 + player['exp'] += xp_earned + player['money'] += points_earned + player['last_hunt'] = time.time() + + # Update accuracy (reward hits) + current_accuracy = player.get('accuracy', 65) + player['accuracy'] = min(current_accuracy + 1, 95) + + # Track best time + if 'best_time' not in player or reaction_time < player['best_time']: + player['best_time'] = reaction_time + + # Store reflex data + player['total_reflex_time'] = player.get('total_reflex_time', 0) + reaction_time + player['reflex_shots'] = player.get('reflex_shots', 0) + 1 + + # Level up check + await self.check_level_up(nick, channel, player) + + # Eggdrop style hit message - exact format match + message = f"{nick} > *BANG* you shot down the duck in {reaction_time:.2f} seconds. \\_X< *KWAK* [+{xp_earned} xp] [TOTAL DUCKS: {player['ducks_shot']}]" + self.send_message(channel, f"{self.colors['green']}{message}{self.colors['reset']}") + + # Award items occasionally - drop to ground for snatching + if random.random() < 0.1: # 10% chance + await self.drop_random_item(nick, channel) + + async def handle_duck_miss(self, nick, channel, player): + """Handle duck miss (eggdrop style)""" + # Reduce accuracy (penalize misses) + current_accuracy = player.get('accuracy', 65) + player['accuracy'] = max(current_accuracy - 2, 10) + + # Track misses + player['missed'] = player.get('missed', 0) + 1 + + # Eggdrop style miss message + message = f"{nick} > *BANG* You missed the duck! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" + self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}") + + # Chance to scare other ducks with the noise + if channel in self.game.ducks and len(self.game.ducks[channel]) > 1: + for other_duck in self.game.ducks[channel][:]: + if random.random() < 0.2: # 20% chance each duck gets scared + self.game.ducks[channel].remove(other_duck) + self.send_message(channel, f"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The other ducks fly away, scared by the noise!") + + async def handle_reload(self, nick, channel, player): + """Handle reload command (eggdrop style) - reload ammo and clear jams""" + current_time = time.time() + + # Check if gun is confiscated + if player.get('gun_confiscated', False): + await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! You cannot reload.") + return + + # Check if gun is jammed + if player.get('jammed', False): + # Clear the jam + player['jammed'] = False + player['last_reload'] = current_time + + message = f"{nick} > *click click* You unjammed your gun! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" + self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") + self.db.save_database() + return + + # Check if already full + if player['shots'] >= player['max_shots']: + message = f"{nick} > Gun is already fully loaded! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" + await self.send_user_message(nick, channel, message) + return + + # Check if have chargers to reload with + if player.get('chargers', 0) <= 0: + message = f"{nick} > No chargers left to reload with! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: 0/{player.get('max_chargers', 2)}" + await self.send_user_message(nick, channel, message) + return + + # Check reload time cooldown + if current_time - player.get('last_reload', 0) < player['reload_time']: + remaining = int(player['reload_time'] - (current_time - player.get('last_reload', 0))) + message = f"{nick} > Reload cooldown: {remaining} seconds remaining | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" + await self.send_user_message(nick, channel, message) + return + + # Perform reload + old_shots = player['shots'] + player['shots'] = player['max_shots'] + player['chargers'] = max(0, player.get('chargers', 2) - 1) # Use one charger + player['last_reload'] = current_time + shots_added = player['shots'] - old_shots + + message = f"{nick} > *click clack* Reloaded! +{shots_added} shots | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player['chargers']}/{player.get('max_chargers', 2)}" + + self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") + + # Save player data + self.db.save_database() + + async def handle_befriend(self, nick, channel, player): + """Handle !bef command - befriend a duck""" + # Check if channel has ducks + if channel not in self.game.ducks or not self.game.ducks[channel]: + await self.send_user_message(nick, channel, "There are no ducks to befriend!") + return + + # Get first duck + duck = self.game.ducks[channel][0] + + # Befriend success rate (starts at 50%, can be improved with items) + befriend_chance = 0.5 + (player.get('charm_bonus', 0) * 0.001) + + if random.random() < befriend_chance: + # Remove duck from channel + self.game.ducks[channel].remove(duck) + + # Award XP and friendship bonus + xp_earned = duck['xp'] + friendship_bonus = duck['points'] // 2 + + player['exp'] += xp_earned + player['money'] += friendship_bonus + player['ducks_befriended'] += 1 + + # Level up check + await self.check_level_up(nick, channel, player) + + # Random positive effect + effects = [ + ("luck", 10, "You feel lucky!"), + ("charm_bonus", 5, "The duck teaches you about friendship!"), + ("accuracy_bonus", 3, "The duck gives you aiming tips!") + ] + + if random.random() < 0.3: # 30% chance for bonus effect + effect, amount, message = random.choice(effects) + player[effect] = player.get(effect, 0) + amount + bonus_msg = f" {message}" + else: + bonus_msg = "" + + message = (f"{nick} befriended a {duck['type']} duck! " + f"+{friendship_bonus} coins, +{xp_earned} XP.{bonus_msg}") + self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}") + + # Award items occasionally + if random.random() < 0.15: # 15% chance (higher than shooting) + await self.award_random_item(nick, channel, player) + else: + # Befriend failed + miss_messages = [ + f"The {duck['type']} duck doesn't trust you yet!", + f"The {duck['type']} duck flies away from you!", + f"You need to be more patient with the {duck['type']} duck!", + f"The {duck['type']} duck looks at you suspiciously!" + ] + + message = f"{nick} {random.choice(miss_messages)}" + self.send_message(channel, f"{self.colors['yellow']}{message}{self.colors['reset']}") + + # Small penalty to charm + player['charm_bonus'] = max(player.get('charm_bonus', 0) - 1, -50) + + # Save player data + self.db.save_database() + + async def handle_shop(self, nick, channel, player): + """Handle shop command""" + shop_items = [ + "=== DUCK HUNT SHOP ===", + "1. Extra Shots (3) - $50", + "2. Faster Reload - $100", + "3. Accuracy Charm - $75", + "4. Lucky Charm - $125", + "5. Friendship Bracelet - $80", + "6. Duck Caller - $200", + "7. Camouflage - $150", + "8. Energy Drink - $60", + "==================", + f"Your money: ${player['money']}", + "Use !use to purchase/use items" + ] + for line in shop_items: + await self.send_user_message(nick, channel, line) + + async def handle_sell(self, nick, channel, item_id, player): + """Handle sell command""" + try: + item_id = int(item_id) + except ValueError: + await self.send_user_message(nick, channel, "Invalid item ID!") + return + + # Check if player has the item + if 'inventory' not in player: + player['inventory'] = {} + + item_key = str(item_id) + if item_key not in player['inventory'] or player['inventory'][item_key] <= 0: + await self.send_user_message(nick, channel, "You don't have that item!") + return + + # Get item info from built-in shop items + shop_items = { + 1: {'name': 'Extra Shots', 'price': 50}, + 2: {'name': 'Faster Reload', 'price': 100}, + 3: {'name': 'Accuracy Charm', 'price': 75}, + 4: {'name': 'Lucky Charm', 'price': 125}, + 5: {'name': 'Friendship Bracelet', 'price': 80}, + 6: {'name': 'Duck Caller', 'price': 200}, + 7: {'name': 'Camouflage', 'price': 150}, + 8: {'name': 'Energy Drink', 'price': 60} + } + item_info = shop_items.get(item_id) + if not item_info: + await self.send_user_message(nick, channel, "Invalid item!") + return + + # Remove item and add money (50% of original price) + player['inventory'][item_key] -= 1 + if player['inventory'][item_key] <= 0: + del player['inventory'][item_key] + + sell_price = item_info['price'] // 2 + player['money'] += sell_price + + message = f"Sold {item_info['name']} for ${sell_price}!" + await self.send_user_message(nick, channel, message) + + # Save player data + self.db.save_database() + + async def handle_use(self, nick, channel, item_id, player, target_nick=None): + """Handle use command""" + try: + item_id = int(item_id) + except ValueError: + await self.send_user_message(nick, channel, "Invalid item ID!") + return + + # Check if it's a shop purchase or inventory use + if 'inventory' not in player: + player['inventory'] = {} + + # Get item info from built-in shop items + shop_items = { + 1: {'name': 'Extra Shots', 'price': 50, 'consumable': True}, + 2: {'name': 'Faster Reload', 'price': 100, 'consumable': True}, + 3: {'name': 'Accuracy Charm', 'price': 75, 'consumable': False}, + 4: {'name': 'Lucky Charm', 'price': 125, 'consumable': False}, + 5: {'name': 'Friendship Bracelet', 'price': 80, 'consumable': False}, + 6: {'name': 'Duck Caller', 'price': 200, 'consumable': True}, + 7: {'name': 'Camouflage', 'price': 150, 'consumable': True}, + 8: {'name': 'Energy Drink', 'price': 60, 'consumable': True} + } + + item_key = str(item_id) + item_info = shop_items.get(item_id) + + if not item_info: + await self.send_user_message(nick, channel, "Invalid item ID!") + return + + # Check if player owns the item + if item_key in player['inventory'] and player['inventory'][item_key] > 0: + # Use owned item + await self.use_item_effect(player, item_id, nick, channel, target_nick) + player['inventory'][item_key] -= 1 + if player['inventory'][item_key] <= 0: + del player['inventory'][item_key] + else: + # Try to buy item + if player['money'] >= item_info['price']: + if item_info.get('consumable', True): + # Buy and immediately use consumable item + player['money'] -= item_info['price'] + await self.use_item_effect(player, item_id, nick, channel, target_nick) + else: + # Buy permanent item and add to inventory + player['money'] -= item_info['price'] + player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1 + await self.send_user_message(nick, channel, f"Purchased {item_info['name']}!") + else: + await self.send_user_message(nick, channel, + f"Not enough money! Need ${item_info['price']}, you have ${player['money']}") + return + + # Save player data + self.db.save_database() + + async def handle_topduck(self, nick, channel): + """Handle topduck command - show leaderboard""" + # Sort players by ducks shot + sorted_players = sorted( + [(name, data) for name, data in self.db.players.items()], + key=lambda x: x[1]['ducks_shot'], + reverse=True + ) + + if not sorted_players: + await self.send_user_message(nick, channel, "No players found!") + return + + # Show top 5 + await self.send_user_message(nick, channel, "=== TOP DUCK HUNTERS ===") + for i, (name, data) in enumerate(sorted_players[:5], 1): + stats = f"{i}. {name}: {data['ducks_shot']} ducks (Level {data['level']})" + await self.send_user_message(nick, channel, stats) + + async def handle_snatch(self, nick, channel, player): + """Handle snatch command - grab dropped items competitively""" + import time + + # Check if there are any dropped items in this channel + if channel not in self.dropped_items or not self.dropped_items[channel]: + await self.send_user_message(nick, channel, f"{nick} > There are no items to snatch!") + return + + # Get the oldest dropped item (first come, first served) + item = self.dropped_items[channel].pop(0) + + # Check if item has expired (60 seconds timeout) + current_time = time.time() + if current_time - item['timestamp'] > 60: + await self.send_user_message(nick, channel, f"{nick} > The item has disappeared!") + # Clean up any other expired items while we're at it + self.dropped_items[channel] = [ + i for i in self.dropped_items[channel] + if current_time - i['timestamp'] <= 60 + ] + return + + # Initialize player inventory if needed + if 'inventory' not in player: + player['inventory'] = {} + + # Add item to player's inventory + item_key = item['item_id'] + player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1 + + # Success message - eggdrop style + message = f"{nick} snatched a {item['item_name']}! ⚡" + self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") + + async def handle_rearm(self, nick, channel, player, target_nick=None): + """Handle rearm command - restore confiscated guns""" + if target_nick: + # Rearm another player (admin only) + target_player = self.db.get_player(target_nick.lower()) + if target_player: + target_player['gun_confiscated'] = False + target_player['shots'] = target_player['max_shots'] + target_player['chargers'] = target_player.get('max_chargers', 2) + target_player['jammed'] = False + target_player['last_reload'] = 0 # Reset reload timer + message = f"{nick} returned {target_nick}'s confiscated gun! | Ammo: {target_player['shots']}/{target_player['max_shots']} | Chargers: {target_player['chargers']}/{target_player.get('max_chargers', 2)}" + self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") + else: + await self.send_user_message(nick, channel, "Player not found!") + else: + # Check if gun is confiscated + if not player.get('gun_confiscated', False): + await self.send_user_message(nick, channel, f"{nick} > Your gun is not confiscated!") + return + + # Rearm self (automatic gun return system or admin) + if self.is_admin(nick): + player['gun_confiscated'] = False + player['shots'] = player['max_shots'] + player['chargers'] = player.get('max_chargers', 2) + player['jammed'] = False + player['last_reload'] = 0 + message = f"{nick} > Gun returned by admin! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player['chargers']}/{player.get('max_chargers', 2)}" + self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") + else: + await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! Wait for an admin or automatic return.") + + self.db.save_database() + # Save database + self.db.save_database() + + async def handle_disarm(self, nick, channel, target_nick): + """Handle disarm command (admin only)""" + target_player = self.db.get_player(target_nick.lower()) + if target_player: + target_player['shots'] = 0 + message = f"Admin {nick} disarmed {target_nick}!" + self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}") + + # Save database + self.db.save_database() + else: + await self.send_user_message(nick, channel, "Player not found!") + + async def handle_ducklaunch(self, nick, channel): + """Handle !ducklaunch admin command""" + duck = await self.game.spawn_duck_now(channel) + if duck: + self.send_message(channel, + f"{self.colors['green']}Admin {nick} launched a duck!{self.colors['reset']}") + else: + await self.send_user_message(nick, channel, "Failed to spawn duck (channel may be full)!") + + async def handle_duckstats(self, nick, channel, player): + """Handle duckstats command""" + stats_msg = ( + f"{nick}'s duck hunting stats: " + f"Level {player['level']} | " + f"Ducks shot: {player['ducks_shot']} | " + f"Befriended: {player['ducks_befriended']} | " + f"Money: ${player['money']} | " + f"XP: {player['exp']}/{self.get_xp_for_level(player['level'] + 1)}" + ) + await self.send_user_message(nick, channel, stats_msg) + + # Show inventory if player has items + if 'inventory' in player and player['inventory']: + # Get item names + shop_items = { + 1: 'Extra Shots', 2: 'Faster Reload', 3: 'Accuracy Charm', 4: 'Lucky Charm', + 5: 'Friendship Bracelet', 6: 'Duck Caller', 7: 'Camouflage', 8: 'Energy Drink', + 9: 'Armor Vest', 10: 'Gunpowder', 11: 'Sight', 12: 'Silencer', + 13: 'Explosive Ammo', 14: 'Mirror', 15: 'Sunglasses', 16: 'Clothes', + 17: 'Grease', 18: 'Brush', 19: 'Sand', 20: 'Water', + 21: 'Sabotage Kit', 22: 'Life Insurance', 23: 'Decoy' + } + + inventory_items = [] + for item_id, quantity in player['inventory'].items(): + item_name = shop_items.get(int(item_id), f"Item {item_id}") + inventory_items.append(f"{item_name} x{quantity}") + + if inventory_items: + inventory_msg = f"Inventory: {', '.join(inventory_items)}" + await self.send_user_message(nick, channel, inventory_msg) + + async def handle_duckhelp(self, nick, channel): + """Handle duckhelp command""" + help_lines = [ + "=== DUCK HUNT COMMANDS ===", + "!bang - Shoot at ducks", + "!reload - Reload your gun", + "!bef - Befriend a duck", + "!duckstats - View your statistics", + "!shop - View the shop", + "!inventory - View your items", + "!use - Use/buy shop items", + "!sell - Sell inventory items", + "!topduck - View leaderboard", + "!rearm - Quick reload (costs money)", + "!ducklaunch - Spawn duck (admin)", + "!disarm - Disarm player (admin)", + "!reset - Reset player (admin)", + "========================" + ] + for line in help_lines: + await self.send_user_message(nick, channel, line) + + async def handle_ignore(self, nick, channel, target_nick): + """Handle ignore command""" + if 'ignored_users' not in self.db.players[nick.lower()]: + self.db.players[nick.lower()]['ignored_users'] = [] + + ignored_list = self.db.players[nick.lower()]['ignored_users'] + if target_nick.lower() not in ignored_list: + ignored_list.append(target_nick.lower()) + await self.send_user_message(nick, channel, f"Now ignoring {target_nick}") + self.db.save_database() + else: + await self.send_user_message(nick, channel, f"{target_nick} is already ignored") + + async def handle_delignore(self, nick, channel, target_nick): + """Handle delignore command""" + if 'ignored_users' not in self.db.players[nick.lower()]: + await self.send_user_message(nick, channel, f"{target_nick} is not ignored") + return + + ignored_list = self.db.players[nick.lower()]['ignored_users'] + if target_nick.lower() in ignored_list: + ignored_list.remove(target_nick.lower()) + await self.send_user_message(nick, channel, f"No longer ignoring {target_nick}") + self.db.save_database() + else: + await self.send_user_message(nick, channel, f"{target_nick} is not ignored") + + async def handle_reset(self, nick, channel, target_nick): + """Handle !reset admin command (requires confirmation)""" + await self.send_user_message(nick, channel, + f"⚠️ WARNING: This will completely reset {target_nick}'s progress! " + f"Use `!reset {target_nick} confirm` to proceed.") + + async def handle_reset_confirm(self, nick, channel, target_nick): + """Handle !reset confirm admin command""" + if target_nick.lower() in self.db.players: + del self.db.players[target_nick.lower()] + self.send_message(channel, + f"{self.colors['red']}Admin {nick} has reset {target_nick}'s progress!{self.colors['reset']}") + self.db.save_database() + else: + await self.send_user_message(nick, channel, "Player not found!") + + async def check_level_up(self, nick, channel, player): + """Check if player leveled up""" + current_level = player['level'] + new_level = self.calculate_level(player['exp']) + + if new_level > current_level: + player['level'] = new_level + + # Award level-up bonuses + player['max_shots'] = min(player['max_shots'] + 1, 10) # Max 10 shots + player['reload_time'] = max(player['reload_time'] - 0.5, 2.0) # Min 2 seconds + + message = (f"🎉 {nick} leveled up to level {new_level}! " + f"Max shots: {player['max_shots']}, " + f"Reload time: {player['reload_time']}s") + self.send_message(channel, f"{self.colors['yellow']}{message}{self.colors['reset']}") + + def calculate_level(self, exp): + """Calculate level from experience points""" + # Exponential level curve: level = floor(sqrt(exp / 100)) + import math + return int(math.sqrt(exp / 100)) + 1 + + def get_xp_for_level(self, level): + """Get XP required for a specific level""" + return (level - 1) ** 2 * 100 + + async def drop_random_item(self, nick, channel): + """Drop a random item to the ground for competitive snatching""" + import time + + # Simple random item IDs + item_ids = [1, 2, 3, 4, 5, 6, 7, 8] + item_id = random.choice(item_ids) + item_key = str(item_id) + + item_names = { + '1': 'Extra Shots', '2': 'Faster Reload', '3': 'Accuracy Charm', + '4': 'Lucky Charm', '5': 'Friendship Bracelet', '6': 'Duck Caller', + '7': 'Camouflage', '8': 'Energy Drink' + } + + item_name = item_names.get(item_key, f'Item {item_id}') + + # Initialize channel dropped items if needed + if channel not in self.dropped_items: + self.dropped_items[channel] = [] + + # Add item to dropped items + dropped_item = { + 'item_id': item_key, + 'item_name': item_name, + 'timestamp': time.time(), + 'dropper': nick + } + self.dropped_items[channel].append(dropped_item) + + # Announce the drop - eggdrop style + message = f"🎁 A {item_name} has been dropped! Type !snatch to grab it!" + self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}") + + async def award_random_item(self, nick, channel, player): + """Award a random item to player""" + if 'inventory' not in player: + player['inventory'] = {} + + # Simple random item IDs + item_ids = [1, 2, 3, 4, 5, 6, 7, 8] + item_id = random.choice(item_ids) + item_key = str(item_id) + + player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1 + + item_names = { + '1': 'Extra Shots', '2': 'Faster Reload', '3': 'Accuracy Charm', + '4': 'Lucky Charm', '5': 'Friendship Bracelet', '6': 'Duck Caller', + '7': 'Camouflage', '8': 'Energy Drink' + } + + item_name = item_names.get(item_key, f'Item {item_id}') + message = f"🎁 {nick} found a {item_name}!" + self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}") + + async def use_item_effect(self, player, item_id, nick, channel, target_nick=None): + """Apply item effects""" + effects = { + 1: "Extra Shots! +3 shots", # Extra shots + 2: "Faster Reload! -1s reload time", # Faster reload + 3: "Accuracy Charm! +5 accuracy", # Accuracy charm + 4: "Lucky Charm! +10 luck", # Lucky charm + 5: "Friendship Bracelet! +5 charm", # Friendship bracelet + 6: "Duck Caller! Next duck spawns faster", # Duck caller + 7: "Camouflage! Ducks can't see you for 60s", # Camouflage + 8: "Energy Drink! +50 energy" # Energy drink + } + + # Apply item effects + if item_id == 1: # Extra shots + player['shots'] = min(player['shots'] + 3, player['max_shots']) + elif item_id == 2: # Faster reload + player['reload_time'] = max(player['reload_time'] - 1, 1) + elif item_id == 3: # Accuracy charm + player['accuracy_bonus'] = player.get('accuracy_bonus', 0) + 5 + elif item_id == 4: # Lucky charm + player['luck'] = player.get('luck', 0) + 10 + elif item_id == 5: # Friendship bracelet + player['charm_bonus'] = player.get('charm_bonus', 0) + 5 + elif item_id == 6: # Duck caller + # Could implement faster duck spawning here + pass + elif item_id == 7: # Camouflage + player['camouflaged_until'] = time.time() + 60 + elif item_id == 8: # Energy drink + player['energy'] = player.get('energy', 100) + 50 + + effect_msg = effects.get(item_id, "Unknown effect") + await self.send_user_message(nick, channel, f"Used item: {effect_msg}") + + async def cleanup_expired_items(self): + """Background task to clean up expired dropped items""" + import time + + while not self.shutdown_requested: + try: + current_time = time.time() + + # Clean up expired items from all channels + for channel in list(self.dropped_items.keys()): + if channel in self.dropped_items: + original_count = len(self.dropped_items[channel]) + + # Remove items older than 60 seconds + self.dropped_items[channel] = [ + item for item in self.dropped_items[channel] + if current_time - item['timestamp'] <= 60 + ] + + # Optional: log cleanup if items were removed + removed_count = original_count - len(self.dropped_items[channel]) + if removed_count > 0: + self.logger.debug(f"Cleaned up {removed_count} expired items from {channel}") + + # Sleep for 30 seconds before next cleanup + await asyncio.sleep(30) + + except Exception as e: + self.logger.error(f"Error in cleanup_expired_items: {e}") + await asyncio.sleep(30) # Continue trying after error + + async def run(self): + """Main bot run loop""" + tasks = [] # Initialize tasks list early + try: + # Setup signal handlers + self.setup_signal_handlers() + + # Load database + self.db.load_database() + + # Connect to IRC + await self.connect() + + # Start background tasks + tasks = [ + asyncio.create_task(self.message_loop()), + asyncio.create_task(self.game.spawn_ducks()), + asyncio.create_task(self.game.duck_timeout_checker()), + asyncio.create_task(self.cleanup_expired_items()), + ] + + # Wait for shutdown or task completion + try: + while not self.shutdown_requested: + # Check if any critical task has failed + for task in tasks: + if task.done() and task.exception(): + self.logger.error(f"Task failed: {task.exception()}") + self.shutdown_requested = True + break + + await asyncio.sleep(0.1) # Short sleep to allow signal handling + + except asyncio.CancelledError: + self.logger.info("Main loop cancelled") + except KeyboardInterrupt: + self.logger.info("Keyboard interrupt received") + self.shutdown_requested = True + + except Exception as e: + self.logger.error(f"Bot error: {e}") + raise + finally: + # Cleanup + self.logger.info("Shutting down bot...") + + # Cancel all tasks + for task in tasks: + if not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + except Exception as e: + self.logger.error(f"Error cancelling task: {e}") + + # Save database + try: + self.db.save_database() + self.logger.info("Database saved") + except Exception as e: + self.logger.error(f"Error saving database: {e}") + + # Close connection + if self.writer and not self.writer.is_closing(): + try: + self.send_raw("QUIT :Bot shutting down") + self.writer.close() + await self.writer.wait_closed() + self.logger.info("IRC connection closed") + except Exception as e: + self.logger.error(f"Error closing connection: {e}") + + self.logger.info("Bot shutdown complete") + + async def message_loop(self): + """Handle incoming IRC messages""" + while not self.shutdown_requested and self.reader: + try: + # Use a timeout so we can check shutdown_requested regularly + line = await asyncio.wait_for(self.reader.readline(), timeout=1.0) + if not line: + self.logger.warning("Empty line received, connection may be closed") + break + + line = line.decode().strip() + if line: + prefix, command, params, trailing = parse_message(line) + await self.handle_message(prefix, command, params, trailing) + + except asyncio.TimeoutError: + # Timeout is expected - just continue to check shutdown flag + continue + except asyncio.CancelledError: + self.logger.info("Message loop cancelled") + break + except Exception as e: + self.logger.error(f"Message loop error: {e}") + break + + self.logger.info("Message loop ended") \ No newline at end of file diff --git a/src/game.py b/src/game.py new file mode 100644 index 0000000..cc5f74c --- /dev/null +++ b/src/game.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +""" +Game mechanics for DuckHunt Bot +""" + +import asyncio +import random +import time +import uuid +import logging +from typing import Dict, Any, Optional, List + + +class DuckGame: + """Game mechanics and duck management""" + + def __init__(self, bot, db): + self.bot = bot + self.db = db + self.ducks = {} # Format: {channel: [{'alive': True, 'spawn_time': time, 'id': uuid}, ...]} + self.logger = logging.getLogger('DuckHuntBot.Game') + + # Colors for IRC messages + self.colors = { + 'red': '\x0304', + 'green': '\x0303', + 'yellow': '\x0308', + 'blue': '\x0302', + 'cyan': '\x0311', + 'magenta': '\x0306', + 'white': '\x0300', + 'bold': '\x02', + 'reset': '\x03', + 'underline': '\x1f' + } + + def get_config(self, path, default=None): + """Helper method to get config values""" + return self.bot.get_config(path, default) + + def get_player_level(self, xp): + """Calculate player level from XP""" + if xp < 0: + return 0 + return int((xp ** 0.5) / 2) + 1 + + def get_xp_for_next_level(self, xp): + """Calculate XP needed for next level""" + level = self.get_player_level(xp) + return ((level * 2) ** 2) - xp + + def calculate_penalty_by_level(self, base_penalty, xp): + """Reduce penalties for higher level players""" + level = self.get_player_level(xp) + return max(1, base_penalty - (level - 1)) + + def update_karma(self, player, event): + """Update player karma based on events""" + if not self.get_config('karma.enabled', True): + return + + karma_changes = { + 'hit': self.get_config('karma.hit_bonus', 2), + 'golden_hit': self.get_config('karma.golden_hit_bonus', 5), + 'teamkill': -self.get_config('karma.teamkill_penalty', 10), + 'wild_shot': -self.get_config('karma.wild_shot_penalty', 3), + 'miss': -self.get_config('karma.miss_penalty', 1), + 'befriend_success': self.get_config('karma.befriend_success_bonus', 2), + 'befriend_fail': -self.get_config('karma.befriend_fail_penalty', 1) + } + + if event in karma_changes: + player['karma'] = player.get('karma', 0) + karma_changes[event] + + def is_sleep_time(self): + """Check if ducks should not spawn due to sleep hours""" + sleep_hours = self.get_config('sleep_hours', []) + if not sleep_hours or len(sleep_hours) != 2: + return False + + import datetime + current_hour = datetime.datetime.now().hour + start_hour, end_hour = sleep_hours + + if start_hour <= end_hour: + return start_hour <= current_hour <= end_hour + else: # Crosses midnight + return current_hour >= start_hour or current_hour <= end_hour + + def calculate_gun_reliability(self, player): + """Calculate gun reliability with modifiers""" + base_reliability = player.get('reliability', 70) + # Add weapon modifiers, items, etc. + return min(100, max(0, base_reliability)) + + def gun_jams(self, player): + """Check if gun jams (eggdrop style)""" + # Base jamming probability is inverse of reliability + reliability = player.get('reliability', 70) + jam_chance = max(1, 101 - reliability) # Higher reliability = lower jam chance + + # Additional factors that increase jam chance + if player.get('total_ammo_used', 0) > 100: + jam_chance += 2 # Gun gets more prone to jamming with use + + if player.get('jammed_count', 0) > 5: + jam_chance += 1 # Previously jammed guns are more prone to jamming + + # Roll for jam (1-100, jam if roll <= jam_chance) + return random.randint(1, 100) <= jam_chance + + async def scare_other_ducks(self, channel, shot_duck_id): + """Scare other ducks when one is shot""" + if channel not in self.ducks: + return + + for duck in self.ducks[channel][:]: # Copy list to avoid modification during iteration + if duck['id'] != shot_duck_id and duck['alive']: + # 30% chance to scare away other ducks + if random.random() < 0.3: + duck['alive'] = False + self.ducks[channel].remove(duck) + + async def scare_duck_on_miss(self, channel, target_duck): + """Scare duck when someone misses""" + if target_duck and random.random() < 0.15: # 15% chance + target_duck['alive'] = False + if channel in self.ducks and target_duck in self.ducks[channel]: + self.ducks[channel].remove(target_duck) + + async def find_bushes_items(self, nick, channel, player): + """Find random items in bushes""" + if not self.get_config('items.enabled', True): + return + + if random.random() < 0.1: # 10% chance + items = [ + ("a mirror", "mirror", "You can now deflect shots!"), + ("some sand", "sand", "Throw this to blind opponents!"), + ("a rusty bullet", None, "It's too rusty to use..."), + ("some bread crumbs", "bread", "Feed ducks to make them friendly!"), + ] + + found_item, item_key, message = random.choice(items) + + if item_key and item_key in player: + player[item_key] = player.get(item_key, 0) + 1 + elif item_key in player: + player[item_key] = player.get(item_key, 0) + 1 + + await self.bot.send_user_message(nick, channel, + f"You found {found_item} in the bushes! {message}") + + def get_duck_spawn_message(self): + """Get random duck spawn message (eggdrop style)""" + messages = [ + "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O< QUACK", + "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o< QUACK!", + "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O< QUAAACK!", + "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_ö< Quack?", + "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O< *QUACK*" + ] + return random.choice(messages) + + async def spawn_duck_now(self, channel, force_golden=False): + """Spawn a duck immediately in the specified channel""" + if channel not in self.ducks: + self.ducks[channel] = [] + + max_ducks = self.get_config('max_ducks_per_channel', 3) + if len([d for d in self.ducks[channel] if d['alive']]) >= max_ducks: + self.logger.debug(f"Max ducks already in {channel}") + return + + # Determine duck type + if force_golden: + duck_type = "golden" + else: + rand = random.random() + if rand < 0.02: + duck_type = "armored" + elif rand < 0.10: + duck_type = "golden" + elif rand < 0.30: + duck_type = "rare" + elif rand < 0.40: + duck_type = "fast" + else: + duck_type = "normal" + + # Get duck configuration + duck_config = self.get_config(f'duck_types.{duck_type}', {}) + if not duck_config.get('enabled', True): + duck_type = "normal" + duck_config = self.get_config('duck_types.normal', {}) + + # Create duck + duck = { + 'id': str(uuid.uuid4())[:8], + 'type': duck_type, + 'alive': True, + 'spawn_time': time.time(), + 'health': duck_config.get('health', 1), + 'max_health': duck_config.get('health', 1) + } + + self.ducks[channel].append(duck) + + # Send spawn message + messages = duck_config.get('messages', [self.get_duck_spawn_message()]) + spawn_message = random.choice(messages) + + self.bot.send_message(channel, spawn_message) + self.logger.info(f"Spawned {duck_type} duck in {channel}") + + # Alert users who have alerts enabled + await self.send_duck_alerts(channel, duck_type) + + return duck + + async def send_duck_alerts(self, channel, duck_type): + """Send alerts to users who have them enabled""" + if not self.get_config('social.duck_alerts_enabled', True): + return + + # Implementation would iterate through players with alerts enabled + # For now, just log + self.logger.debug(f"Duck alerts for {duck_type} duck in {channel}") + + async def spawn_ducks(self): + """Main duck spawning loop""" + while not self.bot.shutdown_requested: + try: + if self.is_sleep_time(): + await asyncio.sleep(300) # Check every 5 minutes during sleep + continue + + for channel in self.bot.channels_joined: + if self.bot.shutdown_requested: + break + + if channel not in self.ducks: + self.ducks[channel] = [] + + # Clean up dead ducks + self.ducks[channel] = [d for d in self.ducks[channel] if d['alive']] + + max_ducks = self.get_config('max_ducks_per_channel', 3) + alive_ducks = len([d for d in self.ducks[channel] if d['alive']]) + + if alive_ducks < max_ducks: + min_spawn_time = self.get_config('duck_spawn_min', 1800) + max_spawn_time = self.get_config('duck_spawn_max', 5400) + + if random.random() < 0.1: # 10% chance each check + await self.spawn_duck_now(channel) + + await asyncio.sleep(random.randint(60, 300)) # Check every 1-5 minutes + + except asyncio.CancelledError: + self.logger.info("Duck spawning loop cancelled") + break + except Exception as e: + self.logger.error(f"Error in duck spawning: {e}") + await asyncio.sleep(60) + + async def duck_timeout_checker(self): + """Check for ducks that should timeout""" + while not self.bot.shutdown_requested: + try: + current_time = time.time() + + for channel in list(self.ducks.keys()): + if self.bot.shutdown_requested: + break + + if channel not in self.ducks: + continue + + for duck in self.ducks[channel][:]: # Copy to avoid modification + if not duck['alive']: + continue + + age = current_time - duck['spawn_time'] + min_timeout = self.get_config('duck_timeout_min', 45) + max_timeout = self.get_config('duck_timeout_max', 75) + + timeout = random.randint(min_timeout, max_timeout) + + if age > timeout: + duck['alive'] = False + self.ducks[channel].remove(duck) + + # Send timeout message (eggdrop style) + timeout_messages = [ + "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The duck flew away!", + "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O> *FLAP FLAP FLAP*", + "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The duck got tired of waiting and left!", + "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O> *KWAK* The duck escaped!" + ] + self.bot.send_message(channel, random.choice(timeout_messages)) + self.logger.debug(f"Duck timed out in {channel}") + + await asyncio.sleep(10) # Check every 10 seconds + + except asyncio.CancelledError: + self.logger.info("Duck timeout checker cancelled") + break + except Exception as e: + self.logger.error(f"Error in duck timeout checker: {e}") + await asyncio.sleep(30) + + def get_alive_ducks(self, channel): + """Get list of alive ducks in channel""" + if channel not in self.ducks: + return [] + return [d for d in self.ducks[channel] if d['alive']] + + def get_duck_by_id(self, channel, duck_id): + """Get duck by ID""" + if channel not in self.ducks: + return None + for duck in self.ducks[channel]: + if duck['id'] == duck_id and duck['alive']: + return duck + return None \ No newline at end of file diff --git a/src/logging_utils.py b/src/logging_utils.py new file mode 100644 index 0000000..e27cf89 --- /dev/null +++ b/src/logging_utils.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +Logging utilities for DuckHunt Bot +""" + +import logging +import logging.handlers + + +class DetailedColorFormatter(logging.Formatter): + """Console formatter with color support""" + COLORS = { + 'DEBUG': '\033[94m', # Blue + 'INFO': '\033[92m', # Green + 'WARNING': '\033[93m', # Yellow + 'ERROR': '\033[91m', # Red + 'CRITICAL': '\033[95m', # Magenta + 'ENDC': '\033[0m' # End color + } + + def format(self, record): + color = self.COLORS.get(record.levelname, '') + endc = self.COLORS['ENDC'] + msg = super().format(record) + return f"{color}{msg}{endc}" + + +class DetailedFileFormatter(logging.Formatter): + """File formatter with extra context but no colors""" + def format(self, record): + return super().format(record) + + +def setup_logger(name="DuckHuntBot"): + """Setup logger with console and file handlers""" + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + + # Clear any existing handlers + logger.handlers.clear() + + # Console handler with colors + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_formatter = DetailedColorFormatter( + '%(asctime)s [%(levelname)s] %(name)s: %(message)s' + ) + console_handler.setFormatter(console_formatter) + logger.addHandler(console_handler) + + # File handler with rotation for detailed logs + try: + file_handler = logging.handlers.RotatingFileHandler( + 'duckhunt.log', + maxBytes=10*1024*1024, # 10MB per file + backupCount=5 # Keep 5 backup files + ) + file_handler.setLevel(logging.DEBUG) + file_formatter = DetailedFileFormatter( + '%(asctime)s [%(levelname)-8s] %(name)s - %(funcName)s:%(lineno)d: %(message)s' + ) + file_handler.setFormatter(file_formatter) + logger.addHandler(file_handler) + + logger.info("Enhanced logging system initialized with file rotation") + except Exception as e: + logger.error(f"Failed to setup file logging: {e}") + + return logger \ No newline at end of file diff --git a/duckhunt/src/sasl.py b/src/sasl.py similarity index 100% rename from duckhunt/src/sasl.py rename to src/sasl.py diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..7564c0c --- /dev/null +++ b/src/utils.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +Utility functions for DuckHunt Bot +""" + +import re +from typing import Optional + + +class InputValidator: + """Input validation utilities""" + + @staticmethod + def validate_nickname(nick: str) -> bool: + """Validate IRC nickname format""" + if not nick or len(nick) > 30: + return False + # RFC 2812 nickname pattern + pattern = r'^[a-zA-Z\[\]\\`_^{|}][a-zA-Z0-9\[\]\\`_^{|}\-]*$' + return bool(re.match(pattern, nick)) + + @staticmethod + def validate_channel(channel: str) -> bool: + """Validate IRC channel format""" + if not channel or len(channel) > 50: + return False + return channel.startswith('#') and ' ' not in channel + + @staticmethod + def validate_numeric_input(value: str, min_val: Optional[int] = None, max_val: Optional[int] = None) -> Optional[int]: + """Safely parse and validate numeric input""" + try: + num = int(value) + if min_val is not None and num < min_val: + return None + if max_val is not None and num > max_val: + return None + return num + except (ValueError, TypeError): + return None + + @staticmethod + def sanitize_message(message: str) -> str: + """Sanitize user input message""" + if not message: + return "" + # Remove control characters and limit length + sanitized = ''.join(char for char in message if ord(char) >= 32 or char in '\t\n') + return sanitized[:500] # Limit message length + + +def parse_message(line): + """Parse IRC message format""" + prefix = '' + trailing = '' + if line.startswith(':'): + prefix, line = line[1:].split(' ', 1) + if ' :' in line: + line, trailing = line.split(' :', 1) + parts = line.split() + command = parts[0] if parts else '' + params = parts[1:] if len(parts) > 1 else [] + return prefix, command, params, trailing \ No newline at end of file