mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
884 Commits
v1.0.0-bet
...
v1.4.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
249ee3bb5a | ||
|
|
a3b3d4ea0e | ||
|
|
27f9d04538 | ||
|
|
03f1869b23 | ||
|
|
479e177a64 | ||
|
|
5cf166af87 | ||
|
|
e24bcd7d38 | ||
|
|
768898df64 | ||
|
|
cf282e04bb | ||
|
|
db4edac083 | ||
|
|
877d0cf7f8 | ||
|
|
e78c441a6e | ||
|
|
e945819365 | ||
|
|
23e8db50fd | ||
|
|
193ffe6394 | ||
|
|
87016186d8 | ||
|
|
d7d96a89cf | ||
|
|
aa5ef23363 | ||
|
|
c18e0401e4 | ||
|
|
8568990295 | ||
|
|
44e6460224 | ||
|
|
d53480290c | ||
|
|
1499d883bc | ||
|
|
883a6902fa | ||
|
|
6d3b754c6c | ||
|
|
62f73ce2e6 | ||
|
|
eeab9f3fb1 | ||
|
|
c21a67d1cf | ||
|
|
afe48a44da | ||
|
|
7e4822e4ec | ||
|
|
705ab6a980 | ||
|
|
963b29eea4 | ||
|
|
b3f889c4c7 | ||
|
|
545b4891b4 | ||
|
|
c89f14b3c2 | ||
|
|
c416b00383 | ||
|
|
669a891eeb | ||
|
|
520d58b262 | ||
|
|
24dff868ff | ||
|
|
cf45bb5060 | ||
|
|
0f9064f2c3 | ||
|
|
f94f329b1f | ||
|
|
dc4560081d | ||
|
|
b42cd0e6dc | ||
|
|
bbe1e45541 | ||
|
|
2c61db1215 | ||
|
|
fde2bb94d9 | ||
|
|
436a43d3ad | ||
|
|
6b2a6f3a83 | ||
|
|
8e5773115c | ||
|
|
626a5dfe16 | ||
|
|
e63f4816c4 | ||
|
|
13852b194b | ||
|
|
a68c20098b | ||
|
|
432b5a767e | ||
|
|
952659198c | ||
|
|
4e518758e5 | ||
|
|
e1b3dd311f | ||
|
|
bb0f923155 | ||
|
|
ab86f02bd7 | ||
|
|
43067cfb07 | ||
|
|
3300694059 | ||
|
|
f59b8715dd | ||
|
|
60abadd1fc | ||
|
|
4297c91c5e | ||
|
|
c8eddc3787 | ||
|
|
d01d81a6d7 | ||
|
|
40b31fd8af | ||
|
|
7b995b35cd | ||
|
|
00885d57c9 | ||
|
|
d03d7dbc47 | ||
|
|
7fd4074bd3 | ||
|
|
8367bca4d5 | ||
|
|
5059990adb | ||
|
|
9dd9d39df4 | ||
|
|
87f89fea6d | ||
|
|
8433b3cf5f | ||
|
|
eb9f1eb4c2 | ||
|
|
7308597f23 | ||
|
|
a4837cf337 | ||
|
|
dd8ba14bbe | ||
|
|
5a5b394a17 | ||
|
|
ab8eb41853 | ||
|
|
38bfcc1581 | ||
|
|
1c7840a203 | ||
|
|
b5fb7e997c | ||
|
|
1df2fd3486 | ||
|
|
957619933d | ||
|
|
3f9fdb10a3 | ||
|
|
bf48a61766 | ||
|
|
6a1606ca6c | ||
|
|
7551ff2ecb | ||
|
|
20e864b672 | ||
|
|
91f7ea072f | ||
|
|
8b62d70562 | ||
|
|
fd43eb0ea1 | ||
|
|
759b89f790 | ||
|
|
54a27df86d | ||
|
|
4a99c44702 | ||
|
|
ed01b1fb87 | ||
|
|
f3e135e5f8 | ||
|
|
c51ef99946 | ||
|
|
01ee3f7418 | ||
|
|
88e58e65ed | ||
|
|
83673981f2 | ||
|
|
dd398ccacc | ||
|
|
0d43243650 | ||
|
|
bb0088171d | ||
|
|
8f74b19aae | ||
|
|
62a9c84114 | ||
|
|
a863def55a | ||
|
|
59d538cdee | ||
|
|
8aad0c71f7 | ||
|
|
de1b5f08b1 | ||
|
|
75217eea04 | ||
|
|
9d37a9eaa5 | ||
|
|
66d7f98716 | ||
|
|
ae6dfa3321 | ||
|
|
3f4c6f8703 | ||
|
|
eb96d22139 | ||
|
|
448b30e0f2 | ||
|
|
110909f64d | ||
|
|
58b25f6da4 | ||
|
|
91110460fb | ||
|
|
a81fafe268 | ||
|
|
f5512b19da | ||
|
|
6da457bb06 | ||
|
|
3117a9d4ea | ||
|
|
8ffcc18c62 | ||
|
|
81364738cf | ||
|
|
ce9436f449 | ||
|
|
bb83a64317 | ||
|
|
a4bd6f7b83 | ||
|
|
83300d0ef1 | ||
|
|
6a6c10b825 | ||
|
|
cb398c5b1d | ||
|
|
68893741f0 | ||
|
|
64dede5c9a | ||
|
|
3f920aaf60 | ||
|
|
749296a6c0 | ||
|
|
709e5a9949 | ||
|
|
5d35b5fb2c | ||
|
|
3059b88991 | ||
|
|
0fb0b4df0a | ||
|
|
26df351edf | ||
|
|
6e4979df44 | ||
|
|
55a681e7f2 | ||
|
|
4388b6512e | ||
|
|
efd6b882d1 | ||
|
|
9f629c6ec6 | ||
|
|
5aae7e9eeb | ||
|
|
165ff43df4 | ||
|
|
0fe5640b23 | ||
|
|
780d52a444 | ||
|
|
de46e0c679 | ||
|
|
200bee3721 | ||
|
|
c7b4bce1aa | ||
|
|
af2d6c4f97 | ||
|
|
6d71e97590 | ||
|
|
c3a4ac781a | ||
|
|
70a9eb4b93 | ||
|
|
f2e06bcd50 | ||
|
|
416e6951fc | ||
|
|
4e608ad268 | ||
|
|
580f3dd308 | ||
|
|
66f3c75b40 | ||
|
|
66d2593a1c | ||
|
|
7473148a8f | ||
|
|
f748967669 | ||
|
|
f922884f04 | ||
|
|
659c19c3bd | ||
|
|
15a0920943 | ||
|
|
2ae634b434 | ||
|
|
64dff2c1cc | ||
|
|
8354372ffb | ||
|
|
1b1861c7a2 | ||
|
|
f3dc72b234 | ||
|
|
eac0700338 | ||
|
|
d75ad73d63 | ||
|
|
8b72c6d21d | ||
|
|
a3185a5a67 | ||
|
|
e7c9674fb4 | ||
|
|
a56b64fd63 | ||
|
|
c4dcd44efb | ||
|
|
2b90234905 | ||
|
|
336952bc57 | ||
|
|
7d2ff25b05 | ||
|
|
b3a1ac2e1e | ||
|
|
e455da63c2 | ||
|
|
d49da4376a | ||
|
|
ab6f072b1c | ||
|
|
a5ab1aebf3 | ||
|
|
ef18f1cb0c | ||
|
|
639568f9a0 | ||
|
|
bc458d7907 | ||
|
|
f843ee1f27 | ||
|
|
5d3cb8c1f3 | ||
|
|
9df35bd9eb | ||
|
|
0619429c00 | ||
|
|
a5f68e1c20 | ||
|
|
8c32714dfb | ||
|
|
dc561a8d17 | ||
|
|
e2cb809961 | ||
|
|
fe6d7fa663 | ||
|
|
3f3a4ea4e3 | ||
|
|
6df93c3436 | ||
|
|
1dd8c4a102 | ||
|
|
14d8197b1e | ||
|
|
828970cc5e | ||
|
|
d9f596f06d | ||
|
|
0274f96d6c | ||
|
|
a1e108ca5e | ||
|
|
6a8a258dc0 | ||
|
|
92db01c4fe | ||
|
|
208af67a50 | ||
|
|
2aad28cd7f | ||
|
|
b3959e0867 | ||
|
|
b0dca67a70 | ||
|
|
2cd0ff81e4 | ||
|
|
d1a39153b3 | ||
|
|
85a29a6942 | ||
|
|
22d8b889fe | ||
|
|
62bddf8b34 | ||
|
|
75d7783e20 | ||
|
|
93cfbc8230 | ||
|
|
c18d56dac5 | ||
|
|
775c54566c | ||
|
|
94d5c4c325 | ||
|
|
2c57d3aacf | ||
|
|
71dd28e09d | ||
|
|
ee01373b27 | ||
|
|
2eeab7a55e | ||
|
|
67d326c6c7 | ||
|
|
7af52572e0 | ||
|
|
4f6d74a19d | ||
|
|
c3232437af | ||
|
|
cf4b51a297 | ||
|
|
124d5af2f2 | ||
|
|
a0eb4a6a6e | ||
|
|
2f560a9049 | ||
|
|
d9c250b9eb | ||
|
|
e67d9903ac | ||
|
|
6b4ca9b0cd | ||
|
|
5eab13ee0c | ||
|
|
3fe19ab837 | ||
|
|
b61161f257 | ||
|
|
9ed65acfb1 | ||
|
|
0404a7444c | ||
|
|
34b2e7af71 | ||
|
|
6f88d4fa1d | ||
|
|
d486aa73e0 | ||
|
|
f447d8ad55 | ||
|
|
9cd25db5c1 | ||
|
|
c198bd30a5 | ||
|
|
964db770d6 | ||
|
|
a906aaf56b | ||
|
|
0ebac22842 | ||
|
|
c93aee580f | ||
|
|
329fafea47 | ||
|
|
c4b5022f45 | ||
|
|
f31416c7cb | ||
|
|
7267542db9 | ||
|
|
ff4eab235d | ||
|
|
e8caf704fe | ||
|
|
5d6f1a9f95 | ||
|
|
726cd7d4d2 | ||
|
|
34077c6818 | ||
|
|
5434228df0 | ||
|
|
b28593c46a | ||
|
|
1502ccf3ac | ||
|
|
8f47bc5799 | ||
|
|
ea0c06444a | ||
|
|
9d42f856d1 | ||
|
|
e0c6e9313b | ||
|
|
a01ce204ab | ||
|
|
194019c091 | ||
|
|
99884884de | ||
|
|
ea437036db | ||
|
|
1eb0b4fda5 | ||
|
|
484b965bbc | ||
|
|
1ee73008be | ||
|
|
3c38357ca9 | ||
|
|
92f9fd4e69 | ||
|
|
9c9b090af4 | ||
|
|
53e4e0c59b | ||
|
|
df446ccab5 | ||
|
|
0d7ea454b6 | ||
|
|
d57dfb1055 | ||
|
|
99b4e5a1b5 | ||
|
|
c42b6cccb1 | ||
|
|
f24a04d0dc | ||
|
|
8373c4b3c5 | ||
|
|
08d7171d1a | ||
|
|
ede455f2e3 | ||
|
|
b63981286c | ||
|
|
b9dfa38bcc | ||
|
|
fe9c63387b | ||
|
|
ee49f69231 | ||
|
|
94907bec26 | ||
|
|
3566016f7b | ||
|
|
182b725f43 | ||
|
|
6af486d772 | ||
|
|
18db0b0028 | ||
|
|
d95962a358 | ||
|
|
983bf4d61f | ||
|
|
0928d8da0d | ||
|
|
ae6c94da97 | ||
|
|
35492ca88b | ||
|
|
b94029e925 | ||
|
|
aa643610b5 | ||
|
|
573d9ec036 | ||
|
|
c84906f2ce | ||
|
|
96ac640116 | ||
|
|
7bc93f8a79 | ||
|
|
a2e3c59857 | ||
|
|
1306b25950 | ||
|
|
d52956ebf6 | ||
|
|
dd22f41fd0 | ||
|
|
f0df305124 | ||
|
|
073f13b44b | ||
|
|
d6418c941a | ||
|
|
3adcafde2b | ||
|
|
6151daa68b | ||
|
|
89bd5727a1 | ||
|
|
31ea9d8c55 | ||
|
|
6d8e901158 | ||
|
|
1c957ba4d9 | ||
|
|
a2d9049541 | ||
|
|
93ba9615d9 | ||
|
|
1a165b36ae | ||
|
|
cbc3cb8464 | ||
|
|
dad7cccae0 | ||
|
|
f14370ac8d | ||
|
|
783dcad5c0 | ||
|
|
014a45a2dc | ||
|
|
24e93e3bd5 | ||
|
|
5ae16e6de1 | ||
|
|
195e6327e0 | ||
|
|
3649246d19 | ||
|
|
7044f85245 | ||
|
|
fcf8fb5846 | ||
|
|
cd9ab37d00 | ||
|
|
c607913e82 | ||
|
|
919b5e730c | ||
|
|
4fbc0f5817 | ||
|
|
afd1bcae17 | ||
|
|
1334763e3d | ||
|
|
c6725395da | ||
|
|
617ca3ecbc | ||
|
|
bdd376c46d | ||
|
|
8e6f126335 | ||
|
|
70af483e01 | ||
|
|
e1331d7fd5 | ||
|
|
6cec06a14e | ||
|
|
1d32ab9ee7 | ||
|
|
9035be3f5d | ||
|
|
cf127ae0e7 | ||
|
|
7b940c409f | ||
|
|
02e6225240 | ||
|
|
97660d7b2d | ||
|
|
b53c0cfe4f | ||
|
|
b676ace34b | ||
|
|
2d1e9af5cb | ||
|
|
679184e8e0 | ||
|
|
7f912e04e7 | ||
|
|
26ed32d51f | ||
|
|
374d76bb35 | ||
|
|
664c3f1277 | ||
|
|
c48c89b0d9 | ||
|
|
7f1787e52b | ||
|
|
a28f51fa55 | ||
|
|
383102802b | ||
|
|
0516749708 | ||
|
|
002f1570dc | ||
|
|
9d025fc3cd | ||
|
|
03bb13b830 | ||
|
|
b351ed0cf4 | ||
|
|
abeb28c9cc | ||
|
|
2325f74581 | ||
|
|
e75ef3e44d | ||
|
|
6431b45769 | ||
|
|
7dfcbb619f | ||
|
|
051b58ca6f | ||
|
|
5777b39c22 | ||
|
|
eb4f135698 | ||
|
|
3bcb04ece7 | ||
|
|
e42aca875f | ||
|
|
28915ab848 | ||
|
|
0fe491871d | ||
|
|
6fc6a483a8 | ||
|
|
13236880b5 | ||
|
|
9461719039 | ||
|
|
63163cc4c1 | ||
|
|
5cdf159bc1 | ||
|
|
47a9981fdf | ||
|
|
f85bdb7d02 | ||
|
|
7796d03e21 | ||
|
|
90094c082a | ||
|
|
2b695e2f2e | ||
|
|
2e05f3ef64 | ||
|
|
316f1efd08 | ||
|
|
4678a63955 | ||
|
|
8584454d5d | ||
|
|
2c6cf76a10 | ||
|
|
cd3226df21 | ||
|
|
4bcc6ef1f2 | ||
|
|
77e56c25ae | ||
|
|
e7b363612a | ||
|
|
a217c61ba1 | ||
|
|
e7b4fe44c8 | ||
|
|
f8cc580b99 | ||
|
|
03057b3263 | ||
|
|
452b9b7c67 | ||
|
|
5ae16d0adc | ||
|
|
5ed135c023 | ||
|
|
d425298cd9 | ||
|
|
786815d073 | ||
|
|
54cad92a3f | ||
|
|
54e21f4644 | ||
|
|
3c76f177cd | ||
|
|
aa05c62d94 | ||
|
|
3f16158e27 | ||
|
|
6f2824512d | ||
|
|
723b67f886 | ||
|
|
008d55e63b | ||
|
|
79885454ab | ||
|
|
ba21723bdc | ||
|
|
c36c4918a8 | ||
|
|
853147518d | ||
|
|
2f19d86c95 | ||
|
|
39bdabffcb | ||
|
|
a7bdaedfe1 | ||
|
|
1c6106081f | ||
|
|
8ab6cded2e | ||
|
|
ff63bb959a | ||
|
|
6c79bb1ff0 | ||
|
|
ce73336c0c | ||
|
|
248977c5b5 | ||
|
|
77a6e63210 | ||
|
|
6f6fb35177 | ||
|
|
9db565cb4f | ||
|
|
2a254a7fab | ||
|
|
15e76ab029 | ||
|
|
66b95cdaa0 | ||
|
|
32ea93cb88 | ||
|
|
22a2a64861 | ||
|
|
d841fdf02c | ||
|
|
9421f09ded | ||
|
|
b4af552fb9 | ||
|
|
69dfb17460 | ||
|
|
4703750c27 | ||
|
|
40caf99939 | ||
|
|
df5f648b26 | ||
|
|
55472bbbc0 | ||
|
|
f23c4e2b6a | ||
|
|
0b101d662e | ||
|
|
a7f07afc14 | ||
|
|
2715021898 | ||
|
|
e8fa0036e2 | ||
|
|
f628a5e7ef | ||
|
|
95e6bc120e | ||
|
|
0e35c891c4 | ||
|
|
b7a3e7014c | ||
|
|
0c1c587bc7 | ||
|
|
a0a2c60e63 | ||
|
|
45c912a0d6 | ||
|
|
39945f161d | ||
|
|
ed958302bd | ||
|
|
477f2141d7 | ||
|
|
d321a958f0 | ||
|
|
d14f1df823 | ||
|
|
f7e3b9f6c7 | ||
|
|
cd3ac201f8 | ||
|
|
ceef493b01 | ||
|
|
480a75e30c | ||
|
|
d8cc953fe7 | ||
|
|
0baec88321 | ||
|
|
74bd3f681f | ||
|
|
713b750f4a | ||
|
|
11eee911ca | ||
|
|
b288aaea90 | ||
|
|
7acc018fd2 | ||
|
|
7aba1096f9 | ||
|
|
0be7202144 | ||
|
|
83a5db74e5 | ||
|
|
8dc4371beb | ||
|
|
e5045a0984 | ||
|
|
2c9b37a0cc | ||
|
|
b608482220 | ||
|
|
9290fac899 | ||
|
|
d7901ee575 | ||
|
|
7eb33a5aef | ||
|
|
c5dc103ac0 | ||
|
|
c90172a862 | ||
|
|
8540786c2c | ||
|
|
7aeb8e851d | ||
|
|
2f207dc3d9 | ||
|
|
7e2be73962 | ||
|
|
e2a87eb945 | ||
|
|
4b79c6304f | ||
|
|
22f63e1056 | ||
|
|
bf1613ba66 | ||
|
|
38e04db0cb | ||
|
|
08cc3034a2 | ||
|
|
a0eb176b6e | ||
|
|
a43774b33f | ||
|
|
8369b4d205 | ||
|
|
87e51cf9f9 | ||
|
|
8a8fb79fde | ||
|
|
1dc4cf36b1 | ||
|
|
75cf43e02a | ||
|
|
dd8357453b | ||
|
|
e9b273a62c | ||
|
|
ebebd0fda6 | ||
|
|
3e06902b07 | ||
|
|
a874b42c41 | ||
|
|
f6215d3563 | ||
|
|
4fad58f7fe | ||
|
|
276e4c3d09 | ||
|
|
8aef4d605b | ||
|
|
962c891baa | ||
|
|
eb596ea901 | ||
|
|
e708ec9adc | ||
|
|
c631a083ea | ||
|
|
169ea8c233 | ||
|
|
241e2258e8 | ||
|
|
5d2d6bc5fb | ||
|
|
c7ef22f5c2 | ||
|
|
3ec89decf0 | ||
|
|
2693f47ed5 | ||
|
|
496a13e0e0 | ||
|
|
71ef416dbb | ||
|
|
5bc80c8677 | ||
|
|
8c300f467c | ||
|
|
78b3bf1475 | ||
|
|
ce74f910a7 | ||
|
|
ece531249e | ||
|
|
5052e2510e | ||
|
|
b8f8f80499 | ||
|
|
a13d98e32b | ||
|
|
94996dcec8 | ||
|
|
4b8ce30df8 | ||
|
|
485a37e9b5 | ||
|
|
b41adb12f7 | ||
|
|
a9cd443bdd | ||
|
|
92b9df718f | ||
|
|
a6a4f91d83 | ||
|
|
0fb2c498f4 | ||
|
|
d87e70f7ee | ||
|
|
bee8bae0ae | ||
|
|
d5ef277121 | ||
|
|
dfc8e6c108 | ||
|
|
9100aee91f | ||
|
|
c94cc92d1c | ||
|
|
a41f5e3aca | ||
|
|
cc2d15de0d | ||
|
|
39e08aedae | ||
|
|
fe8730fb1f | ||
|
|
aac3ac8947 | ||
|
|
1acc908bb8 | ||
|
|
47ed98a5e1 | ||
|
|
103034d1b8 | ||
|
|
608714d00a | ||
|
|
575c287860 | ||
|
|
820470bef7 | ||
|
|
ea000ef56c | ||
|
|
793b8f8495 | ||
|
|
373eee3024 | ||
|
|
194273635e | ||
|
|
0972d7d89d | ||
|
|
d1415f9d86 | ||
|
|
b3ff3bb406 | ||
|
|
1efce62a8a | ||
|
|
442f2ff927 | ||
|
|
84cefa1be8 | ||
|
|
45e9c1eccb | ||
|
|
e7e2c9604e | ||
|
|
edaf6875ef | ||
|
|
e7976a0a88 | ||
|
|
e7daae9250 | ||
|
|
91a188f3fd | ||
|
|
49a6d48101 | ||
|
|
a4b5ed3597 | ||
|
|
2efbfeef8c | ||
|
|
1a9093bbdf | ||
|
|
9b3d3c6bdc | ||
|
|
620a6f4795 | ||
|
|
1615fa9e51 | ||
|
|
205906ebb7 | ||
|
|
0d8016651c | ||
|
|
ffdc776413 | ||
|
|
9b92b3d1ae | ||
|
|
3bfea2fd0e | ||
|
|
97636ca575 | ||
|
|
8c4c2095d7 | ||
|
|
9855656e9a | ||
|
|
cffdf92daf | ||
|
|
d07ca4076e | ||
|
|
47740df9a9 | ||
|
|
e830d3fef9 | ||
|
|
3c0a87354e | ||
|
|
b5a2eb9fb9 | ||
|
|
244bd02771 | ||
|
|
95a978495d | ||
|
|
292ad8bb61 | ||
|
|
8b0a2263e7 | ||
|
|
bb9ffe3492 | ||
|
|
ea7ef074e1 | ||
|
|
897d6727b4 | ||
|
|
0f99728be3 | ||
|
|
51903d04f5 | ||
|
|
dde7dbf3a0 | ||
|
|
508998efa8 | ||
|
|
2691247532 | ||
|
|
f3c9d8211f | ||
|
|
ec970d9f08 | ||
|
|
04b86ae7e4 | ||
|
|
b7b31ca5c2 | ||
|
|
7c76e3b3a9 | ||
|
|
863f8d8630 | ||
|
|
b12d16f7a7 | ||
|
|
ead8474783 | ||
|
|
03a1ead721 | ||
|
|
234dbb60fe | ||
|
|
43503df53c | ||
|
|
6c381aecb7 | ||
|
|
8d1e366e10 | ||
|
|
d6a0713f52 | ||
|
|
113bde4bab | ||
|
|
b5354a2431 | ||
|
|
c166dd9ed8 | ||
|
|
80b73da72c | ||
|
|
98490e6444 | ||
|
|
56649471d0 | ||
|
|
0186bdc15a | ||
|
|
eb02014371 | ||
|
|
993292cec8 | ||
|
|
aac99eaf07 | ||
|
|
e5d82df65c | ||
|
|
10bbbecf7b | ||
|
|
8b5f7c4bcb | ||
|
|
10c568fcac | ||
|
|
7443a2fbc4 | ||
|
|
3e5681abc5 | ||
|
|
e5036c0b45 | ||
|
|
a312a81141 | ||
|
|
40dd55d828 | ||
|
|
135bb622e1 | ||
|
|
ed8911be5f | ||
|
|
a126d0610f | ||
|
|
946301ba86 | ||
|
|
604e8ec366 | ||
|
|
2593dd4e86 | ||
|
|
9ee9cdd969 | ||
|
|
ad3918f071 | ||
|
|
3cbc1efa59 | ||
|
|
446f0336b6 | ||
|
|
e3c3becb63 | ||
|
|
8564c85378 | ||
|
|
eedca4c1d0 | ||
|
|
28c46dcbfd | ||
|
|
544c4c3e80 | ||
|
|
8bfd1a4d11 | ||
|
|
3fae6241a7 | ||
|
|
48ff4209c4 | ||
|
|
1d3986c589 | ||
|
|
52b2ec776b | ||
|
|
9d3c6b28ef | ||
|
|
c242eb3750 | ||
|
|
e718fea565 | ||
|
|
14644da9c1 | ||
|
|
0c99dc52d3 | ||
|
|
9cf94d2d1d | ||
|
|
de560f9b8a | ||
|
|
47b8b7a9b8 | ||
|
|
216c468118 | ||
|
|
dd6735167b | ||
|
|
a400eabc0e | ||
|
|
3b95f1b873 | ||
|
|
7b1441814d | ||
|
|
cc39a3a173 | ||
|
|
34df5cfc6a | ||
|
|
37836e7842 | ||
|
|
121231f6fa | ||
|
|
71c8c98ce9 | ||
|
|
2c3cce4d51 | ||
|
|
34ed1247e3 | ||
|
|
e8351a1136 | ||
|
|
4f93e3f5e2 | ||
|
|
33017aa948 | ||
|
|
cbfe68230c | ||
|
|
775c998e4d | ||
|
|
46b8fc7dcd | ||
|
|
d671eb14ce | ||
|
|
cd7d4969ef | ||
|
|
a4e7509354 | ||
|
|
cd74573adc | ||
|
|
86f14ad88a | ||
|
|
847634e524 | ||
|
|
f162019f77 | ||
|
|
b814632f74 | ||
|
|
741ced6289 | ||
|
|
d939a69d16 | ||
|
|
08416cb4e1 | ||
|
|
a30a607f4f | ||
|
|
5caae28b55 | ||
|
|
c85b9d9ee1 | ||
|
|
3d8c8520de | ||
|
|
9e8aec16e5 | ||
|
|
91ea47a6f4 | ||
|
|
c0d108ee77 | ||
|
|
94479f47d9 | ||
|
|
f6fd017871 | ||
|
|
15de706fba | ||
|
|
f7745ac556 | ||
|
|
ad32ed5a80 | ||
|
|
18e1522374 | ||
|
|
bd01da15cc | ||
|
|
1b4ca71e38 | ||
|
|
68fabef2be | ||
|
|
c07f756d67 | ||
|
|
bf52369d2d | ||
|
|
e49e63f39a | ||
|
|
f651f2e577 | ||
|
|
f424da410b | ||
|
|
ada809066c | ||
|
|
e7ec8518e7 | ||
|
|
1ea6a4d987 | ||
|
|
f111aad6b7 | ||
|
|
88282db0e4 | ||
|
|
55e09938bb | ||
|
|
add6c73af7 | ||
|
|
62a932b9ed | ||
|
|
037024395d | ||
|
|
e2105e1fa3 | ||
|
|
be67b33f99 | ||
|
|
7224da9c7a | ||
|
|
fd68007c41 | ||
|
|
e77c7935de | ||
|
|
cf0072fce4 | ||
|
|
6f8a8b4264 | ||
|
|
746db62fdb | ||
|
|
fa4e254ba4 | ||
|
|
0a2e385f41 | ||
|
|
2d6e939306 | ||
|
|
b5d7107760 | ||
|
|
575dd08b74 | ||
|
|
b043541dca | ||
|
|
fa5fd85b5d | ||
|
|
be68cece47 | ||
|
|
50245e618b | ||
|
|
e0d85a65f9 | ||
|
|
a414a994fb | ||
|
|
c0e7f4c0fa | ||
|
|
c03375c7f0 | ||
|
|
453cfbb306 | ||
|
|
deb5ee8374 | ||
|
|
5d2f7dbe8a | ||
|
|
3831998add | ||
|
|
e85982e17c | ||
|
|
46c17c8470 | ||
|
|
5fd326f2e8 | ||
|
|
ad5e086eb5 | ||
|
|
ef9231f51f | ||
|
|
bf34661e42 | ||
|
|
286db4fbea | ||
|
|
4f4dbfbc6f | ||
|
|
1046daaf16 | ||
|
|
b7df9d05a7 | ||
|
|
394c3ef4f6 | ||
|
|
7a071c33cd | ||
|
|
71733de05f | ||
|
|
d5e48a3e36 | ||
|
|
e36755a21d | ||
|
|
9620164884 | ||
|
|
711844cc83 | ||
|
|
6eb82b26a7 | ||
|
|
b12cf6219a | ||
|
|
7e46305277 | ||
|
|
14aa127f31 | ||
|
|
6e7e89c2d0 | ||
|
|
819c37bdec | ||
|
|
c80690d66e | ||
|
|
084f879537 | ||
|
|
7afd6bbbe9 | ||
|
|
46641e8a86 | ||
|
|
010b386ce1 | ||
|
|
a73b320715 | ||
|
|
5f822a6230 | ||
|
|
024fac90cd | ||
|
|
133bc36cca | ||
|
|
51a7ff2820 | ||
|
|
bc96f8df49 | ||
|
|
979f197476 | ||
|
|
1677b69363 | ||
|
|
d627f694df | ||
|
|
4c52cba21f | ||
|
|
597fdd1695 | ||
|
|
9031704b9b | ||
|
|
510a5c5007 | ||
|
|
469e76c50b | ||
|
|
f6c6c58c17 | ||
|
|
e546866f78 | ||
|
|
081566b5d9 | ||
|
|
ec078666ae | ||
|
|
1ce394c7a1 | ||
|
|
2fc3930b43 | ||
|
|
9fa9da5e74 | ||
|
|
d6ad0b5e94 | ||
|
|
15dc50804f | ||
|
|
63c3e35064 | ||
|
|
297930c4d1 | ||
|
|
098c344047 | ||
|
|
4f74677d14 | ||
|
|
0869b19408 | ||
|
|
9b02611700 | ||
|
|
5daa71e6c1 | ||
|
|
aa5f2f66f8 | ||
|
|
92d04f81c3 | ||
|
|
5d53db4211 | ||
|
|
eb3bbdd3c5 | ||
|
|
1ac816ca37 | ||
|
|
33cf18cde5 | ||
|
|
0c0d53dd78 | ||
|
|
1959ee7560 | ||
|
|
ee13401b5a | ||
|
|
78b1cf4af5 | ||
|
|
0599260e31 | ||
|
|
08dd921088 | ||
|
|
e66e938d7d | ||
|
|
b5b7d2a9d2 | ||
|
|
46298d555b | ||
|
|
8fb34b5fde | ||
|
|
28f8986837 | ||
|
|
e968173f61 | ||
|
|
f703a8868b | ||
|
|
0a29e5f156 | ||
|
|
c5c28ee042 | ||
|
|
44ca43399d | ||
|
|
13a47d822d | ||
|
|
5621cd90bb | ||
|
|
9f7055ffd2 | ||
|
|
37a9fc2eb0 | ||
|
|
923325874c | ||
|
|
7ca0c4d744 | ||
|
|
a584a71429 | ||
|
|
70f47635b4 | ||
|
|
8e35d77e07 | ||
|
|
7024f2d472 | ||
|
|
7e2dd4c7ff | ||
|
|
f20d83ca8c | ||
|
|
f31f920137 | ||
|
|
0f428438a3 | ||
|
|
b7882b0322 | ||
|
|
3a417a9281 | ||
|
|
748085c2be | ||
|
|
6a3f56f95f | ||
|
|
f6d6fb7185 | ||
|
|
7865263c1c | ||
|
|
2cf51d5a09 | ||
|
|
f993be950f | ||
|
|
52c4c49bab | ||
|
|
60fdc7b7ea | ||
|
|
a330cff3e5 | ||
|
|
9ffbac7420 | ||
|
|
7909707894 | ||
|
|
8d8014b157 | ||
|
|
a459b7a393 | ||
|
|
7d405dc0c2 | ||
|
|
3decf8749b | ||
|
|
ba6869ec76 | ||
|
|
33cb70ea17 | ||
|
|
69f1b7471f | ||
|
|
76a7d1dba7 | ||
|
|
9f0d3c9d3b | ||
|
|
ff6292160f | ||
|
|
52dcb7972f | ||
|
|
10e2b0ee59 | ||
|
|
473eccbdea | ||
|
|
f6b2e0a506 | ||
|
|
22e16db1f2 | ||
|
|
2c71ca9b8a | ||
|
|
023189bca9 | ||
|
|
8447985b98 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,15 +1,20 @@
|
||||
# config
|
||||
config.ini
|
||||
|
||||
# Pickle files, specifically, bbsdb.pkl
|
||||
bbsdb.pkl
|
||||
bbsdm.pkl
|
||||
# Pickle files
|
||||
*.pkl
|
||||
|
||||
# virtualenv
|
||||
venv/
|
||||
|
||||
# logs
|
||||
logs/*.log
|
||||
|
||||
# modified .service files
|
||||
etc/*.service
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
|
||||
# rag data
|
||||
data/rag/*
|
||||
|
||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM python:3.10-slim
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
RUN apt-get update && apt-get install -y gettext tzdata locales && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the locale default to en_US.UTF-8
|
||||
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
|
||||
dpkg-reconfigure --frontend=noninteractive locales && \
|
||||
update-locale LANG=en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV TZ="America/Los_Angeles"
|
||||
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
COPY . .
|
||||
|
||||
COPY config.ini /app/config.ini
|
||||
COPY entrypoint.sh /app/entrypoint.sh
|
||||
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
415
README.md
415
README.md
@@ -1,70 +1,113 @@
|
||||
# meshing-around
|
||||
Random Mesh Scripts for Network Testing and BBS Activities for Use with [Meshtastic](https://meshtastic.org/docs/introduction/) Nodes
|
||||
# Mesh Bot for Network Testing and BBS Activities
|
||||
|
||||

|
||||
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, this bot has you covered.
|
||||
|
||||
## mesh_bot.sh
|
||||
The feature-rich bot requires the internet for full functionality. These responder bots will trap keywords like ping and respond to a DM (direct message) with pong! The script will also monitor the group channels for keywords to trap. You can also `Ping @Data to Echo` as an example.
|
||||

|
||||
|
||||
Along with network testing, this bot has a lot of other fun features, like simple mail messaging you can leave for another device, and when that device is seen, it can send the mail as a DM.
|
||||
## Key Features
|
||||
|
||||
The bot is also capable of using dual radio/nodes, so you can monitor two networks at the same time and send messages to nodes using the same `bbspost @nodeNumber #message` or `bbspost @nodeShportName #message` function. There is a small message board to fit in the constraints of Meshtastic for posting bulletin messages with `bbspost $subject #message`.
|
||||
### Intelligent Keyword Responder
|
||||
- **Automated Responses**: The bot traps keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
|
||||
- **Customizable Triggers**: Monitor group channels for specific keywords and set custom responses.
|
||||
|
||||
The bot will report on anyone who is getting close to the configured lat/long, if in a remote location.
|
||||
### Network Tools
|
||||
- **Enhance and build local mesh**: Ping allow for message delivery testing with more realistic packets vs. telemetry
|
||||
- **Test node hardware**: `test` will send incremental data into the radio buffer for overall length of message testing
|
||||
|
||||
Store and forward-like message re-play with `messages`, and there is a repeater module for dual radio bots to cross post messages. Messages are also logged locally to disk.
|
||||
### Dual Radio/Node Support
|
||||
- **Simultaneous Monitoring**: Monitor two networks at the same time.
|
||||
- **Flexible Messaging**: send mail and messages, between networks.
|
||||
|
||||
The bot can also be used to monitor a radio frequency and let you know when high SNR RF activity is seen. Using Hamlib(rigctld) to watch the S meter on a connected radio. You can send alerts to channels when a frequency is detected for 20 seconds within the thresholds set in config.ini
|
||||
### Advanced Messaging Capabilities
|
||||
- **Mail Messaging**: Leave messages for other devices, which are sent as DMs when the device is seen.
|
||||
- **Scheduler**: Schedule messages like weather updates or reminders for weekly VHF nets.
|
||||
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
|
||||
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
|
||||
- **BBS Linking**: Combine multiple bots to expand BBS reach
|
||||
|
||||
Any messages that are over 160 characters are chunked into 160 message bytes to help traverse hops, in testing, this keeps delivery success higher.
|
||||
### Interactive AI and Data Lookup
|
||||
- **NOAA location Data**: Get localized weather(alerts) and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
|
||||
- **Wiki Integration**: Look up data using Wikipedia results.
|
||||
- **Ollama LLM AI**: Interact with the [Ollama](https://github.com/ollama/ollama/tree/main/docs) LLM AI for advanced queries and responses.
|
||||
|
||||
Full list of commands for the bot.
|
||||
### Proximity Alerts
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
|
||||
|
||||
- Various solar details for radio propagation (spaceWeather module)
|
||||
- `sun` and `moon` return info on rise and set local time
|
||||
- `solar` gives an idea of the x-ray flux
|
||||
- `hfcond` returns a table of HF solar conditions
|
||||
- Bulletin Board (BBS) functions
|
||||
- `bbshelp` returns the following
|
||||
- `bbslist` list the messages by ID and subject
|
||||
- `bbsread` read a message example use: `bbsread #1`
|
||||
- `bbspost` post a message to public board or send a DM example use: `bbspost $subject #message, or bbspost @nodeNumber #message or bbspost @nodeShportName #message`
|
||||
- `bbsdelete` delete a message example use: `bbsdelete #4`
|
||||
- Other functions
|
||||
- `whereami` returns the address of location of sender if known
|
||||
- `tide` returns the local tides, NOAA data source
|
||||
- `wx` and `wxc` returns local weather forecast, (wxc is metric value), NOAA or Open Meteo for weather forcasting.
|
||||
- `wxa` and `wxalert` return NOAA alerts. Short title or expanded details
|
||||
- `joke` tells a joke
|
||||
- `messages` Replay the last messages heard, like Store and Forward
|
||||
- `motd` or to set the message `motd $New Message Of the day`
|
||||
- `lheard` returns the last 5 heard nodes with SNR, can also use `sitrep`
|
||||
- `cmd` returns the list of commands (the help message)
|
||||
### Fun and Games
|
||||
- **Built-in Games**: Enjoy games like DopeWars, Lemonade Stand, BlackJack, and VideoPoker.
|
||||
- **Command-Based Gameplay**: Issue `games` to display help and start playing.
|
||||
|
||||
## pong_bot.sh
|
||||
Stripped-down bot, mostly around for archive purposes. The mesh-bot enhanced modules can be disabled by config to disable features.
|
||||
### Radio Frequency Monitoring
|
||||
- **SNR RF Activity Alerts**: Monitor a radio frequency and get alerts when high SNR RF activity is detected.
|
||||
- **Hamlib Integration**: Use Hamlib (rigctld) to watch the S meter on a connected radio.
|
||||
|
||||
## Hardware
|
||||
The project is written on Linux on a Pi and should work anywhere [Meshtastic](https://meshtastic.org/docs/software/python/cli/) Python modules will function, with any supported [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. While BLE and TCP will work, they are not as reliable as serial connections.
|
||||
### NOAA EAS Alerts
|
||||
- **EAS Alerts via NOAA API**: Use an internet connected node to message Emergency Alerts from NOAA
|
||||
- **EAS Alerts over the air**: Utalizing external tools to report EAS alerts offline over mesh
|
||||
|
||||
## Install
|
||||
Clone the project with `git clone https://github.com/spudgunman/meshing-around`
|
||||
code is under a lot of development, so check back often with `git pull`
|
||||
Copy [config.template](config.template) to `config.ini` and edit for your needs.
|
||||
### File Monitor Alerts
|
||||
- **File Mon**: Monitor a flat/text file for changes, brodcast the contents of the message to mesh channel.
|
||||
|
||||
Optionally:
|
||||
- `install.sh` will automate optional venv and requirements installation.
|
||||
- `launch.sh` will activate and launch the app in the venv if built.
|
||||
### Data Reporting
|
||||
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
|
||||
|
||||
### Configurations
|
||||
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect tcp if possible.
|
||||
### Robust Message Handling
|
||||
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across hops.
|
||||
|
||||
## Getting Started
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware.
|
||||
|
||||
### Installation
|
||||
|
||||
#### Clone the Repository
|
||||
```sh
|
||||
git clone https://github.com/spudgunman/meshing-around
|
||||
```
|
||||
#config.ini
|
||||
The code is under active development, so make sure to pull the latest changes regularly!
|
||||
|
||||
#### Optional Automation of setup
|
||||
- **Automated Installation**: `install.sh` will automate optional venv and requirements installation.
|
||||
- **Launch Script**: `launch.sh` will activate and launch the app in the venv
|
||||
|
||||
#### Docker Installation
|
||||
If you prefer to use Docker, follow these steps:
|
||||
|
||||
1. Ensure your serial port is properly shared and the GPU is configured if using LLM with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html).
|
||||
2. Build the Docker image:
|
||||
```sh
|
||||
cd meshing-around
|
||||
docker build -t meshing-around .
|
||||
```
|
||||
3. Run the Docker container:
|
||||
```sh
|
||||
docker run --rm -it --device=/dev/ttyUSB0 meshing-around
|
||||
```
|
||||
|
||||
#### Custom Install
|
||||
Install the required dependencies using pip:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Copy the configuration template to `config.ini` and edit it to suit your needs:
|
||||
```sh
|
||||
cp config.template config.ini
|
||||
```
|
||||
|
||||
### Configuration
|
||||
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
|
||||
```sh
|
||||
meshtastic --ble-scan
|
||||
```
|
||||
|
||||
**Note**: The code has been tested with a single BLE device and is written to support only one BLE port.
|
||||
|
||||
```ini
|
||||
# config.ini
|
||||
# type can be serial, tcp, or ble.
|
||||
# port is the serial port to use; commented out will try to auto-detect
|
||||
# hostname is the IP address of the device to connect to for TCP type
|
||||
# mac is the MAC address of the device to connect to for ble type
|
||||
# mac is the MAC address of the device to connect to for BLE type
|
||||
|
||||
[interface]
|
||||
type = serial
|
||||
@@ -72,20 +115,24 @@ type = serial
|
||||
# hostname = 192.168.0.1
|
||||
# mac = 00:11:22:33:44:55
|
||||
|
||||
# Additional interface for dual radio support See config.template for more.
|
||||
# Additional interface for dual radio support. See config.template for more.
|
||||
[interface2]
|
||||
enabled = False
|
||||
```
|
||||
The following pair of settings determine how to respond: The default action is to not spam the default channel. Setting'respond_by_DM_only'` will force all messages to be sent to DM, which may not be wanted. Setting the value to False will allow responses in the channel for all to see.
|
||||
|
||||
Setting the default channel is the channel that won't be spammed by the bot. It's the public default channel 0 on the new Meshtastic firmware. Anti-Spam is hard-coded into the responder to prevent abuse of the public channel.
|
||||
```
|
||||
### General Settings
|
||||
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index.
|
||||
|
||||
```ini
|
||||
[general]
|
||||
respond_by_dm_only = True
|
||||
defaultChannel = 0
|
||||
```
|
||||
The weather forcasting defaults to NOAA but for outside the USA you can set UseMeteoWxAPI `True` to use a world weather API. The lat and lon are for defaults when a node has no location data to use.
|
||||
```
|
||||
|
||||
### Location Settings
|
||||
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data. It is also the default used for Sentry.
|
||||
|
||||
```ini
|
||||
[location]
|
||||
enabled = True
|
||||
lat = 48.50
|
||||
@@ -93,8 +140,10 @@ lon = -123.0
|
||||
UseMeteoWxAPI = True
|
||||
```
|
||||
|
||||
Modules can be disabled or enabled.
|
||||
```
|
||||
### Module Settings
|
||||
Modules can be enabled or disabled as needed.
|
||||
|
||||
```ini
|
||||
[bbs]
|
||||
enabled = False
|
||||
|
||||
@@ -102,69 +151,136 @@ enabled = False
|
||||
DadJokes = False
|
||||
StoreForward = False
|
||||
```
|
||||
Sentry Bot detects anyone comeing close to the bot-node
|
||||
```
|
||||
# detect anyone close to the bot
|
||||
SentryEnabled = True
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryChannel = 9
|
||||
# channel to send a message to when the watchdog is triggered
|
||||
SentryHoldoff = 2
|
||||
# list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
sentryIgnoreList =
|
||||
```
|
||||
The BBS has admin and block lists; see the [config.template](config.template)
|
||||
|
||||
A repeater function for two different nodes and cross-posting messages. The'repeater_channels` is a list of repeater channel(s) that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. With great power comes great responsibility; danger could lurk in the use of this feature! If you have the two nodes in the same radio configuration, you could create a feedback loop!!!
|
||||
### History
|
||||
The history command shows the last commands the user ran, and [`lheard`] reflects the last users on the bot.
|
||||
|
||||
```ini
|
||||
enableCmdHistory = True # history command enabler
|
||||
lheardCmdIgnoreNodes = # command history ignore list ex: 2813308004,4258675309
|
||||
```
|
||||
# repeater module
|
||||
[repeater]
|
||||
|
||||
### Sentry Settings
|
||||
|
||||
Sentry Bot detects anyone coming close to the bot-node.
|
||||
|
||||
```ini
|
||||
SentryEnabled = True # detect anyone close to the bot
|
||||
SentryRadius = 100 # radius in meters to detect someone close to the bot
|
||||
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
|
||||
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
```
|
||||
|
||||
### Repeater Settings
|
||||
A repeater function for two different nodes and cross-posting messages. The [`repeater_channels`] is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
|
||||
|
||||
```ini
|
||||
[repeater] # repeater module
|
||||
enabled = True
|
||||
repeater_channels = [2, 3]
|
||||
```
|
||||
A module allowing a Hamlib compatible radio to connect to the bot, when functioning it will message the channel configured with a message of in use. **Requires hamlib/rigctld to be running as a service.**
|
||||
|
||||
```
|
||||
### Radio Monitoring
|
||||
A module allowing a Hamlib compatible radio to connect to the bot. When functioning, it will message the configured channel with a message of in use. **Requires hamlib/rigctld to be running as a service.**
|
||||
|
||||
```ini
|
||||
[radioMon]
|
||||
enabled = False
|
||||
rigControlServerAddress = localhost:4532
|
||||
# channel to brodcast to can be 2,3
|
||||
sigWatchBrodcastCh = 2
|
||||
# minimum SNR as reported by radio via hamlib
|
||||
signalDetectionThreshold = -10
|
||||
# hold time for high SNR
|
||||
signalHoldTime = 10
|
||||
# the following are combined to reset the monitor
|
||||
signalCooldown = 5
|
||||
sigWatchBroadcastCh = 2 # channel to broadcast to can be 2,3
|
||||
signalDetectionThreshold = -10 # minimum SNR as reported by radio via hamlib
|
||||
signalHoldTime = 10 # hold time for high SNR
|
||||
signalCooldown = 5 # the following are combined to reset the monitor
|
||||
signalCycleLimit = 5
|
||||
```
|
||||
|
||||
Logging messages to disk or Syslog to disk uses the python native logging fuction. Take a look at the [/modules/log.py](/modules/log.py) you can set the file logger for syslog to INFO for example to not log DEBUG messages to file log, or modify the stdOut level.
|
||||
```
|
||||
[general]
|
||||
# logging to file of the non Bot messages
|
||||
LogMessagesToFile = True
|
||||
# Logging of system messages to file
|
||||
SyslogToFile = True
|
||||
*log.py
|
||||
file_handler.setLevel(logging.INFO) # DEBUG used by default for system logs to disk example here shows INFO
|
||||
### Ollama (LLM/AI) Settings
|
||||
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma2:2b`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server)
|
||||
|
||||
```ini
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
ollama = True # Ollama model to use (defaults to gemma2:2b)
|
||||
ollamaModel = gemma2 #ollamaModel = llama3.1
|
||||
ollamaHostName = http://localhost:11434 # server instance to use (defaults to local machine install)
|
||||
```
|
||||
|
||||
# requirements
|
||||
Python 3.4 and likely higher is needed, developed on latest release.
|
||||
|
||||
The following can also be installed with `pip install -r requirements.txt` or using the install.sh script for venv and automation
|
||||
Also see `llm.py` for changing the defaults of:
|
||||
|
||||
```ini
|
||||
# LLM System Variables
|
||||
llmEnableHistory = True # enable history for the LLM model to use in responses adds to compute time
|
||||
llmContext_fromGoogle = True # enable context from google search results helps with responses accuracy
|
||||
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
|
||||
```
|
||||
|
||||
### File Monitoring
|
||||
Some dev notes for ideas of use
|
||||
```ini
|
||||
[fileMon]
|
||||
enabled = True
|
||||
file_path = alert.txt
|
||||
broadcastCh = 2,4
|
||||
```
|
||||
#### NOAA EAS
|
||||
To Alert on Mesh with the NOAA EAS API you can set the channels and enable, checks every 30min
|
||||
|
||||
```ini
|
||||
# EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2,4
|
||||
```
|
||||
|
||||
To Monitor EAS with no internet connection see the following notes
|
||||
|
||||
- [EAS2Text](https://github.com/A-c0rN/EAS2Text)
|
||||
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng) or [direwolf](https://github.com/wb2osz/direwolf)
|
||||
- [dsame3](https://github.com/jamieden/dsame3) // recomend not using anything but the sample file for basic work
|
||||
- this can be used with a rtl-sdr to capture alerts
|
||||
- has a sample .ogg file for testing alerts
|
||||
|
||||
The following example shell command can pipe the data using [etc/eas_alert_parser.py](etc/eas_alert_parser.py) to alert.txt
|
||||
```bash
|
||||
sox -t ogg WXR-RWT.ogg -esigned-integer -b16 -r 22050 -t raw - | multimon-ng -a EAS -v 1 -t raw - | python eas_alert_parser.py
|
||||
```
|
||||
The following example shell command will pipe rtl_sdr to alert.txt
|
||||
```bash
|
||||
rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas_alert_parser.py
|
||||
```
|
||||
|
||||
### Scheduler
|
||||
The Scheduler is enabled in the `settings.py` by setting `scheduler_enabled = True`. The actions and settings are via code only at this time. See mesh_bot.py around line [1050](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1050) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more.
|
||||
|
||||
```python
|
||||
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
|
||||
#Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
```
|
||||
|
||||
#### BBS Link
|
||||
The scheduler also handles the BBL Link Brodcast message
|
||||
```python
|
||||
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
|
||||
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
|
||||
```
|
||||
|
||||
### MQTT Notes
|
||||
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two methods have been mentioned as allowing MQTT routing for the project.
|
||||
|
||||
### Requirements
|
||||
Python 3.8? or later is needed (dev on latest). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
|
||||
|
||||
```sh
|
||||
pip install meshtastic
|
||||
pip install pubsub
|
||||
```
|
||||
mesh-bot enhancements
|
||||
|
||||
```
|
||||
Mesh-bot enhancements:
|
||||
|
||||
```sh
|
||||
pip install pyephem
|
||||
pip install requests
|
||||
pip install geopy
|
||||
@@ -173,22 +289,111 @@ pip install beautifulsoup4
|
||||
pip install dadjokes
|
||||
pip install geopy
|
||||
pip install schedule
|
||||
pip install wikipedia
|
||||
```
|
||||
The following is needed for open-meteo use
|
||||
```
|
||||
|
||||
For open-meteo use:
|
||||
|
||||
```sh
|
||||
pip install openmeteo_requests
|
||||
pip install retry_requests
|
||||
pip install numpy
|
||||
```
|
||||
|
||||
To enable emoji in the Debian console, install the fonts `sudo apt-get install fonts-noto-color-emoji`
|
||||
For the Ollama LLM:
|
||||
|
||||
```sh
|
||||
pip install ollama
|
||||
pip install googlesearch-python
|
||||
```
|
||||
|
||||
To enable emoji in the Debian console, install the fonts:
|
||||
|
||||
```sh
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
```
|
||||
|
||||
## Full list of commands for the bot
|
||||
|
||||
### Networking
|
||||
| Command | Description | ✅ Works Off-Grid |
|
||||
|---------|-------------|-
|
||||
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15) | ✅ |
|
||||
| `test` | Returns like ping but also can be used to test the limits of data buffers `test 4` sends data to the maxBuffer limit (default 225) | ✅ |
|
||||
| `whereami` | Returns the address of the sender's location if known |
|
||||
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
|
||||
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
|
||||
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
|
||||
| `history` | Returns the last commands run by user(s) | ✅ |
|
||||
| `cmd` | Returns the list of commands (the help message) | ✅ |
|
||||
|
||||
### Radio Propagation & Weather Forcasting
|
||||
| Command | Description | |
|
||||
|---------|-------------|-------------------
|
||||
| `sun` and `moon` | Return info on rise and set local time | ✅ |
|
||||
| `solar` | Gives an idea of the x-ray flux | |
|
||||
| `hfcond` | Returns a table of HF solar conditions | |
|
||||
| `tide` | Returns the local tides (NOAA data source) |
|
||||
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
|
||||
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting |
|
||||
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details |
|
||||
|
||||
### Bulletin Board & Mail
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `bbshelp` | Returns the following help message | ✅ |
|
||||
| `bbslist` | Lists the messages by ID and subject | ✅ |
|
||||
| `bbsread` | Reads a message. Example: `bbsread #1` | ✅ |
|
||||
| `bbspost` | Posts a message to the public board or sends a DM(Mail) Examples: `bbspost $subject #message`, `bbspost @nodeNumber #message`, `bbspost @nodeShortName #message` | ✅ |
|
||||
| `bbsdelete` | Deletes a message. Example: `bbsdelete #4` | ✅ |
|
||||
| `bbsinfo` | Provides stats on BBS delivery and messages (sysop) | ✅ |
|
||||
| `bbllink` | Links Bulletin Messages between BBS Systems | ✅ |
|
||||
|
||||
### Data Lookup
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
|
||||
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
|
||||
| `messages` | Replays the last messages heard, like Store and Forward | ✅ |
|
||||
|
||||
|
||||
|
||||
### Games (via DM)
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `joke` | Tells a joke | ✅ |
|
||||
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
|
||||
| `dopewars` | Plays the classic drug trader game | ✅ |
|
||||
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
|
||||
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
|
||||
| `mastermind` | Plays the classic code-breaking game | ✅ |
|
||||
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
|
||||
|
||||
# Recognition
|
||||
|
||||
I used ideas and snippets from other responder bots and want to call them out!
|
||||
- https://github.com/Murturtle/MeshLink
|
||||
- https://github.com/pdxlocations/meshtastic-Python-Examples
|
||||
- https://github.com/geoffwhittington/meshtastic-matrix-relay
|
||||
|
||||
GitHub user PiDiBi looking at test functions and other suggestions like wxc, CPU use, and alerting ideas
|
||||
Discord and Mesh user Cisien, and github Hailo1999, for testing and ideas!
|
||||
### Inspiration and Code Snippets
|
||||
- [MeshLink](https://github.com/Murturtle/MeshLink)
|
||||
- [Meshtastic Python Examples](https://github.com/pdxlocations/meshtastic-Python-Examples)
|
||||
- [Meshtastic Matrix Relay](https://github.com/geoffwhittington/meshtastic-matrix-relay)
|
||||
|
||||
### Games Ported From
|
||||
- [Lemonade Stand](https://github.com/tigerpointe/Lemonade-Stand/)
|
||||
- [Drug Wars](https://github.com/Reconfirefly/drugwars)
|
||||
- [BlackJack](https://github.com/Himan10/BlackJack)
|
||||
- [Video Poker Terminal Game](https://github.com/devtronvarma/Video-Poker-Terminal-Game)
|
||||
- [Python Mastermind](https://github.com/pwdkramer/pythonMastermind/)
|
||||
- [Golf](https://github.com/danfriedman30/pythongame)
|
||||
|
||||
### Special Thanks
|
||||
- **xdep**: For the reporting tools.
|
||||
- **Nestpebble**: For new ideas and enhancements.
|
||||
- **mrpatrick1991**: For Docker configurations.
|
||||
- **Mike O'Connell/skrrt**: For [eas_alert_parser](etc/eas_alert_parser.py) enhanced by **sheer.cold**
|
||||
- **PiDiBi**: For looking at test functions and other suggestions like wxc, CPU use, and alerting ideas.
|
||||
- **Cisien, bitflip, **Woof**, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
|
||||
|
||||
### Tools
|
||||
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)
|
||||
|
||||
@@ -25,18 +25,35 @@ port = /dev/ttyUSB0
|
||||
[general]
|
||||
# if False will respond on all channels but the default channel
|
||||
respond_by_dm_only = True
|
||||
# defaultChannel is the meshtastic default public channel
|
||||
# defaultChannel is the meshtastic default public channel, e.g. LongFast (if none use -1)
|
||||
defaultChannel = 0
|
||||
# ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
ignoreDefaultChannel = False
|
||||
# motd is reset to this value on boot
|
||||
motd = Thanks for using MeshBOT! Have a good day!
|
||||
welcome_message = MeshBot, here for you like a friend who is not. Try sending: ping @foo or, cmd
|
||||
# whoami
|
||||
whoami = True
|
||||
# enable or disable the Joke module
|
||||
DadJokes = True
|
||||
DadJokesEmoji = False
|
||||
# enable or disable the Solar module
|
||||
spaceWeather = True
|
||||
# enable or disable the wikipedia search module
|
||||
wikipedia = True
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
ollama = False
|
||||
# Ollama model to use (defaults to gemma2:2b)
|
||||
# ollamaModel = llama3.1
|
||||
# server instance to use (defaults to local machine install)
|
||||
ollamaHostName = http://localhost:11434
|
||||
# StoreForward Enabled and Limits
|
||||
StoreForward = True
|
||||
StoreLimit = 3
|
||||
# history command
|
||||
enableCmdHistory = True
|
||||
# command history ignore list ex: 2813308004,4258675309
|
||||
lheardCmdIgnoreNodes =
|
||||
# 24 hour clock
|
||||
zuluTime = False
|
||||
# wait time for URL requests
|
||||
@@ -44,8 +61,20 @@ urlTimeout = 10
|
||||
# logging to file of the non Bot messages
|
||||
LogMessagesToFile = False
|
||||
# Logging of system messages to file
|
||||
SyslogToFile = False
|
||||
SyslogToFile = True
|
||||
# Number of log files to keep in days, 0 to keep all
|
||||
log_backup_count = 32
|
||||
|
||||
[games]
|
||||
# if hop limit for the user exceeds this value, the message will be dropped
|
||||
game_hop_limit = 5
|
||||
# enable or disable the games module(s)
|
||||
dopeWars = True
|
||||
lemonade = True
|
||||
blackjack = True
|
||||
videopoker = True
|
||||
mastermind = True
|
||||
golfsim = True
|
||||
|
||||
[sentry]
|
||||
# detect anyone close to the bot
|
||||
@@ -75,10 +104,16 @@ lon = -123.0
|
||||
NOAAforecastDuration = 4
|
||||
# number of weather alerts to display
|
||||
NOAAalertCount = 2
|
||||
# use Open-Meteo API for weather data not NOAA usefull for non US locations
|
||||
# use Open-Meteo API for weather data not NOAA useful for non US locations
|
||||
UseMeteoWxAPI = False
|
||||
# Default to metric units rather than imperial
|
||||
useMetric = False
|
||||
# repeaterList lookup location (rbook / artsci)
|
||||
repeaterLookup = rbook
|
||||
# EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2
|
||||
|
||||
# repeater module
|
||||
[repeater]
|
||||
@@ -93,8 +128,8 @@ repeater_channels =
|
||||
# using Hamlib rig control will monitor and alert on channel use
|
||||
enabled = False
|
||||
rigControlServerAddress = localhost:4532
|
||||
# brodcast to all nodes on the channel can alsp be = 2,3
|
||||
sigWatchBrodcastCh = 2
|
||||
# broadcast to all nodes on the channel can also be = 2,3
|
||||
sigWatchBroadcastCh = 2
|
||||
# minimum SNR as reported by radio via hamlib
|
||||
signalDetectionThreshold = -10
|
||||
# hold time for high SNR
|
||||
@@ -102,3 +137,22 @@ signalHoldTime = 10
|
||||
# the following are combined to reset the monitor
|
||||
signalCooldown = 5
|
||||
signalCycleLimit = 5
|
||||
|
||||
[fileMon]
|
||||
enabled = False
|
||||
file_path = alert.txt
|
||||
broadcastCh = 2
|
||||
|
||||
[messagingSettings]
|
||||
# delay in seconds for response to avoid message collision
|
||||
responseDelay = 0.7
|
||||
# delay in seconds for splits in messages to avoid message collision
|
||||
splitDelay = 0.0
|
||||
# message chunk size for sending at high success rate
|
||||
MESSAGE_CHUNK_SIZE = 160
|
||||
# Request Acknowledgement of message OTA
|
||||
wantAck = False
|
||||
# Max lilmit Buffer for radio testing
|
||||
maxBuffer = 220
|
||||
|
||||
|
||||
|
||||
1
data/README.md
Normal file
1
data/README.md
Normal file
@@ -0,0 +1 @@
|
||||
database admin tool is in [./etc/db_admin.py](../etc/db_admin.py)
|
||||
6
entrypoint.sh
Normal file
6
entrypoint.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Substitute environment variables in the config file
|
||||
envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini
|
||||
|
||||
exec python /app/mesh_bot.py
|
||||
@@ -1,29 +0,0 @@
|
||||
# Load the bbs messages from the database file to screen for admin functions
|
||||
import pickle # pip install pickle
|
||||
|
||||
|
||||
# load the bbs messages from the database file
|
||||
try:
|
||||
with open('../bbsdb.pkl', 'rb') as f:
|
||||
bbs_messages = pickle.load(f)
|
||||
except:
|
||||
try:
|
||||
with open('bbsdb.pkl', 'rb') as f:
|
||||
bbs_messages = pickle.load(f)
|
||||
except:
|
||||
print ("\nSystem: bbsdb.pkl not found")
|
||||
|
||||
try:
|
||||
with open('../bbsdm.pkl', 'rb') as f:
|
||||
bbs_dm = pickle.load(f)
|
||||
except:
|
||||
try:
|
||||
with open('bbsdm.pkl', 'rb') as f:
|
||||
bbs_dm = pickle.load(f)
|
||||
except:
|
||||
print ("\nSystem: bbsdm.pkl not found")
|
||||
|
||||
print ("\nSystem: bbs_messages")
|
||||
print (bbs_messages)
|
||||
print ("\nSystem: bbs_dm")
|
||||
print (bbs_dm)
|
||||
100
etc/db_admin.py
Normal file
100
etc/db_admin.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# Load the bbs messages from the database file to screen for admin functions
|
||||
import pickle # pip install pickle
|
||||
|
||||
|
||||
# load the bbs messages from the database file
|
||||
try:
|
||||
with open('../data/bbsdb.pkl', 'rb') as f:
|
||||
bbs_messages = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('data/bbsdb.pkl', 'rb') as f:
|
||||
bbs_messages = pickle.load(f)
|
||||
except Exception as e:
|
||||
bbs_messages = "System: data/bbsdb.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../data/bbsdm.pkl', 'rb') as f:
|
||||
bbs_dm = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('data/bbsdm.pkl', 'rb') as f:
|
||||
bbs_dm = pickle.load(f)
|
||||
except Exception as e:
|
||||
bbs_dm = "System: data/bbsdm.pkl not found"
|
||||
|
||||
# Game HS tables
|
||||
try:
|
||||
with open('../data/lemonstand.pkl', 'rb') as f:
|
||||
lemon_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('data/lemonstand.pkl', 'rb') as f:
|
||||
lemon_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
lemon_score = "System: data/lemonstand.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../data/dopewar_hs.pkl', 'rb') as f:
|
||||
dopewar_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('data/dopewar_hs.pkl', 'rb') as f:
|
||||
dopewar_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
dopewar_score = "System: data/dopewar_hs.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../data/blackjack_hs.pkl', 'rb') as f:
|
||||
blackjack_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('data/blackjack_hs.pkl', 'rb') as f:
|
||||
blackjack_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
blackjack_score = "System: data/blackjack_hs.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../data/videopoker_hs.pkl', 'rb') as f:
|
||||
videopoker_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('data/videopoker_hs.pkl', 'rb') as f:
|
||||
videopoker_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
videopoker_score = "System: data/videopoker_hs.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../mmind_hs.pkl', 'rb') as f:
|
||||
mmind_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('mmind_hs.pkl', 'rb') as f:
|
||||
mmind_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
mmind_score = "System: mmind_hs.pkl not found"
|
||||
|
||||
try:
|
||||
with open('../data/golfsim_hs.pkl', 'rb') as f:
|
||||
golfsim_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
try:
|
||||
with open('data/golfsim_hs.pkl', 'rb') as f:
|
||||
golfsim_score = pickle.load(f)
|
||||
except Exception as e:
|
||||
golfsim_score = "System: data/golfsim_hs.pkl not found"
|
||||
|
||||
|
||||
print ("\n Meshing-Around Database Admin Tool\n")
|
||||
print ("System: bbs_messages")
|
||||
print (bbs_messages)
|
||||
print ("\nSystem: bbs_dm")
|
||||
print (bbs_dm)
|
||||
print (f"\n\nGame HS tables\n")
|
||||
print (f"lemon:{lemon_score}")
|
||||
print (f"dopewar:{dopewar_score}")
|
||||
print (f"blackjack:{blackjack_score}")
|
||||
print (f"videopoker:{videopoker_score}")
|
||||
print (f"mmind:{mmind_score}")
|
||||
print (f"golfsim:{golfsim_score}")
|
||||
print ("\n")
|
||||
40
etc/eas_alert_parser.py
Normal file
40
etc/eas_alert_parser.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Super sloppy multimon-ng output cleaner for processing by EAS2Text
|
||||
# I maed dis, sorta, mostly just mashed code I found or that chatGPT hallucinated
|
||||
# by Mike O'Connell/skrrt, no licence or whatever just be chill yo
|
||||
# enhanced by sheer.cold
|
||||
|
||||
import re
|
||||
from EAS2Text import EAS2Text
|
||||
|
||||
buff=[] # store messages for writing
|
||||
seen=set()
|
||||
pattern = re.compile(r'ZCZC.*?NWS-')
|
||||
while True:
|
||||
try:
|
||||
# Handle piped input
|
||||
line=input().strip()
|
||||
except EOFError:
|
||||
break
|
||||
# only want EAS lines
|
||||
if line.startswith("EAS:") or line.startswith("EAS (part):"):
|
||||
content=line.split(maxsplit=1)[1]
|
||||
if content=="NNNN": # end of EAS message
|
||||
# write if we have something
|
||||
if buff:
|
||||
print("writing")
|
||||
with open("alert.txt","w") as fh:
|
||||
fh.write('\n'.join(buff))
|
||||
# prepare for new data
|
||||
buff.clear()
|
||||
seen.clear()
|
||||
elif content in seen:
|
||||
# don't need repeats'
|
||||
continue
|
||||
else:
|
||||
# check for national weather service
|
||||
match=pattern.search(content)
|
||||
if match:
|
||||
seen.add(content)
|
||||
msg=EAS2Text(content).EASText
|
||||
print("got message", msg)
|
||||
buff.append(msg)
|
||||
@@ -7,8 +7,12 @@ Description=MESH-BOT
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=/usr/bin/bash /dir/launch.sh mesh
|
||||
ExecStart=python3 mesh_bot.py
|
||||
ExecStop=pkill -f mesh_bot.py
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
10
etc/mesh_bot_reporting.timer
Normal file
10
etc/mesh_bot_reporting.timer
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=MeshingAround-ReportingTask
|
||||
|
||||
[Timer]
|
||||
OnUnitActiveSec=1h
|
||||
OnbootSec=5min
|
||||
Unit=mesh_bot_reporting.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
25
etc/mesh_bot_reporting.tmp
Normal file
25
etc/mesh_bot_reporting.tmp
Normal file
@@ -0,0 +1,25 @@
|
||||
# /etc/systemd/system/mesh_bot.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl start mesh_bot.service
|
||||
|
||||
[Unit]
|
||||
Description=MeshingAround-Reporting
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 etc/report_generator5.py
|
||||
ExecStop=pkill -f report_generator5.py
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
Restart=on-failure
|
||||
Type=notify #try simple if any problems
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -7,8 +7,12 @@ Description=PONG-BOT
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=/usr/bin/bash /dir/launch.sh pong
|
||||
ExecStart=python3 pong_bot.py
|
||||
ExecStop=pkill -f pong_bot.py
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
990
etc/report_generator.py
Normal file
990
etc/report_generator.py
Normal file
@@ -0,0 +1,990 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import glob
|
||||
import json
|
||||
import pickle
|
||||
import platform
|
||||
import requests
|
||||
import subprocess
|
||||
import configparser
|
||||
from string import Template
|
||||
from datetime import datetime
|
||||
from importlib.metadata import version
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
# global variables
|
||||
LOG_PATH = '/opt/meshing-around/logs' # override path to log files (defaults to ../log)
|
||||
W3_PATH = '/var/www/html/' # override path to web server root (defaults to ../www)
|
||||
multiLogReader = False # set to True to read all logs in ../log
|
||||
shameWordList = ['password', 'combo', 'key', 'hidden', 'secret', 'pass', 'token', 'login', 'username', 'admin', 'root', 'base64:', '==' ]
|
||||
|
||||
# system variables
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
www_dir = os.path.join(script_dir, 'www')
|
||||
config_file = os.path.join(script_dir, 'web_reporter.cfg')
|
||||
|
||||
# set up report.cfg as ini file
|
||||
config = configparser.ConfigParser()
|
||||
try:
|
||||
config.read(config_file)
|
||||
except Exception as e:
|
||||
print(f"Error reading web_reporter.cfg: {str(e)} generating default config")
|
||||
|
||||
if config.sections() == []:
|
||||
print(f"web_reporter.cfg is empty or does not exist, generating default config")
|
||||
shameWordList = shameWordList_str = ', '.join(shameWordList)
|
||||
config['reporting'] = {'log_path': script_dir, 'w3_path': www_dir, 'multi_log_reader': 'False', 'shame_word_list': shameWordList}
|
||||
with open(config_file, 'w') as configfile:
|
||||
config.write(configfile)
|
||||
|
||||
# read config file
|
||||
LOG_PATH = config['reporting'].get('log_path', LOG_PATH)
|
||||
W3_PATH = config['reporting'].get('w3_path', W3_PATH)
|
||||
multiLogReader = config['reporting'].getboolean('multi_log_reader', multiLogReader)
|
||||
# config['reporting']['shame_word_list'] is a comma-separated string
|
||||
shameWordList = config['reporting'].get('shame_word_list', '')
|
||||
if isinstance(shameWordList, str):
|
||||
shameWordList = shameWordList.split(', ')
|
||||
|
||||
|
||||
def parse_log_file(file_path):
|
||||
global log_data
|
||||
lines = ['']
|
||||
|
||||
# see if many logs are present
|
||||
if multiLogReader:
|
||||
# set file_path to the cwd of the default project ../log
|
||||
log_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'logs')
|
||||
log_files = glob.glob(os.path.join(log_dir, 'meshbot*.log'))
|
||||
print(f"Checking log files: {log_files}")
|
||||
|
||||
if log_files:
|
||||
log_files.sort()
|
||||
|
||||
for logFile in log_files:
|
||||
with open(os.path.join(log_dir, logFile), 'r') as file:
|
||||
lines += file.readlines()
|
||||
else:
|
||||
try:
|
||||
print(f"Checking log file: {file_path}")
|
||||
with open(file_path, 'r') as file:
|
||||
lines = file.readlines()
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File not found at {file_path}")
|
||||
sys.exit(1)
|
||||
|
||||
if multiLogReader:
|
||||
print(f"Consumed {len(lines)} lines from {len(log_files)} log files")
|
||||
else:
|
||||
print(f"Consumed {len(lines)} lines from {file_path}")
|
||||
|
||||
log_data = {
|
||||
'command_counts': Counter(),
|
||||
'message_types': Counter(),
|
||||
'llm_queries': Counter(),
|
||||
'unique_users': set(),
|
||||
'warnings': [],
|
||||
'errors': [],
|
||||
'hourly_activity': defaultdict(int),
|
||||
'bbs_messages': 0,
|
||||
'messages_waiting': 0,
|
||||
'total_messages': 0,
|
||||
'gps_coordinates': defaultdict(list),
|
||||
'command_timestamps': [],
|
||||
'message_timestamps': [],
|
||||
'firmware1_version': "N/A",
|
||||
'firmware2_version': "N/A",
|
||||
'node1_uptime': "N/A",
|
||||
'node2_uptime': "N/A",
|
||||
'node1_name': "N/A",
|
||||
'node2_name': "N/A",
|
||||
'node1_ID': "N/A",
|
||||
'node2_ID': "N/A",
|
||||
'shameList': []
|
||||
}
|
||||
|
||||
for line in lines:
|
||||
timestamp_match = re.match(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d+', line)
|
||||
if timestamp_match:
|
||||
timestamp = datetime.strptime(timestamp_match.group(1), '%Y-%m-%d %H:%M:%S')
|
||||
log_data['hourly_activity'][timestamp.strftime('%Y-%m-%d %H:00:00')] += 1
|
||||
|
||||
if 'Bot detected Commands' in line or 'LLM Query:' in line or 'PlayingGame' in line:
|
||||
# get the command and user from the line
|
||||
command = re.search(r"'cmd': '(\w+)'", line)
|
||||
user = re.search(r"From: (.+)$", line)
|
||||
|
||||
if 'LLM Query:' in line:
|
||||
log_data['command_counts']['LLM Query'] += 1
|
||||
log_data['command_timestamps'].append((timestamp.isoformat(), 'LLM Query'))
|
||||
|
||||
if 'PlayingGame' in line:
|
||||
#log line looks like this. 2024-10-04 20:24:53,381 | DEBUG | System: 862418040 PlayingGame BlackJack last_cmd: new
|
||||
game = re.search(r'PlayingGame (\w+)', line)
|
||||
user = re.search(r'System: (\d+)', line)
|
||||
log_data['command_counts'][game.group(1)] += 1
|
||||
log_data['command_timestamps'].append((timestamp.isoformat(), game))
|
||||
|
||||
if 'Sending DM:' in line or 'Sending Multi-Chunk DM:' in line or 'SendingChannel:' in line or 'Sending Multi-Chunk Message:' in line:
|
||||
log_data['message_types']['Outgoing DM'] += 1
|
||||
log_data['total_messages'] += 1
|
||||
log_data['message_timestamps'].append((timestamp.isoformat(), 'Outgoing DM'))
|
||||
|
||||
if 'Received DM:' in line or 'Ignoring DM:' in line or 'Ignoring Message:' in line or 'ReceivedChannel:' in line or 'LLM Query:' in line:
|
||||
log_data['message_types']['Incoming DM'] += 1
|
||||
log_data['total_messages'] += 1
|
||||
# include a little of the message
|
||||
if 'Ignoring Message:' in line:
|
||||
log_data['message_timestamps'].append((timestamp.isoformat(), f'Incoming: {line.split("Ignoring Message:")[1][:90]}'))
|
||||
elif 'Ignoring DM:' in line:
|
||||
log_data['message_timestamps'].append((timestamp.isoformat(), f'Incoming: {line.split("Ignoring DM:")[1][:90]}'))
|
||||
elif 'LLM Query:' in line:
|
||||
log_data['message_timestamps'].append((timestamp.isoformat(), f'Incoming: {line.split("LLM Query:")[1][:90]}'))
|
||||
else:
|
||||
log_data['message_timestamps'].append((timestamp.isoformat(), 'Incoming:'))
|
||||
# check for shame words in the message
|
||||
for word in shameWordList:
|
||||
if word in line.lower():
|
||||
if line not in log_data['shameList']:
|
||||
line = line.replace('Ignoring Message:', '')
|
||||
line = line.replace('|', '')
|
||||
line = line.replace('INFO', '')
|
||||
line = line.replace('DEBUG', '')
|
||||
log_data['shameList'].insert(0, line)
|
||||
|
||||
# get the user who sent the message
|
||||
if 'To: ' in line:
|
||||
user_match = re.search(r"From: '([^']+)'(?: To:|$)", line)
|
||||
else:
|
||||
user_match = re.search(r"From: (.+)$", line)
|
||||
if user_match:
|
||||
log_data['unique_users'].add(user_match.group(1))
|
||||
|
||||
# Error Logs
|
||||
if 'WARNING |' in line:
|
||||
# remove some junk from the line
|
||||
line = line.replace('|', '')
|
||||
line = line.replace(' ', ' ')
|
||||
log_data['warnings'].insert(0, line)
|
||||
|
||||
if 'ERROR |' in line or 'CRITICAL |' in line:
|
||||
# remove some junk from the line
|
||||
line = line.replace('System:', '')
|
||||
line = line.replace('|', '')
|
||||
line = line.replace(' ', ' ')
|
||||
log_data['errors'].insert(0, line)
|
||||
|
||||
# bbs messages
|
||||
bbs_match = re.search(r'📡BBSdb has (\d+) messages.*?Messages waiting: (\d+)', line)
|
||||
if bbs_match:
|
||||
bbs_messages = int(bbs_match.group(1))
|
||||
messages_waiting = int(bbs_match.group(2))
|
||||
log_data['bbs_messages'] = bbs_messages
|
||||
log_data['messages_waiting'] = messages_waiting
|
||||
|
||||
gps_match = re.search(r'location data for (\d+) is ([-\d.]+),([-\d.]+)', line)
|
||||
if gps_match:
|
||||
node_id = None
|
||||
node_id, lat, lon = gps_match.groups()
|
||||
log_data['gps_coordinates'][node_id].append((float(lat), float(lon)))
|
||||
|
||||
# get telemetry data
|
||||
if '| Telemetry:' in line:
|
||||
telemetry_match = re.search(r'Telemetry:(\d+) numPacketsRx:(\d+) numPacketsRxErr:(\d+) numPacketsTx:(\d+) numPacketsTxErr:(\d+) ChUtil%:(\d+\.\d+) AirTx%:(\d+\.\d+) totalNodes:(\d+) Online:(\d+) Uptime:(\d+d) Volt:(\d+\.\d+) Firmware:(\d+\.\d+\.\d+\.\w+)', line)
|
||||
if telemetry_match:
|
||||
interface_number, numPacketsRx, numPacketsRxErr, numPacketsTx, numPacketsTxErr, ChUtil, AirTx, totalNodes, online, uptime, volt, firmware_version = telemetry_match.groups()
|
||||
data = f"Tx: {numPacketsTx} Rx: {numPacketsRx} Uptime: {uptime} Volt: {volt} numPacketsRxErr: {numPacketsRxErr} numPacketsTxErr: {numPacketsTxErr} ChUtil: {ChUtil} AirTx: {AirTx} totalNodes: {totalNodes} Online: {online}"
|
||||
if interface_number == '1':
|
||||
log_data['firmware1_version'] = firmware_version
|
||||
log_data['node1_uptime'] = data
|
||||
log_data['nodeCount1'] = totalNodes
|
||||
log_data['nodeCountOnline1'] = online
|
||||
log_data['tx1'] = numPacketsTx
|
||||
log_data['rx1'] = numPacketsRx
|
||||
elif interface_number == '2':
|
||||
log_data['firmware2_version'] = firmware_version
|
||||
log_data['node2_uptime'] = data
|
||||
log_data['nodeCount2'] = totalNodes
|
||||
log_data['nodeCountOnline2'] = online
|
||||
log_data['tx2'] = numPacketsTx
|
||||
log_data['rx2'] = numPacketsRx
|
||||
|
||||
# get name and nodeID for devices
|
||||
if 'Autoresponder Started for Device' in line:
|
||||
device_match = re.search(r'Autoresponder Started for Device(\d+)\s+([^\s,]+).*?NodeID: (\d+)', line)
|
||||
if device_match:
|
||||
device_id = device_match.group(1)
|
||||
device_name = device_match.group(2)
|
||||
node_id = device_match.group(3)
|
||||
if device_id == '1':
|
||||
log_data['node1_name'] = device_name
|
||||
log_data['node1_ID'] = node_id
|
||||
elif device_id == '2':
|
||||
log_data['node2_name'] = device_name
|
||||
log_data['node2_ID'] = node_id
|
||||
|
||||
log_data['unique_users'] = list(log_data['unique_users'])
|
||||
log_data['unique_users'].reverse()
|
||||
return log_data
|
||||
|
||||
def get_system_info():
|
||||
def get_command_output(command):
|
||||
try:
|
||||
return subprocess.check_output(command, shell=True).decode('utf-8').strip()
|
||||
except subprocess.CalledProcessError:
|
||||
return "N/A"
|
||||
|
||||
# Capture some system information from log_data
|
||||
firmware1_version = log_data['firmware1_version']
|
||||
firmware2_version = log_data['firmware2_version']
|
||||
node1_uptime = log_data['node1_uptime']
|
||||
node2_uptime = log_data['node2_uptime']
|
||||
node1_name = log_data['node1_name']
|
||||
node2_name = log_data['node2_name']
|
||||
node1_ID = log_data['node1_ID']
|
||||
node2_ID = log_data['node2_ID']
|
||||
|
||||
print(f"Node1: {node1_name} {node1_ID} {firmware1_version}")
|
||||
print(f"Node2: {node2_name} {node2_ID} {firmware2_version}")
|
||||
|
||||
# get Meshtastic CLI version on web
|
||||
try:
|
||||
url = "https://pypi.org/pypi/meshtastic/json"
|
||||
data = requests.get(url, timeout=5).json()
|
||||
pypi_version = data["info"]["version"]
|
||||
cli_web = f"v{pypi_version}"
|
||||
except Exception:
|
||||
pass
|
||||
# get Meshtastic CLI version on local
|
||||
try:
|
||||
if "importlib.metadata" in sys.modules:
|
||||
cli_local = version("meshtastic")
|
||||
except:
|
||||
pass # Python 3.7 and below, meh..
|
||||
|
||||
|
||||
if platform.system() == "Linux":
|
||||
uptime = get_command_output("uptime -p")
|
||||
memory_total = get_command_output("free -m | awk '/Mem:/ {print $2}'")
|
||||
memory_available = get_command_output("free -m | awk '/Mem:/ {print $7}'")
|
||||
disk_total = get_command_output("df -h / | awk 'NR==2 {print $2}'")
|
||||
disk_free = get_command_output("df -h / | awk 'NR==2 {print $4}'")
|
||||
elif platform.system() == "Darwin": # macOS
|
||||
uptime = get_command_output("uptime | awk '{print $3,$4,$5}'")
|
||||
memory_total = get_command_output("sysctl -n hw.memsize | awk '{print $0/1024/1024}'")
|
||||
memory_available = "N/A" # Not easily available on macOS without additional tools
|
||||
disk_total = get_command_output("df -h / | awk 'NR==2 {print $2}'")
|
||||
disk_free = get_command_output("df -h / | awk 'NR==2 {print $4}'")
|
||||
else:
|
||||
return {
|
||||
'uptime': "N/A",
|
||||
'memory_total': "N/A",
|
||||
'memory_available': "N/A",
|
||||
'disk_total': "N/A",
|
||||
'disk_free': "N/A",
|
||||
'interface1_version': "N/A",
|
||||
'interface2_version': "N/A",
|
||||
'node1_uptime': "N/A",
|
||||
'node2_uptime': "N/A",
|
||||
'node1_name': "N/A",
|
||||
'node2_name': "N/A",
|
||||
'node1_ID': "N/A",
|
||||
'node2_ID': "N/A",
|
||||
'cli_web': "N/A",
|
||||
'cli_local': "N/A"
|
||||
}
|
||||
|
||||
return {
|
||||
'uptime': uptime,
|
||||
'memory_total': f"{memory_total} MB",
|
||||
'memory_available': f"{memory_available} MB" if memory_available != "N/A" else "N/A",
|
||||
'disk_total': disk_total,
|
||||
'disk_free': disk_free,
|
||||
'interface1_version': firmware1_version,
|
||||
'interface2_version': firmware2_version,
|
||||
'node1_uptime': node1_uptime,
|
||||
'node2_uptime': node2_uptime,
|
||||
'node1_name': node1_name,
|
||||
'node2_name': node2_name,
|
||||
'node1_ID': node1_ID,
|
||||
'node2_ID': node2_ID,
|
||||
'cli_web': cli_web,
|
||||
'cli_local': cli_local
|
||||
}
|
||||
|
||||
def get_wall_of_shame():
|
||||
# Get the wall of shame out of the log data
|
||||
logShameList = log_data['shameList']
|
||||
|
||||
# future space for other ideas
|
||||
|
||||
return {
|
||||
'shame': ', '.join(shameWordList),
|
||||
'shameList': '\n'.join(f'<li>{line}</li>' for line in logShameList),
|
||||
}
|
||||
|
||||
def get_database_info():
|
||||
# ../config.ini location to script path
|
||||
config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'config.ini')
|
||||
# get config.ini variables
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
# for section in config.sections():
|
||||
# print(f"Section: {section}")
|
||||
# for key in config[section]:
|
||||
# print(f"Key: {key}, Value: {config[section][key]}")
|
||||
banList = config['bbs'].get('bbs_ban_list', 'none')
|
||||
adminList = config['bbs'].get('bbs_admin_list', 'none')
|
||||
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', 'none')
|
||||
|
||||
# Define the base directory
|
||||
base_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'data'))
|
||||
|
||||
# data files
|
||||
databaseFiles = [os.path.join(base_dir, 'lemonstand_hs.pkl'),
|
||||
os.path.join(base_dir, 'dopewar_hs.pkl'),
|
||||
os.path.join(base_dir, 'blackjack_hs.pkl'),
|
||||
os.path.join(base_dir, 'videopoker_hs.pkl'),
|
||||
os.path.join(base_dir, 'mmind_hs.pkl'),
|
||||
os.path.join(base_dir, 'golfsim_hs.pkl'),
|
||||
os.path.join(base_dir, 'bbsdb.pkl'),
|
||||
os.path.join(base_dir, 'bbsdm.pkl')]
|
||||
|
||||
for file in databaseFiles:
|
||||
try:
|
||||
with open(file, 'rb') as f:
|
||||
if 'lemonstand' in file:
|
||||
lemon_score = pickle.load(f)
|
||||
elif 'dopewar' in file:
|
||||
dopewar_score = pickle.load(f)
|
||||
elif 'blackjack' in file:
|
||||
blackjack_score = pickle.load(f)
|
||||
elif 'videopoker' in file:
|
||||
videopoker_score = pickle.load(f)
|
||||
elif 'mmind' in file:
|
||||
mmind_score = pickle.load(f)
|
||||
elif 'golfsim' in file:
|
||||
golfsim_score = pickle.load(f)
|
||||
elif 'bbsdb' in file:
|
||||
bbsdb = pickle.load(f)
|
||||
elif 'bbsdm' in file:
|
||||
bbsdm = pickle.load(f)
|
||||
except Exception as e:
|
||||
print(f"Warning issue reading database file: {str(e)}")
|
||||
if 'lemonstand' in file:
|
||||
lemon_score = "no data"
|
||||
elif 'dopewar' in file:
|
||||
dopewar_score = "no data"
|
||||
elif 'blackjack' in file:
|
||||
blackjack_score = "no data"
|
||||
elif 'videopoker' in file:
|
||||
videopoker_score = "no data"
|
||||
elif 'mmind' in file:
|
||||
mmind_score = "no data"
|
||||
elif 'golfsim' in file:
|
||||
golfsim_score = "no data"
|
||||
elif 'bbsdb' in file:
|
||||
bbsdb = "no data"
|
||||
elif 'bbsdm' in file:
|
||||
bbsdm = "no data"
|
||||
|
||||
# pretty print the bbsdb
|
||||
prettyBBSdb = ""
|
||||
try:
|
||||
for i in range(len(bbsdb)):
|
||||
prettyBBSdb += f'<li>{bbsdb[i]}</li>'
|
||||
except Exception as e:
|
||||
print(f"Error with database: {str(e)}")
|
||||
pass
|
||||
|
||||
# pretty print the bbsdm
|
||||
prettyBBSdm = ""
|
||||
try:
|
||||
for i in range(len(bbsdm)):
|
||||
prettyBBSdm += f'<li>{bbsdm[i]}</li>'
|
||||
except Exception as e:
|
||||
print(f"Error with database: {str(e)}")
|
||||
pass
|
||||
|
||||
if 'no data' in [lemon_score, dopewar_score, blackjack_score, videopoker_score, mmind_score, golfsim_score]:
|
||||
database = "Error(s) Detected"
|
||||
else:
|
||||
database = " Online"
|
||||
|
||||
return {
|
||||
'database': database,
|
||||
"bbsdb": prettyBBSdb,
|
||||
"bbsdm": prettyBBSdm,
|
||||
'lemon_score': lemon_score,
|
||||
'dopewar_score': dopewar_score,
|
||||
'blackjack_score': blackjack_score,
|
||||
'videopoker_score': videopoker_score,
|
||||
'mmind_score': mmind_score,
|
||||
'golfsim_score': golfsim_score,
|
||||
'banList': banList,
|
||||
'adminList': adminList,
|
||||
'sentryIgnoreList': sentryIgnoreList
|
||||
}
|
||||
|
||||
def generate_main_html(log_data, system_info):
|
||||
html_template = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MeshBot (BBS) Web Dashboard</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
}
|
||||
.header {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
font-size: 24px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
background-color: #ddd;
|
||||
padding: 10px;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.content {
|
||||
margin-left: 220px;
|
||||
margin-top: 60px;
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.chart-container {
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
#map, .chart-content {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
.list-container {
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
li {
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
#iframe-content {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 220px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: white;
|
||||
z-index: 900;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
.timestamp-list {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">MeshBot (BBS) Web Dashboard</div>
|
||||
<div class="sidebar">
|
||||
<ul>
|
||||
<li><a href="#" onclick="showDashboard(); return false;">Dashboard</a></li>
|
||||
<li><a href="#" onclick="showIframe('network_map_${date}.html'); return false;">Network Map</a></li>
|
||||
<li><a href="#" onclick="showIframe('wall_of_shame_${date}.html'); return false;">Wall of Shame</a></li>
|
||||
<li><a href="#" onclick="showIframe('database_${date}.html'); return false;">Database</a></li>
|
||||
<li><a href="#" onclick="showIframe('hosts_${date}.html'); return false;">System Host</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content" id="dashboard-content">
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Node Locations</div>
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Network Activity</div>
|
||||
<div class="chart-content">
|
||||
<canvas id="activityChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Command Usage</div>
|
||||
<div class="chart-content">
|
||||
<canvas id="commandChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Message Types</div>
|
||||
<div class="chart-content">
|
||||
<canvas id="messageChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">BBS Stored Message Counts</div>
|
||||
<div class="chart-content">
|
||||
<canvas id="messageCountChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Recent Commands</div>
|
||||
<div class="timestamp-list">
|
||||
<ul>
|
||||
${command_timestamps}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<div class="chart-title">Recent Messages</div>
|
||||
<div class="timestamp-list">
|
||||
<ul>
|
||||
${message_timestamps}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-container">
|
||||
<div class="chart-title">Unique Users</div>
|
||||
<ul>
|
||||
${unique_users}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="list-container">
|
||||
<div class="chart-title">Warnings</div>
|
||||
<ul>
|
||||
${warnings}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="list-container">
|
||||
<div class="chart-title">Errors</div>
|
||||
<ul>
|
||||
${errors}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="iframe-content">
|
||||
<iframe id="content-iframe" src=""></iframe>
|
||||
</div>
|
||||
<script>
|
||||
const commandData = ${command_data};
|
||||
const messageData = ${message_data};
|
||||
const activityData = ${activity_data};
|
||||
const messageCountData = {
|
||||
labels: ['BBSdm Messages', 'BBSdb Messages', 'Channel Messages'],
|
||||
datasets: [{
|
||||
label: 'Message Counts',
|
||||
data: [${messages_waiting}, ${bbs_messages}, ${total_messages}],
|
||||
backgroundColor: ['rgba(255, 206, 86, 0.6)', 'rgba(75, 192, 192, 0.6)', 'rgba(54, 162, 235, 0.6)']
|
||||
}]
|
||||
};
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false
|
||||
};
|
||||
|
||||
new Chart(document.getElementById('commandChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: Object.keys(commandData),
|
||||
datasets: [{
|
||||
label: 'Command Usage',
|
||||
data: Object.values(commandData),
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.6)'
|
||||
}]
|
||||
},
|
||||
options: chartOptions
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('messageChart'), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: Object.keys(messageData),
|
||||
datasets: [{
|
||||
data: Object.values(messageData),
|
||||
backgroundColor: ['rgba(255, 99, 132, 0.6)', 'rgba(54, 162, 235, 0.6)']
|
||||
}]
|
||||
},
|
||||
options: chartOptions
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('activityChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: Object.keys(activityData),
|
||||
datasets: [{
|
||||
label: 'Hourly Activity',
|
||||
data: Object.entries(activityData).map(([time, count]) => ({x: new Date(time), y: count})),
|
||||
borderColor: 'rgba(153, 102, 255, 1)',
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...chartOptions,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'hour',
|
||||
displayFormats: {
|
||||
hour: 'MMM d, HH:mm'
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Time'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Activity Count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
new Chart(document.getElementById('messageCountChart'), {
|
||||
type: 'bar',
|
||||
data: messageCountData,
|
||||
options: {
|
||||
...chartOptions,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var map = L.map('map').setView([0, 0], 2);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
var gpsCoordinates = ${gps_coordinates};
|
||||
for (var nodeId in gpsCoordinates) {
|
||||
var coords = gpsCoordinates[nodeId][0];
|
||||
L.marker(coords).addTo(map)
|
||||
.bindPopup("Node ID: " + nodeId);
|
||||
}
|
||||
|
||||
var bounds = [];
|
||||
for (var nodeId in gpsCoordinates) {
|
||||
bounds.push(gpsCoordinates[nodeId][0]);
|
||||
}
|
||||
map.fitBounds(bounds);
|
||||
|
||||
function showIframe(src) {
|
||||
document.getElementById('dashboard-content').style.display = 'none';
|
||||
document.getElementById('iframe-content').style.display = 'block';
|
||||
document.getElementById('content-iframe').src = src;
|
||||
}
|
||||
|
||||
function showDashboard() {
|
||||
document.getElementById('dashboard-content').style.display = 'grid';
|
||||
document.getElementById('iframe-content').style.display = 'none';
|
||||
document.getElementById('content-iframe').src = '';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
template = Template(html_template)
|
||||
return template.safe_substitute(
|
||||
date=datetime.now().strftime('%Y_%m_%d'),
|
||||
command_data=json.dumps(log_data['command_counts']),
|
||||
message_data=json.dumps(log_data['message_types']),
|
||||
activity_data=json.dumps(log_data['hourly_activity']),
|
||||
bbs_messages=log_data['bbs_messages'],
|
||||
messages_waiting=log_data['messages_waiting'],
|
||||
total_messages=log_data['total_messages'],
|
||||
total_llm_queries=log_data['message_types']['LLM Query'],
|
||||
gps_coordinates=json.dumps(log_data['gps_coordinates']),
|
||||
unique_users='\n'.join(f'<li>{user}</li>' for user in log_data['unique_users']),
|
||||
warnings='\n'.join(f'<li>{warning}</li>' for warning in log_data['warnings']),
|
||||
errors='\n'.join(f'<li>{error}</li>' for error in log_data['errors']),
|
||||
command_timestamps='\n'.join(f'<li>{timestamp}: {cmd}</li>' for timestamp, cmd in reversed(log_data['command_timestamps'][-50:])),
|
||||
message_timestamps='\n'.join(f'<li>{timestamp}: {msg_type}</li>' for timestamp, msg_type in reversed(log_data['message_timestamps'][-50:]))
|
||||
)
|
||||
|
||||
def generate_network_map_html(log_data):
|
||||
html_template = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Network Map</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
|
||||
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
|
||||
<style>
|
||||
body { margin: 0; padding: 0; }
|
||||
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script>
|
||||
var map = L.map('map').setView([0, 0], 2);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
var gpsCoordinates = ${gps_coordinates};
|
||||
for (var nodeId in gpsCoordinates) {
|
||||
var coords = gpsCoordinates[nodeId][0];
|
||||
L.marker(coords).addTo(map)
|
||||
.bindPopup("Node ID: " + nodeId);
|
||||
}
|
||||
|
||||
var bounds = [];
|
||||
for (var nodeId in gpsCoordinates) {
|
||||
bounds.push(gpsCoordinates[nodeId][0]);
|
||||
}
|
||||
map.fitBounds(bounds);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
template = Template(html_template)
|
||||
return template.safe_substitute(gps_coordinates=json.dumps(log_data['gps_coordinates']))
|
||||
|
||||
def generate_sys_hosts_html(system_info):
|
||||
html_template = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>System Host Information</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; }
|
||||
h1 { color: #00ff00; }
|
||||
table { border-collapse: collapse; width: 100%; background-color: #d3d3d3; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #f2f2f2; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>System Host Information</h1>
|
||||
<table>
|
||||
<tr><th>OS Metric</th><th>Value</th></tr>
|
||||
<tr><td>Uptime</td><td>${uptime}</td></tr>
|
||||
<tr><td>Total Memory</td><td>${memory_total}</td></tr>
|
||||
<tr><td>Available Memory</td><td>${memory_available}</td></tr>
|
||||
<tr><td>Total Disk Space</td><td>${disk_total}</td></tr>
|
||||
<tr><td>Free Disk Space</td><td>${disk_free}</td></tr>
|
||||
<tr><th>Meshtastic Metric</th><th>Value</th></tr>
|
||||
<tr><td>API Version/Latest</td><td>${cli_local} / ${cli_web}</td></tr>
|
||||
<tr><td>Int1 Name ID</td><td>${node1_name} (${node1_ID})</td></tr>
|
||||
<tr><td>Int1 Stat</td><td>${node1_uptime}</td></tr>
|
||||
<tr><td>Int1 FW Version</td><td>${interface1_version}</td></tr>
|
||||
<tr><td>Int2 Name ID</td><td>${node2_name} (${node2_ID})</td></tr>
|
||||
<tr><td>Int2 Stat</td><td>${node2_uptime}</td></tr>
|
||||
<tr><td>Int2 FW Version</td><td>${interface2_version}</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
template = Template(html_template)
|
||||
return template.safe_substitute(system_info)
|
||||
|
||||
def generate_wall_of_shame_html(shame_info):
|
||||
html_template = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wall Of Shame</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; }
|
||||
h1 { color: #00ff00; }
|
||||
table { border-collapse: collapse; width: 100%; background-color: #d3d3d3; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #f2f2f2; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Collected Shame</h1>
|
||||
<table>
|
||||
<tr><th>Shame Metric</th><th>Value</th></tr>
|
||||
<tr><td>Shamefull words</td><td>${shame}</td></tr>
|
||||
<tr><td>Shamefull messages</td><td>${shameList}</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
template = Template(html_template)
|
||||
return template.safe_substitute(shame_info)
|
||||
|
||||
def generate_database_html(database_info):
|
||||
html_template = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Database Information</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; }
|
||||
h1 { color: #00ff00; }
|
||||
table { border-collapse: collapse; width: 100%; background-color: #d3d3d3; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #f2f2f2; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Database Information</h1>
|
||||
<p>Connection ${database}</p>
|
||||
<table>
|
||||
<tr><th>config.ini Settings</th><th>Value</th></tr>
|
||||
<tr><td>Admin List</td><td>${adminList}</td></tr>
|
||||
<tr><td>Ban List</td><td>${banList}</td></tr>
|
||||
<tr><td>Sentry Ignore List</td><td>${sentryIgnoreList}</td></tr>
|
||||
</table>
|
||||
<h1>BBS Message Database</h1>
|
||||
<p>BBSdb: ${bbsdb}</p>
|
||||
<p>BBSdm: ${bbsdm}</p>
|
||||
<h1>High Scores</h1>
|
||||
<table>
|
||||
<tr><th>Game</th><th>High Score</th></tr>
|
||||
<tr><td>Lemonade Stand</td><td>${lemon_score}</td></tr>
|
||||
<tr><td>Dopewars</td><td>${dopewar_score}</td></tr>
|
||||
<tr><td>Blackjack</td><td>${blackjack_score}</td></tr>
|
||||
<tr><td>Video Poker</td><td>${videopoker_score}</td></tr>
|
||||
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
|
||||
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
template = Template(html_template)
|
||||
return template.safe_substitute(database_info)
|
||||
|
||||
def main():
|
||||
log_dir = LOG_PATH
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
log_file = f'meshbot.log'
|
||||
log_path = os.path.join(log_dir, log_file)
|
||||
|
||||
if not os.path.exists(log_path):
|
||||
# set file_path to the cwd of the default project ../log
|
||||
file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'logs')
|
||||
file_path = os.path.abspath(file_path)
|
||||
log_path = os.path.join(file_path, log_file)
|
||||
|
||||
log_data = parse_log_file(log_path)
|
||||
system_info = get_system_info()
|
||||
shame_info = get_wall_of_shame()
|
||||
database_info = get_database_info()
|
||||
|
||||
main_html = generate_main_html(log_data, system_info)
|
||||
network_map_html = generate_network_map_html(log_data)
|
||||
hosts_html = generate_sys_hosts_html(system_info)
|
||||
wall_of_shame = generate_wall_of_shame_html(shame_info)
|
||||
database_html = generate_database_html(database_info)
|
||||
|
||||
output_dir = W3_PATH
|
||||
index_path = os.path.join(output_dir, 'index.html')
|
||||
|
||||
print(f"\n\nMeshBot (BBS) Web Dashboard Report Generator")
|
||||
print(f"\nMain dashboard: file://{index_path}\n")
|
||||
|
||||
try:
|
||||
if not os.path.exists(output_dir):
|
||||
output_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'www')
|
||||
output_dir = os.path.abspath(output_dir)
|
||||
index_path = os.path.join(output_dir, 'index.html')
|
||||
|
||||
# Create backup of existing index.html if it exists
|
||||
if os.path.exists(index_path):
|
||||
backup_path = os.path.join(output_dir, f'index_backup_{today}.html')
|
||||
os.rename(index_path, backup_path)
|
||||
print(f"Existing index.html backed up to {backup_path}")
|
||||
|
||||
# Write main HTML to index.html
|
||||
with open(index_path, 'w') as f:
|
||||
f.write(main_html)
|
||||
print(f"Main dashboard written to {index_path}")
|
||||
|
||||
# Write other HTML files
|
||||
with open(os.path.join(output_dir, f'network_map_{today}.html'), 'w') as f:
|
||||
f.write(network_map_html)
|
||||
|
||||
with open(os.path.join(output_dir, f'hosts_{today}.html'), 'w') as f:
|
||||
f.write(hosts_html)
|
||||
|
||||
with open(os.path.join(output_dir, f'wall_of_shame_{today}.html'), 'w') as f:
|
||||
f.write(wall_of_shame)
|
||||
|
||||
with open(os.path.join(output_dir, f'database_{today}.html'), 'w') as f:
|
||||
f.write(database_html)
|
||||
|
||||
print(f"HTML reports generated for {today} in {output_dir}")
|
||||
|
||||
except PermissionError:
|
||||
print("Error: Permission denied. Please run the script with appropriate permissions (e.g., using sudo).")
|
||||
except Exception as e:
|
||||
print(f"An error occurred while writing the output: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1285
etc/report_generator5.py
Normal file
1285
etc/report_generator5.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
etc/reporting.jpg
Normal file
BIN
etc/reporting.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
64
etc/simulator.py
Normal file
64
etc/simulator.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
# # Simulate meshing-around de K7MHI 2024
|
||||
from modules.log import * # Import the logger
|
||||
import time
|
||||
import random
|
||||
|
||||
# Initialize the tool
|
||||
projectName = "example_handler" # name of _handler function to match the function name under test
|
||||
randomNode = False # Set to True to use random node IDs
|
||||
|
||||
# bot.py Simulated functions
|
||||
def get_NodeID():
|
||||
nodeList = [4258675309, 1212121212, 1234567890, 9876543210]
|
||||
if randomNode:
|
||||
nodeID = random.choice(nodeList) # get a random node ID
|
||||
else:
|
||||
nodeID = nodeList[0]
|
||||
return nodeID
|
||||
def get_name_from_number(nodeID, length='short', interface=1):
|
||||
# return random name for nodeID
|
||||
names = ["Max","Molly","Jake","Kelly"]
|
||||
return names[nodeID % len(names)]
|
||||
# # end Initialization of the tool
|
||||
|
||||
# # Function to handle, or the project in test
|
||||
|
||||
|
||||
def example_handler(message, nodeID, deviceID):
|
||||
readableTime = time.ctime(time.time())
|
||||
msg = "Hello World! "
|
||||
msg += f" You are Node ID: {nodeID} "
|
||||
msg += f" Its: {readableTime} "
|
||||
msg += f" You just sent: {message}"
|
||||
return msg
|
||||
|
||||
|
||||
# # end of function test code
|
||||
|
||||
# # Simulate the meshing-around mesh-bot for prototyping new projects
|
||||
if __name__ == '__main__': # represents the bot's main loop
|
||||
packet = ""
|
||||
nodeInt = 1 # represents the device/node number
|
||||
logger.info(f"System: Meshing-Around Simulator Starting for {projectName}")
|
||||
nodeID = get_NodeID() # assign a nodeID
|
||||
projectResponse = globals()[projectName]("", nodeID, nodeInt) # Call the project handler under test
|
||||
while True: # represents the onReceive() loop in the bot.py
|
||||
projectResponse = ""
|
||||
responseLength = 0
|
||||
if randomNode:
|
||||
nodeID = get_NodeID() # assign a random nodeID
|
||||
packet = input(f"CLIENT {nodeID} INPUT: " ) # Emulate the client input
|
||||
if packet != "":
|
||||
#try:
|
||||
projectResponse = globals()[projectName](message = packet, nodeID = nodeID, deviceID = nodeInt)
|
||||
# except Exception as e:
|
||||
# logger.error(f"System: Handler: {e}")
|
||||
# projectResponse = "Error in handler"
|
||||
if projectResponse:
|
||||
responseLength = len(projectResponse) # Evaluate the response length
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"Sending {responseLength} long DM: " +\
|
||||
CustomFormatter.white + projectResponse + CustomFormatter.purple + " To: " + CustomFormatter.white + str(nodeID))
|
||||
time.sleep(0.5)
|
||||
nodeID = get_NodeID() # assign a nodeID
|
||||
# # End of launcher
|
||||
20
etc/www/localscripts/chart.js
Normal file
20
etc/www/localscripts/chart.js
Normal file
File diff suppressed because one or more lines are too long
7
etc/www/localscripts/chartjs-adapter-date-fns.bundle.min.js
vendored
Normal file
7
etc/www/localscripts/chartjs-adapter-date-fns.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
252
etc/www/localscripts/css2
Normal file
252
etc/www/localscripts/css2
Normal file
@@ -0,0 +1,252 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7W0Q5n-wU.woff2) format('woff2');
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/inter/v18/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
640
etc/www/localscripts/leaflet.css
Normal file
640
etc/www/localscripts/leaflet.css
Normal file
@@ -0,0 +1,640 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg,
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-tile {
|
||||
will-change: opacity;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline: 0;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-container a.leaflet-active {
|
||||
outline: 2px solid orange;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path {
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-container .leaflet-control-attribution,
|
||||
.leaflet-container .leaflet-control-scale {
|
||||
font-size: 11px;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 19px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 18px 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 4px 4px 0 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
font: 16px/14px Tahoma, Verdana, sans-serif;
|
||||
color: #c3c3c3;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover {
|
||||
color: #999;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip-container {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-clickable {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
6
etc/www/localscripts/leaflet.js
Normal file
6
etc/www/localscripts/leaflet.js
Normal file
File diff suppressed because one or more lines are too long
101
install.sh
101
install.sh
@@ -2,10 +2,30 @@
|
||||
|
||||
# install.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
|
||||
|
||||
printf "\nMeshing Around Installer\n"
|
||||
printf "\nThis script will install the Meshing Around bot and its dependencies works best in debian/ubuntu\n"
|
||||
printf "\nChecking for dependencies\n"
|
||||
|
||||
|
||||
# add user to groups for serial access
|
||||
printf "\nAdding user to dialout and tty groups for serial access\n"
|
||||
sudo usermod -a -G dialout $USER
|
||||
sudo usermod -a -G tty $USER
|
||||
sudo usermod -a -G bluetooth $USER
|
||||
|
||||
# check for pip
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, please install pip with your OS\n"
|
||||
sudo apt-get install python3-pip
|
||||
else
|
||||
printf "python pip found\n"
|
||||
fi
|
||||
|
||||
# generate config file, check if it exists
|
||||
if [ -f config.ini ]; then
|
||||
@@ -20,21 +40,12 @@ printf "\nConfig file generated\n"
|
||||
# set virtual environment and install dependencies
|
||||
printf "\nMeshing Around Installer\n"
|
||||
|
||||
#check if python3 has venv module
|
||||
if ! python3 -m venv --help &> /dev/null
|
||||
then
|
||||
printf "Python3 venv module not found, please install python3-venv with your OS\n"
|
||||
else
|
||||
printf "Python3 venv module found\n"
|
||||
fi
|
||||
|
||||
echo "Do you want to install the bot in a virtual environment? (y/n)"
|
||||
read venv
|
||||
|
||||
if [ $venv == "y" ]; then
|
||||
# set virtual environment
|
||||
if ! python3 -m venv --help &> /dev/null
|
||||
then
|
||||
if ! python3 -m venv --help &> /dev/null; then
|
||||
printf "Python3 venv module not found, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
else
|
||||
@@ -42,13 +53,28 @@ if [ $venv == "y" ]; then
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# install dependencies
|
||||
pip install -U -r requirements.txt
|
||||
#check if python3 has venv module
|
||||
if [ -f venv/bin/activate ]; then
|
||||
printf "\nFpund virtual environment for python\n"
|
||||
else
|
||||
sudo apt-get install python3-venv
|
||||
printf "\nPython3 venv module not found, please install python3-venv with your OS if not already done. re-run the script\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# config service files for virtual environment
|
||||
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
|
||||
# install dependencies
|
||||
pip install -U -r requirements.txt
|
||||
fi
|
||||
else
|
||||
printf "\nSkipping virtual environment...\n"
|
||||
# install dependencies
|
||||
printf "Are you on Raspberry Pi?\nshould we add --break-system-packages to the pip install command? (y/n)"
|
||||
printf "Are you on Raspberry Pi(debian/ubuntu)?\nshould we add --break-system-packages to the pip install command? (y/n)"
|
||||
read rpi
|
||||
if [ $rpi == "y" ]; then
|
||||
pip install -U -r requirements.txt --break-system-packages
|
||||
@@ -61,33 +87,42 @@ printf "\n\n"
|
||||
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
|
||||
read bot
|
||||
|
||||
#set the correct path in the service file
|
||||
program_path=$(pwd)
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
# set the correct path in the service file
|
||||
replace="s|/dir/|$program_path/|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
# set the correct user in the service file?
|
||||
whoami=$(whoami)
|
||||
replace="s|User=pi|User=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
replace="s|Group=pi|Group=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
sudo systemctl daemon-reload
|
||||
printf "\n service files updated\n"
|
||||
|
||||
# ask if emoji font should be installed for linux
|
||||
echo "Do you want to install the emoji font for debian linux? (y/n)"
|
||||
echo "Do you want to install the emoji font for debian/ubuntu linux? (y/n)"
|
||||
read emoji
|
||||
if [ $emoji == "y" ]; then
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
sudo apt-get install -y fonts-noto-color-emoji
|
||||
echo "Emoji font installed!, reboot to load the font"
|
||||
fi
|
||||
|
||||
if [ $bot == "pong" ]; then
|
||||
# install service for pong bot
|
||||
sudo cp etc/pong_bot.service /etc/systemd/system/
|
||||
exit 0
|
||||
sudo systemctl enable pong_bot.service
|
||||
fi
|
||||
|
||||
if [ $bot == "mesh" ]; then
|
||||
# install service for mesh bot
|
||||
sudo cp etc/mesh_bot.service /etc/systemd/system/
|
||||
exit 0
|
||||
sudo systemctl enable mesh_bot.service
|
||||
fi
|
||||
|
||||
if [ $bot == "n" ]; then
|
||||
@@ -97,6 +132,28 @@ if [ $bot == "n" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\nOptionally if you want to install the LLM Ollama compnents we will execute the following commands\n"
|
||||
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
|
||||
|
||||
# ask if the user wants to install the LLM Ollama components
|
||||
echo "Do you want to install the LLM Ollama components? (y/n)"
|
||||
read ollama
|
||||
if [ $ollama == "y" ]; then
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# ask if want to install gemma2:2b
|
||||
printf "\n Ollama install done now we can install the Gemma2:2b components, multi GB download\n"
|
||||
echo "Do you want to install the Gemma2:2b components? (y/n)"
|
||||
read gemma
|
||||
if [ $gemma == "y" ]; then
|
||||
ollama pull gemma2:2b
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Good time to reboot? (y/n)"
|
||||
read reboot
|
||||
if [ $reboot == "y" ]; then
|
||||
sudo reboot
|
||||
fi
|
||||
|
||||
printf "\nGoodbye!"
|
||||
exit 0
|
||||
|
||||
29
launch.sh
29
launch.sh
@@ -3,22 +3,31 @@
|
||||
# launch.sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# activate the virtual environment if it exists
|
||||
if [ -d "venv" ]; then
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
if [ ! -f "config.ini" ]; then
|
||||
cp config.template config.ini
|
||||
fi
|
||||
|
||||
# launch the application
|
||||
if [ "$1" == "pong" ]; then
|
||||
python3 pong_bot.py
|
||||
elif [ "$1" == "mesh" ]; then
|
||||
python3 mesh_bot.py
|
||||
# activate the virtual environment if it exists
|
||||
if [ -d "venv" ]; then
|
||||
source venv/bin/activate
|
||||
else
|
||||
printf "\nPlease provide a bot to launch (pong/mesh)"
|
||||
echo "Virtual environment not found, this tool just launches the .py in venv"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# launch the application
|
||||
if [[ "$1" == pong* ]]; then
|
||||
python3 pong_bot.py
|
||||
elif [[ "$1" == mesh* ]]; then
|
||||
python3 mesh_bot.py
|
||||
elif [ "$1" == "html" ]; then
|
||||
python3 etc/report_generator.py
|
||||
elif [ "$1" == "html5" ]; then
|
||||
python3 etc/report_generator5.py
|
||||
else
|
||||
echo "Please provide a bot to launch (pong/mesh) or a report to generate (html/html5)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
deactivate
|
||||
26
logs/README.md
Normal file
26
logs/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
Logs will collect here. Give a day of logs or a bunch of messages to have good reports.
|
||||
|
||||
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed.
|
||||
- `multi_log_reader = True` on by default will read all logs (or set to false to return daily logs)
|
||||
- Make sure to have `SyslogToFile = True` and default of DEBUG log level to fully enable reporting! ‼️
|
||||
- provided serviceTimer templates in etc/
|
||||
|
||||

|
||||
|
||||
Logging messages to disk or 'Syslog' to disk uses the python native logging function.
|
||||
```
|
||||
[general]
|
||||
# logging to file of the non Bot messages only
|
||||
LogMessagesToFile = False
|
||||
# Logging of system messages to file, needed for reporting engine
|
||||
SyslogToFile = True
|
||||
# Number of log files to keep in days, 0 to keep all
|
||||
log_backup_count = 32
|
||||
```
|
||||
|
||||
To change the stdout (what you see on the console) logging level (default is DEBUG) see the following example, line is in [../modules/log.py](../modules/log.py)
|
||||
|
||||
```
|
||||
# Set level for stdout handler
|
||||
stdout_handler.setLevel(logging.INFO)
|
||||
```
|
||||
988
mesh_bot.py
988
mesh_bot.py
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
import pickle # pip install pickle
|
||||
from modules.log import *
|
||||
|
||||
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp")
|
||||
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
|
||||
|
||||
# global message list, later we will use a pickle on disk
|
||||
bbs_messages = []
|
||||
@@ -14,19 +14,19 @@ def load_bbsdb():
|
||||
global bbs_messages
|
||||
# load the bbs messages from the database file
|
||||
try:
|
||||
with open('bbsdb.pkl', 'rb') as f:
|
||||
with open('data/bbsdb.pkl', 'rb') as f:
|
||||
bbs_messages = pickle.load(f)
|
||||
except:
|
||||
except Exception as e:
|
||||
bbs_messages = [[1, "Welcome to meshBBS", "Welcome to the BBS, please post a message!",0]]
|
||||
logger.debug("\nSystem: Creating new bbsdb.pkl")
|
||||
with open('bbsdb.pkl', 'wb') as f:
|
||||
logger.debug("System: Creating new data/bbsdb.pkl")
|
||||
with open('data/bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
|
||||
def save_bbsdb():
|
||||
global bbs_messages
|
||||
# save the bbs messages to the database file
|
||||
logger.debug("System: Saving bbsdb.pkl\n")
|
||||
with open('bbsdb.pkl', 'wb') as f:
|
||||
logger.debug("System: Saving data/bbsdb.pkl")
|
||||
with open('data/bbsdb.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_messages, f)
|
||||
|
||||
def bbs_help():
|
||||
@@ -77,6 +77,12 @@ def bbs_post_message(subject, message, fromNode):
|
||||
if str(fromNode) in bbs_ban_list:
|
||||
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {subject}, {message} and was dropped.")
|
||||
return "Message posted. ID is: " + str(messageID)
|
||||
|
||||
# validate not a duplicate message
|
||||
for msg in bbs_messages:
|
||||
if msg[1].strip().lower() == subject.strip().lower() and msg[2].strip().lower() == message.strip().lower():
|
||||
messageID = msg[0]
|
||||
return "Message posted. ID is: " + str(messageID)
|
||||
|
||||
# append the message to the list
|
||||
bbs_messages.append([messageID, subject, message, fromNode])
|
||||
@@ -100,20 +106,20 @@ def bbs_read_message(messageID = 0):
|
||||
def save_bbsdm():
|
||||
global bbs_dm
|
||||
# save the bbs messages to the database file
|
||||
logger.debug("System: Saving Updated BBS Direct Messages bbsdm.pkl")
|
||||
with open('bbsdm.pkl', 'wb') as f:
|
||||
logger.debug("System: Saving Updated BBS Direct Messages data/bbsdm.pkl")
|
||||
with open('data/bbsdm.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_dm, f)
|
||||
|
||||
def load_bbsdm():
|
||||
global bbs_dm
|
||||
# load the bbs messages from the database file
|
||||
try:
|
||||
with open('bbsdm.pkl', 'rb') as f:
|
||||
with open('data/bbsdm.pkl', 'rb') as f:
|
||||
bbs_dm = pickle.load(f)
|
||||
except:
|
||||
bbs_dm = [[1234567890, "Message", 1234567890]]
|
||||
logger.debug("\nSystem: Creating new bbsdm.pkl")
|
||||
with open('bbsdm.pkl', 'wb') as f:
|
||||
logger.debug("System: Creating new data/bbsdm.pkl")
|
||||
with open('data/bbsdm.pkl', 'wb') as f:
|
||||
pickle.dump(bbs_dm, f)
|
||||
|
||||
def bbs_post_dm(toNode, message, fromNode):
|
||||
@@ -130,6 +136,11 @@ def bbs_post_dm(toNode, message, fromNode):
|
||||
save_bbsdm()
|
||||
return "BBS DM Posted for node " + str(toNode)
|
||||
|
||||
def get_bbs_stats():
|
||||
global bbs_messages, bbs_dm
|
||||
# Return some stats on the bbs pending messages and total posted messages
|
||||
return f"📡BBSdb has {len(bbs_messages)} messages.\nDirect ✉️ Messages waiting: {(len(bbs_dm) - 1)}"
|
||||
|
||||
def bbs_check_dm(toNode):
|
||||
global bbs_dm
|
||||
# Check for any messages for toNode
|
||||
@@ -151,6 +162,29 @@ def bbs_delete_dm(toNode, message):
|
||||
return "System: cleared mail for" + str(toNode)
|
||||
return "System: No DM found for node " + str(toNode)
|
||||
|
||||
def bbs_sync_posts(input, peerNode, RxNode):
|
||||
messageID = 0
|
||||
# respond when another bot asks for the bbs posts to sync
|
||||
if "bbslink" in input.lower():
|
||||
if "$" in input and "#" in input:
|
||||
#store the message
|
||||
subject = input.split("$")[1].split("#")[0]
|
||||
body = input.split("#")[1]
|
||||
bbs_post_message(subject, body, peerNode)
|
||||
messageID = input.split(" ")[1]
|
||||
return f"bbsack {messageID}"
|
||||
elif "bbsack" in input.lower():
|
||||
# increment the messageID
|
||||
ack = int(input.split(" ")[1])
|
||||
messageID = int(ack) + 1
|
||||
|
||||
# send message
|
||||
if messageID < len(bbs_messages):
|
||||
return f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]}"
|
||||
else:
|
||||
logger.debug("System: bbslink sync complete with peer " + str(peerNode))
|
||||
|
||||
|
||||
#initialize the bbsdb's
|
||||
load_bbsdb()
|
||||
load_bbsdm()
|
||||
|
||||
32
modules/filemon.py
Normal file
32
modules/filemon.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# File monitor module for the meshing-around bot
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
|
||||
from modules.log import *
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
async def watch_file():
|
||||
def read_file(file_monitor_file_path):
|
||||
try:
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
content = f.read()
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
|
||||
return None
|
||||
|
||||
if not os.path.exists(file_monitor_file_path):
|
||||
return None
|
||||
else:
|
||||
last_modified_time = os.path.getmtime(file_monitor_file_path)
|
||||
while True:
|
||||
current_modified_time = os.path.getmtime(file_monitor_file_path)
|
||||
if current_modified_time != last_modified_time:
|
||||
# File has been modified
|
||||
content = read_file(file_monitor_file_path)
|
||||
last_modified_time = current_modified_time
|
||||
# Cleanup the content
|
||||
content = content.replace('\n', ' ').replace('\r', '').strip()
|
||||
if content:
|
||||
return content
|
||||
await asyncio.sleep(1) # Check every
|
||||
473
modules/games/blackjack.py
Normal file
473
modules/games/blackjack.py
Normal file
@@ -0,0 +1,473 @@
|
||||
# Port of https://github.com/Himan10/BlackJack
|
||||
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
from random import choices, shuffle
|
||||
from modules.log import *
|
||||
import time
|
||||
import pickle
|
||||
|
||||
jack_starting_cash = 100 # Replace 100 with your desired starting cash value
|
||||
jackTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': jack_starting_cash,\
|
||||
'bet': 0, 'gameStats': {'p_win': 0, 'd_win': 0, 'draw': 0}, 'p_cards':[], 'd_cards':[], 'p_hand':[], 'd_hand':[], 'next_card':[]}]
|
||||
|
||||
SUITS = ("♥️", "♦️", "♠️", "♣️")
|
||||
RANKS = (
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"J",
|
||||
"Q",
|
||||
"K",
|
||||
"A",
|
||||
)
|
||||
VALUES = {
|
||||
"2": 2,
|
||||
"3": 3,
|
||||
"4": 4,
|
||||
"5": 5,
|
||||
"6": 6,
|
||||
"7": 7,
|
||||
"8": 8,
|
||||
"9": 9,
|
||||
"10": 10,
|
||||
"J": 10,
|
||||
"Q": 10,
|
||||
"K": 10,
|
||||
"A": 11,
|
||||
}
|
||||
|
||||
class jackCard:
|
||||
def __init__(self, suit, rank):
|
||||
self.suit = suit
|
||||
self.rank = rank
|
||||
|
||||
def __str__(self):
|
||||
return self.rank + " of " + self.suit
|
||||
|
||||
class jackDeck:
|
||||
""" Creating a Deck of cards and Deal two cards to both player and dealer. """
|
||||
|
||||
def __init__(self):
|
||||
self.deck = []
|
||||
self.player = []
|
||||
self.dealer = []
|
||||
for suit in SUITS:
|
||||
for rank in RANKS:
|
||||
self.deck.append((suit, rank))
|
||||
|
||||
def shuffle(self):
|
||||
shuffle(self.deck)
|
||||
|
||||
def deal_cards(self):
|
||||
self.player = choices(self.deck, k=2)
|
||||
self.delete_cards(self.player)
|
||||
self.dealer = choices(self.deck, k=2)
|
||||
self.delete_cards(self.dealer) # Delete Drawn Cards
|
||||
return self.player, self.dealer
|
||||
|
||||
def delete_cards(self, total_drawn):
|
||||
""" Delete Drawn cards from the Decks """
|
||||
try:
|
||||
for i in total_drawn:
|
||||
self.deck.remove(i)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
class jackHand:
|
||||
""" Adding the values of player/dealer cards and change the values of Aces acc. to situation. """
|
||||
def __init__(self):
|
||||
self.cards = []
|
||||
self.value = 0
|
||||
self.aces = 0
|
||||
|
||||
def add_cards(self, card):
|
||||
self.cards.extend(card)
|
||||
for count, ele in enumerate(card, 0):
|
||||
if ele[1] == "A":
|
||||
self.aces += 1
|
||||
self.value += VALUES[ele[1]]
|
||||
self.adjust_for_ace()
|
||||
|
||||
def adjust_for_ace(self):
|
||||
while self.aces > 0 and self.value > 21:
|
||||
self.value -= 10
|
||||
self.aces -= 1
|
||||
|
||||
class jackChips:
|
||||
""" Player/dealer chips for making bets and Adding/Deducting amount in/from Player's total. """
|
||||
def __init__(self):
|
||||
self.total = jack_starting_cash
|
||||
self.bet = 0
|
||||
self.winnings = 0
|
||||
|
||||
def win_bet(self):
|
||||
self.total += self.bet
|
||||
self.winnings += 1
|
||||
|
||||
def loss_bet(self):
|
||||
self.total -= self.bet
|
||||
self.winnings -= 1
|
||||
|
||||
def success_rate(card, obj_h):
|
||||
""" Calculate Success rate of 'HIT' new cards """
|
||||
msg = ""
|
||||
rate = 0
|
||||
diff = 21 - obj_h.value
|
||||
if diff != 0:
|
||||
rate = (VALUES[card[0][1]] / diff) * 100
|
||||
|
||||
if rate < 100:
|
||||
msg += f"If Hit, chance {int(rate)}% failure, {100-int(rate)}% success."
|
||||
else:
|
||||
l_rate = int(rate - (rate - 99)) # Round to 99
|
||||
if card[0][1] == "A":
|
||||
l_rate -= 99
|
||||
msg += f"If Hit, chance {100-l_rate}% failure, and {l_rate}% success"
|
||||
return msg
|
||||
|
||||
def hits(obj_de):
|
||||
new_card = [obj_de.deal_cards()[0][0]]
|
||||
# obj_h.add_cards(new_card)
|
||||
return new_card
|
||||
|
||||
def display_hand(hand):
|
||||
# Display the cards in the hand nicely
|
||||
d = "" # display
|
||||
for card in hand:
|
||||
d += f"{card[1]}{card[0]}"
|
||||
if card != hand[-1]:
|
||||
d += ", "
|
||||
return d
|
||||
|
||||
def show_some(player_cards, dealer_cards, obj_h):
|
||||
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
|
||||
msg += f"Dealer[{VALUES[dealer_cards[1][1]]}] {dealer_cards[1][1]}{dealer_cards[1][0]} "
|
||||
return msg
|
||||
|
||||
def show_all(player_cards, dealer_cards, obj_h, obj_d):
|
||||
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
|
||||
msg += f"Dealer[{obj_d.value}] {display_hand(dealer_cards)}"
|
||||
return msg
|
||||
|
||||
def player_bust(obj_h, obj_c):
|
||||
if obj_h.value > 21:
|
||||
obj_c.loss_bet()
|
||||
return True
|
||||
return False
|
||||
|
||||
def player_wins(obj_h, obj_d, obj_c):
|
||||
if any((obj_h.value == 21, obj_h.value > obj_d.value and obj_h.value < 21)):
|
||||
obj_c.win_bet()
|
||||
return True
|
||||
return False
|
||||
|
||||
def dealer_bust(obj_d, obj_h, obj_c):
|
||||
if obj_d.value > 21:
|
||||
if obj_h.value < 21:
|
||||
obj_c.win_bet()
|
||||
return True
|
||||
return False
|
||||
|
||||
def dealer_wins(obj_h, obj_d, obj_c):
|
||||
if any((obj_d.value == 21, obj_d.value > obj_h.value and obj_d.value < 21)):
|
||||
obj_c.loss_bet()
|
||||
return True
|
||||
return False
|
||||
|
||||
def push(obj_h, obj_d):
|
||||
if obj_h.value == obj_d.value:
|
||||
return True
|
||||
return False
|
||||
|
||||
def player_surrender(obj_c):
|
||||
obj_c.loss_bet()
|
||||
return True
|
||||
|
||||
def gameStats(p_count, d_count, draw_c):
|
||||
msg = f"\n📊🏆P:{p_count},D:{d_count},T:{draw_c}"
|
||||
return msg
|
||||
|
||||
def getLastCmdJack(nodeID):
|
||||
for i in range(len(jackTracker)):
|
||||
if jackTracker[i]['nodeID'] == nodeID:
|
||||
return jackTracker[i]['cmd']
|
||||
return None
|
||||
|
||||
def setLastCmdJack(nodeID, cmd):
|
||||
for i in range(len(jackTracker)):
|
||||
if jackTracker[i]['nodeID'] == nodeID:
|
||||
jackTracker[i]['cmd'] = cmd
|
||||
return True
|
||||
return False
|
||||
|
||||
def saveHSJack(nodeID, highScore):
|
||||
# Save the game state to pickle
|
||||
highScore = {'nodeID': nodeID, 'highScore': highScore}
|
||||
try:
|
||||
with open('data/blackjack_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: BlackJack: Creating new data/blackjack_hs.pkl file")
|
||||
with open('data/blackjack_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
|
||||
def loadHSJack():
|
||||
try:
|
||||
with open('data/blackjack_hs.pkl', 'rb') as file:
|
||||
highScore = pickle.load(file)
|
||||
return highScore
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: BlackJack: Creating new data/blackjack_hs.pkl file")
|
||||
highScore = {'nodeID': 0, 'highScore': 0}
|
||||
with open('data/blackjack_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
return 0
|
||||
|
||||
def playBlackJack(nodeID, message):
|
||||
# Initalize the Game
|
||||
msg, last_cmd = '', None
|
||||
blackJack = False
|
||||
p_win, d_win, draw = 0, 0, 0
|
||||
p_chips = jackChips()
|
||||
p_hand = jackHand()
|
||||
d_hand = jackHand()
|
||||
p_cards, d_cards = [], []
|
||||
bet_money = 0
|
||||
# Initalize the Cards
|
||||
cards_deck = jackDeck()
|
||||
cards_deck.shuffle()
|
||||
p_cards, d_cards = cards_deck.deal_cards()
|
||||
# Deal the cards to player and dealer
|
||||
p_hand.add_cards(p_cards)
|
||||
d_hand.add_cards(d_cards)
|
||||
next_card = hits(cards_deck)
|
||||
|
||||
# Check if player, use tracking
|
||||
for i in range(len(jackTracker)):
|
||||
if jackTracker[i]['nodeID'] == nodeID:
|
||||
last_cmd = jackTracker[i]['cmd']
|
||||
p_chips.total = jackTracker[i]['cash']
|
||||
p_win = jackTracker[i]['gameStats']['p_win']
|
||||
d_win = jackTracker[i]['gameStats']['d_win']
|
||||
draw = jackTracker[i]['gameStats']['draw']
|
||||
bet_money = jackTracker[i]['bet']
|
||||
if last_cmd == "playing":
|
||||
p_chips.bet = bet_money
|
||||
p_cards = jackTracker[i]['p_cards']
|
||||
d_cards = jackTracker[i]['d_cards']
|
||||
p_hand = jackTracker[i]['p_hand']
|
||||
d_hand = jackTracker[i]['d_hand']
|
||||
next_card = jackTracker[i]['next_card']
|
||||
|
||||
if last_cmd is None:
|
||||
# create new player if not in tracker
|
||||
logger.debug(f"System: BlackJack: New Player {nodeID}")
|
||||
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': jack_starting_cash,\
|
||||
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
|
||||
return f"Welcome to ♠️♥️BlackJack♣️♦️ you have {p_chips.total} chips. Whats your bet?"
|
||||
|
||||
if getLastCmdJack(nodeID) == "new":
|
||||
# Place Bet
|
||||
try:
|
||||
# handle B letter
|
||||
if message.lower() == "b":
|
||||
if bet_money == 0:
|
||||
bet_money = 5
|
||||
elif message.lower() == "r":
|
||||
#resend the hand
|
||||
msg += show_some(p_cards, d_cards, p_hand)
|
||||
return msg
|
||||
else:
|
||||
try:
|
||||
bet_money = int(message)
|
||||
except ValueError:
|
||||
return "Invalid Bet, please enter a valid number."
|
||||
|
||||
if bet_money <= p_chips.total and bet_money >= 1:
|
||||
p_chips.bet = bet_money
|
||||
else:
|
||||
return f"Invalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
|
||||
except ValueError:
|
||||
return f"Invalid Bet, the maximum bet, {p_chips.total}"
|
||||
|
||||
# Show the cards
|
||||
msg += show_some(p_cards, d_cards, p_hand)
|
||||
# check for blackjack 21 and only two cards
|
||||
if p_hand.value == 21 and len(p_hand.cards) == 2:
|
||||
msg += "Player 🎰 BLAAAACKJACKKKK 💰"
|
||||
p_chips.total += round(p_chips.bet * 1.5)
|
||||
setLastCmdJack(nodeID, "dealerTurn")
|
||||
blackJack = True
|
||||
# Save the game state
|
||||
for i in range(len(jackTracker)):
|
||||
if jackTracker[i]['nodeID'] == nodeID:
|
||||
jackTracker[i]['cash'] = int(p_chips.total)
|
||||
break
|
||||
else:
|
||||
# Display the statistics
|
||||
stats = success_rate(next_card, p_hand)
|
||||
msg += stats
|
||||
setLastCmdJack(nodeID, "betPlaced")
|
||||
|
||||
if getLastCmdJack(nodeID) == "betPlaced":
|
||||
setLastCmdJack(nodeID, "playing")
|
||||
msg += "(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
|
||||
|
||||
# save the game state
|
||||
for i in range(len(jackTracker)):
|
||||
if jackTracker[i]['nodeID'] == nodeID:
|
||||
jackTracker[i]['cash'] = p_chips.total
|
||||
jackTracker[i]['bet'] = p_chips.bet
|
||||
jackTracker[i]['p_cards'] = p_cards
|
||||
jackTracker[i]['d_cards'] = d_cards
|
||||
jackTracker[i]['p_hand'] = p_hand
|
||||
jackTracker[i]['d_hand'] = d_hand
|
||||
jackTracker[i]['next_card'] = next_card
|
||||
return msg
|
||||
|
||||
|
||||
while getLastCmdJack(nodeID) == "playing": # Recall var. from hit and stand function
|
||||
next_card = hits(cards_deck)
|
||||
|
||||
# Get the statistics
|
||||
stats = success_rate(next_card, p_hand)
|
||||
|
||||
# Player's Turn
|
||||
choice = message.lower()
|
||||
|
||||
if choice == "hit" or choice == "h":
|
||||
# hits(obj_de, p_hand)
|
||||
p_hand.add_cards(next_card)
|
||||
msg += show_some(p_hand.cards, d_cards, p_hand)
|
||||
elif choice == "stand" or choice == "s":
|
||||
setLastCmdJack(nodeID, "dealerTurn")
|
||||
elif choice == "forfit" or choice == "f":
|
||||
p_chips.bet = p_chips.bet / 2
|
||||
setLastCmdJack(nodeID, "dealerTurn")
|
||||
p_hand.value += 21
|
||||
elif choice == "double" or choice == "d":
|
||||
if p_chips.bet * 2 <= p_chips.total:
|
||||
p_chips.bet *= 2
|
||||
next_d_card = hits(cards_deck)
|
||||
p_hand.add_cards(next_d_card)
|
||||
setLastCmdJack(nodeID, "dealerTurn")
|
||||
else:
|
||||
return "You can't Double Down, dont have enough chips"
|
||||
elif choice == "resend" or choice == "r":
|
||||
msg += show_some(p_hand.cards, d_cards, p_hand)
|
||||
else:
|
||||
return "(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
|
||||
|
||||
# Check if player bust
|
||||
if player_bust(p_hand, p_chips):
|
||||
d_win += 1
|
||||
msg += "💥PlayerBUST💥"
|
||||
setLastCmdJack(nodeID, "dealerTurn")
|
||||
|
||||
if getLastCmdJack(nodeID) == "playing":
|
||||
msg += stats
|
||||
msg += "[H,S,F,D]"
|
||||
|
||||
# Save the game state
|
||||
for i in range(len(jackTracker)):
|
||||
if jackTracker[i]['nodeID'] == nodeID:
|
||||
jackTracker[i]['cash'] = int(p_chips.total)
|
||||
jackTracker[i]['bet'] = int(p_chips.bet)
|
||||
jackTracker[i]['gameStats']['p_win'] = int(p_win)
|
||||
jackTracker[i]['gameStats']['d_win'] = int(d_win)
|
||||
jackTracker[i]['gameStats']['draw'] = int(draw)
|
||||
jackTracker[i]['p_cards'] = p_cards
|
||||
jackTracker[i]['d_cards'] = d_cards
|
||||
jackTracker[i]['p_hand'] = p_hand
|
||||
jackTracker[i]['d_hand'] = d_hand
|
||||
break
|
||||
|
||||
# Exit player's turn
|
||||
if getLastCmdJack(nodeID) == "dealerTurn":
|
||||
break
|
||||
|
||||
return msg
|
||||
|
||||
if getLastCmdJack(nodeID) == "dealerTurn":
|
||||
# Dealers Turn
|
||||
if not blackJack:
|
||||
# recall the game state
|
||||
for i in range(len(jackTracker)):
|
||||
if jackTracker[i]['nodeID'] == nodeID:
|
||||
p_chips.total = jackTracker[i]['cash']
|
||||
p_chips.bet = jackTracker[i]['bet']
|
||||
p_win = jackTracker[i]['gameStats']['p_win']
|
||||
d_win = jackTracker[i]['gameStats']['d_win']
|
||||
draw = jackTracker[i]['gameStats']['draw']
|
||||
p_cards = jackTracker[i]['p_cards']
|
||||
d_cards = jackTracker[i]['d_cards']
|
||||
p_hand = jackTracker[i]['p_hand']
|
||||
d_hand = jackTracker[i]['d_hand']
|
||||
next_card = jackTracker[i]['next_card']
|
||||
break
|
||||
|
||||
if p_hand.value <= 21:
|
||||
# Dealer's Turn
|
||||
while d_hand.value < 17:
|
||||
d_card = hits(cards_deck)
|
||||
d_hand.add_cards(d_card)
|
||||
if dealer_bust(d_hand, p_hand, p_chips):
|
||||
p_win += 1
|
||||
msg += "💰DealerBUST💥"
|
||||
break
|
||||
# Show all cards
|
||||
msg += show_all(p_hand.cards, d_hand.cards, p_hand, d_hand)
|
||||
|
||||
# Check who wins
|
||||
if push(p_hand, d_hand):
|
||||
draw += 1
|
||||
msg += "👌PUSH"
|
||||
elif player_wins(p_hand, d_hand, p_chips):
|
||||
p_win += 1
|
||||
msg += "🎉PLAYER WINS🎰"
|
||||
elif dealer_wins(p_hand, d_hand, p_chips):
|
||||
d_win += 1
|
||||
msg += "👎DEALER WINS"
|
||||
else:
|
||||
msg += "👎DEALER WINS"
|
||||
|
||||
# Display the Game Stats
|
||||
msg += gameStats(str(p_win), str(d_win), str(draw))
|
||||
|
||||
# Display the chips left
|
||||
if p_chips.total < 1:
|
||||
if p_chips.total > 0:
|
||||
msg += "🪙Keep the change you filthy animal!"
|
||||
else:
|
||||
msg += "💸NO MORE CHIPS!🏧💳"
|
||||
p_chips.total = jack_starting_cash
|
||||
else:
|
||||
# check high score
|
||||
highScore = loadHSJack()
|
||||
if highScore != 0 and p_chips.total > highScore['highScore']:
|
||||
msg += f"💰HighScore💰{p_chips.total} "
|
||||
saveHSJack(nodeID, p_chips.total)
|
||||
else:
|
||||
msg += f"💰You have {p_chips.total} chips "
|
||||
|
||||
msg += " Bet or Leave?"
|
||||
|
||||
# Reset the game
|
||||
setLastCmdJack(nodeID, "new")
|
||||
jackTracker[i]['cash'] = p_chips.total
|
||||
jackTracker[i]['gameStats']['p_win'] = p_win
|
||||
jackTracker[i]['gameStats']['d_win'] = d_win
|
||||
jackTracker[i]['gameStats']['draw'] = draw
|
||||
jackTracker[i]['p_cards'] = []
|
||||
jackTracker[i]['d_cards'] = []
|
||||
jackTracker[i]['p_hand'] = []
|
||||
jackTracker[i]['d_hand'] = []
|
||||
jackTracker[i]['time'] = time.time()
|
||||
|
||||
return msg
|
||||
686
modules/games/dopewar.py
Normal file
686
modules/games/dopewar.py
Normal file
@@ -0,0 +1,686 @@
|
||||
# Port of https://github.com/Reconfirefly/drugwars/tree/master
|
||||
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
import random
|
||||
import time
|
||||
import pickle
|
||||
from modules.log import *
|
||||
|
||||
# Global variables
|
||||
total_days = 7 # number of days or rotations the player has to play
|
||||
starting_cash = 5000
|
||||
# Database for the game reset on boot
|
||||
dwInventoryDb = [{'userID': 1234567890, 'inventory': 0, 'priceList': [], 'amount': []}]
|
||||
dwCashDb = [{'userID': 1234567890, 'cash': starting_cash},]
|
||||
dwGameDayDb = [{'userID': 1234567890, 'day': 0},]
|
||||
dwLocationDb = [{'userID': 1234567890, 'location': 'USA', 'loc_choice': 0},]
|
||||
dwPlayerTracker = [{'userID': 1234567890, 'last_played': time.time(), 'cmd': 'start'},]
|
||||
# high score is saved in a pickle file
|
||||
dwHighScore = {}
|
||||
|
||||
class Drugs:
|
||||
|
||||
def __init__(self, name, price_range):
|
||||
self.name = name
|
||||
self.price_range = price_range
|
||||
self.price_check()
|
||||
|
||||
def price_check(self):
|
||||
# the * is to unpack the touple of values that the random goes between
|
||||
self.price = random.randint(*self.price_range)
|
||||
# print("the price of " + self.name + " is " + str(self.price))
|
||||
return self.price
|
||||
|
||||
class Events:
|
||||
|
||||
def __init__(self, name, text, price_range):
|
||||
self.name = name
|
||||
self.price_range = price_range
|
||||
self.text = text
|
||||
self.price_mod()
|
||||
|
||||
def price_mod(self):
|
||||
self.price = random.randint(*self.price_range)
|
||||
return self.price
|
||||
|
||||
my_drugs = [
|
||||
# Drugs("Name", (min price, max price), amount)
|
||||
Drugs("Cocaine", (15000, 28000)),
|
||||
Drugs("Heroin", (2000, 10000)),
|
||||
Drugs("Weed", (300, 1000)),
|
||||
Drugs("Hash", (200, 1200)),
|
||||
Drugs("Opium", (400, 1800)),
|
||||
Drugs("Acid", (1000, 4200)),
|
||||
Drugs("Ludes", (18, 75)),
|
||||
]
|
||||
|
||||
event_list = [
|
||||
# Events("Name", "Text", (min price, max price))
|
||||
Events("Cocaine", 'El Chapo Arrested! 🚔 Coke price thru the roof! 📈', (40000, 110000)),
|
||||
Events("Heroin", 'Trump cracks down on opiates! Heroin in high demand by addicts📈', (9000, 25000)),
|
||||
Events("Weed", 'The DEA has fully legalized weed! Prices are at an all time low!📉', (50, 400)),
|
||||
Events("Hash", 'Ricky\'s hash driveway burned down! 🚒 Look at the price boys!📈', (800, 2000)),
|
||||
Events("Opium", 'Shenzhen 深圳 Opium 鸦片 Den 塔 was raided! 🚔 Street price is popping off!📈', (1800, 6000)),
|
||||
Events("Acid", 'The Grateful Dead are on tour! Acid prices are skyrocketing!📈', (5000, 15000)),
|
||||
Events("Ludes", 'The Wolf of Wall Street is back! Ludes are in demand!', (100, 300)),
|
||||
Events("Cocaine", "The Biden administration has legalized cocaine! Prices are at an all time low!📉", (3000, 10000)),
|
||||
Events("Heroin", "Oregon has legalized heroin! Prices are at an all time low!📉", (500, 2500)),
|
||||
Events("Weed", "Prices are at an all time HIGH!📈", (1000, 5000)),
|
||||
Events("Hash", "The Middle East has legalised hash! Prices are at an all time low!📉", (50, 1000)),
|
||||
Events("Opium", "The Sackler's flood the market with cheap opium! Prices are at an all time low!📉", (300, 900)),
|
||||
Events("Acid", "The FBI admits to dosing the water supply with LSD! Acid at an all time low!📉", (500, 2000)),
|
||||
Events("Ludes", "The FDA approves ludes for sale! Prices are at an all time low!📉", (3, 45))
|
||||
]
|
||||
|
||||
def generatelocations():
|
||||
# dictionary of locations
|
||||
locs = {'Canada': ('Red Deer', 'Edmonton', 'Calgary', 'Toronto', 'Vancouver', 'St. Johns'),
|
||||
'USA': ('L.A.', 'NYC', 'Chicago', 'Miami', 'Houston', 'Phoenix'), 'Mexico': ('Tijuana', 'Mexico City', 'Cancun', 'Juarez', 'Acapulco', 'Guadalajara'),\
|
||||
'South America': ('Bogota', 'Caracas', 'Lima', 'Santiago', 'Buenos Aires', 'Rio'), 'Europe': ('London', 'Paris', 'Berlin', 'Rome', 'Madrid', 'Moscow')}
|
||||
|
||||
country = list(locs.keys())
|
||||
country = country[random.randint(0, len(country)-1)]
|
||||
|
||||
# return the location list for the user's country
|
||||
location = []
|
||||
for i in range(len(locs[country])):
|
||||
location.append(locs[country][i])
|
||||
return location
|
||||
|
||||
def generate_event():
|
||||
# roll to see if an event happens
|
||||
event_choice = random.randint(0, len(event_list)-1)
|
||||
if random.randint(0, 100) > 35:
|
||||
return event_choice
|
||||
else:
|
||||
return -1
|
||||
|
||||
def officer(nodeID):
|
||||
global dwCashDb, dwInventoryDb
|
||||
|
||||
# get the inventory for the user
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
inventory = dwInventoryDb[i].get('inventory')
|
||||
amount = check_inv(nodeID)
|
||||
|
||||
# get the cash for the user
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == nodeID:
|
||||
cash = dwCashDb[i].get('cash')
|
||||
|
||||
# rolls to see if the officer takes drugs from you
|
||||
if random.randint(0, 100) > 65: # confiscation chance is 35%
|
||||
j, k = 0, 0
|
||||
for i in range(0, len(my_drugs)):
|
||||
j = amount[i]
|
||||
amount[i] = 0
|
||||
k += j
|
||||
# set the cash_taken to conf for confiscation not of cash
|
||||
cash_taken = 'conf'
|
||||
# Update the inventory_db
|
||||
inventory -= k
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
dwInventoryDb[i]['inventory'] = inventory
|
||||
amount = dwInventoryDb[i].get('amount')
|
||||
return cash_taken
|
||||
# rolls to see how much cash the officer takes
|
||||
cash_taken = random.randint(1, cash-1)
|
||||
cash -= cash_taken
|
||||
# Update the cash_db and inventory_db
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == nodeID:
|
||||
dwCashDb[i]['cash'] = cash
|
||||
return cash_taken
|
||||
|
||||
def get_found_items(nodeID):
|
||||
global dwInventoryDb, dwCashDb
|
||||
msg = ''
|
||||
# get the inventory for the user
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
inventory = dwInventoryDb[i].get('inventory')
|
||||
amount = check_inv(nodeID)
|
||||
|
||||
# get the cash for the user
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == nodeID:
|
||||
cash = dwCashDb[i].get('cash')
|
||||
|
||||
if random.randint(0, 100) > 50: # 50% chance to find cash or drugs
|
||||
if random.randint(0, 100) > 30: # 30% chance to find drugs
|
||||
found = random.choice(range(len(my_drugs)))
|
||||
# rolls to see how much of the drug the user finds
|
||||
qty =random.randint(1, 80 - inventory)
|
||||
amount[found] += qty
|
||||
inventory += qty
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
dwInventoryDb[i]['inventory'] = inventory
|
||||
dwInventoryDb[i]['amount'] = amount
|
||||
msg = "💊You found " + str(qty) + " of " + str(my_drugs[found])
|
||||
else:
|
||||
# rolls to see how much cash the user finds
|
||||
cash_found = random.randint(1, 977)
|
||||
cash += cash_found
|
||||
# Update the cash_db
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == nodeID:
|
||||
dwCashDb[i]['cash'] = cash
|
||||
msg = "You found $" + str(cash_found) + "💸"
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def price_change(event_number):
|
||||
price_list = []
|
||||
for i in range(0, len(my_drugs)):
|
||||
j = my_drugs[i]
|
||||
k = j.price_check()
|
||||
price_list.append(k)
|
||||
|
||||
# check if IndexError will be thrown and find a new event_number with generate_event
|
||||
while event_number > len(price_list)-1:
|
||||
event_number = generate_event()
|
||||
|
||||
if event_number != -1:
|
||||
price_list[event_number] = event_list[event_number].price_mod()
|
||||
|
||||
return price_list
|
||||
|
||||
def check_inv(nodeID):
|
||||
global dwInventoryDb
|
||||
|
||||
# get the inventory ammount for the user
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
amount = dwInventoryDb[i].get('amount')
|
||||
|
||||
# if ammount is empty list initialize it
|
||||
if not amount:
|
||||
amount = []
|
||||
for i in range(0, len(my_drugs)):
|
||||
amount.append(0,)
|
||||
|
||||
# save the amount to the inventory_db
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
dwInventoryDb[i]['amount'] = amount
|
||||
|
||||
return amount
|
||||
|
||||
def buy_func(nodeID, price_list, choice=0, value='0'):
|
||||
global dwCashDb, dwInventoryDb, dwPlayerTracker
|
||||
msg = ''
|
||||
|
||||
# get the inventory for the user
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
inventory = dwInventoryDb[i].get('inventory')
|
||||
amount = check_inv(nodeID)
|
||||
|
||||
# get the cash for the user
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == nodeID:
|
||||
cash = dwCashDb[i].get('cash')
|
||||
|
||||
drug_choice = choice
|
||||
if choice == 0:
|
||||
msg = f"Didnt see a drug chouce. ex: s,1,10 sells 10 of drug 1{my_drugs[1].name}, or p for price list"
|
||||
return msg
|
||||
else:
|
||||
if drug_choice in range(1, len(my_drugs) + 1):
|
||||
drug_choice = drug_choice - 1
|
||||
cost = price_list[drug_choice]
|
||||
msg = my_drugs[drug_choice].name + ": you have🎒 " + str(amount[drug_choice]) + " "
|
||||
msg += " The going price is: $" + "{:,}".format(cost) + " "
|
||||
|
||||
buy_amount = value
|
||||
if buy_amount == 'm':
|
||||
buy_amount = cash // price_list[drug_choice]
|
||||
if buy_amount > 100 - inventory:
|
||||
buy_amount = 100 - inventory
|
||||
# set the buy amount to the max if the user enters m
|
||||
buy_amount = int(buy_amount)
|
||||
|
||||
if buy_amount == 0:
|
||||
msg = f"Didnt see a qty. ex: b,1,10 buys 10 of {my_drugs[1].name}, can also use m for max"
|
||||
return msg
|
||||
elif buy_amount not in range(1, 101):
|
||||
msg = "Enter qty or m for max"
|
||||
return msg
|
||||
elif buy_amount > 100 - inventory:
|
||||
msg = "You don\'t have enough space for all that.🎒"
|
||||
return msg
|
||||
elif buy_amount * price_list[drug_choice] <= cash:
|
||||
amount[drug_choice] += buy_amount
|
||||
cash -= buy_amount * price_list[drug_choice]
|
||||
inventory += buy_amount
|
||||
msg += "You bought " + str(buy_amount) + " " + my_drugs[drug_choice].name + '. Remaining cash: $' + str(cash)
|
||||
msg += f"\nBuy💸, Sell💰, Fly🛫?"
|
||||
else:
|
||||
msg = "You don't have enough cash!😭"
|
||||
return msg
|
||||
|
||||
# update the cash_db and inventory_db values
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == nodeID:
|
||||
dwCashDb[i]['cash'] = cash
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
dwInventoryDb[i]['inventory'] = inventory
|
||||
# save the last command as ask_bsf
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def sell_func(nodeID, price_list, choice=0, value='0'):
|
||||
global dwCashDb, dwInventoryDb, dwPlayerTracker
|
||||
msg = ''
|
||||
|
||||
# get the inventory for the user
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
inventory = dwInventoryDb[i].get('inventory')
|
||||
amount = check_inv(nodeID)
|
||||
|
||||
# get the cash for the user
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == nodeID:
|
||||
cash = dwCashDb[i].get('cash')
|
||||
|
||||
# get the drug choice and amount to sell
|
||||
drug_choice = choice
|
||||
sell_amount = value
|
||||
|
||||
try:
|
||||
if sell_amount == 'm':
|
||||
sell_amount = amount[drug_choice - 1]
|
||||
else:
|
||||
sell_amount = int(sell_amount)
|
||||
if sell_amount not in range(1, 101):
|
||||
msg = "You can only sell between 1 and 100"
|
||||
return msg
|
||||
except ValueError:
|
||||
msg = "Enter qty or m for max"
|
||||
return msg
|
||||
|
||||
# check if the user has any of the drug they are trying to sell
|
||||
if choice == 0:
|
||||
msg = "Enter b or s and the drug number and qty you want to buy or sell. ex: b,1,10 buys 10 of drug 1"
|
||||
return msg
|
||||
else:
|
||||
if drug_choice in range(1, len(my_drugs) + 1) and amount[drug_choice - 1] > 0:
|
||||
drug_choice = drug_choice - 1
|
||||
cost = price_list[drug_choice]
|
||||
msg = my_drugs[drug_choice].name + ": you have " + str(amount[drug_choice]) +\
|
||||
" The going price is: $" + str("{:,}".format(cost))
|
||||
# check if the user has enough of the drug to sell
|
||||
if sell_amount <= amount[drug_choice]:
|
||||
amount[drug_choice] -= sell_amount
|
||||
cash += sell_amount * price_list[drug_choice]
|
||||
inventory -= sell_amount
|
||||
profit = sell_amount * price_list[drug_choice]
|
||||
msg += " You sold " + str(sell_amount) + " " + my_drugs[drug_choice].name +\
|
||||
' for $' + "{:,}".format(profit) + '. Total cash: $' + "{:,}".format(cash)
|
||||
else:
|
||||
msg = "You don't have that much"
|
||||
return msg
|
||||
else:
|
||||
msg = "You don't have any " + my_drugs[drug_choice - 1].name
|
||||
return msg
|
||||
|
||||
# update the cash_db and inventory_db values
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == nodeID:
|
||||
dwCashDb[i]['cash'] = cash
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
dwInventoryDb[i]['inventory'] = inventory
|
||||
dwInventoryDb[i]['amount'] = amount
|
||||
# save the last command as ask_bsf
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
|
||||
|
||||
return msg
|
||||
|
||||
def get_location_table(nodeID, choice=0):
|
||||
global dwLocationDb
|
||||
# get the location for the user
|
||||
for i in range(0, len(dwLocationDb)):
|
||||
if dwLocationDb[i].get('userID') == nodeID:
|
||||
loc = dwLocationDb[i].get('location')
|
||||
|
||||
# list the lcaitons and their index in two columns
|
||||
loc_table_string = ''
|
||||
for i in range(len(loc)):
|
||||
loc_table_string += str(i+1) + '. ' + loc[i] + ' '
|
||||
loc_table_string += ' Where do you want to 🛫?#'
|
||||
return loc_table_string
|
||||
|
||||
def endGameDw(nodeID):
|
||||
global dwCashDb, dwInventoryDb, dwLocationDb, dwGameDayDb, dwHighScore
|
||||
msg = ''
|
||||
dwHighScore = getHighScoreDw()
|
||||
# Confirm the cash for the user
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == nodeID:
|
||||
cash = dwCashDb[i].get('cash')
|
||||
logger.debug("System: DopeWars: Game Over for user: " + str(nodeID) + " with cash: " + str(cash))
|
||||
|
||||
# remove the player from the game databases
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == nodeID:
|
||||
dwCashDb.pop(i)
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
dwInventoryDb.pop(i)
|
||||
for i in range(0, len(dwLocationDb)):
|
||||
if dwLocationDb[i].get('userID') == nodeID:
|
||||
dwLocationDb.pop(i)
|
||||
for i in range(0, len(dwGameDayDb)):
|
||||
if dwGameDayDb[i].get('userID') == nodeID:
|
||||
dwGameDayDb.pop(i)
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker.pop(i)
|
||||
|
||||
# checks if the player's score is higher than the high score and writes a new high score if it is
|
||||
if cash > dwHighScore.get('cash'):
|
||||
dwHighScore = ({'userID': nodeID, 'cash': round(cash, 2)})
|
||||
with open('data/dopewar_hs.pkl', 'wb') as file:
|
||||
pickle.dump(dwHighScore, file)
|
||||
msg = "You finished with $" + "{:,}".format(cash) + " and beat the high score!🎉💰"
|
||||
return msg
|
||||
if cash > starting_cash:
|
||||
msg = 'You made money! 💵 Up ' + str((cash/starting_cash).__round__()) + 'x! Well done.'
|
||||
return msg
|
||||
if cash == starting_cash:
|
||||
msg = 'You broke even... hope you at least had fun 💉💊'
|
||||
return msg
|
||||
if cash < starting_cash:
|
||||
msg = "You lost money, better go get a real job.💸"
|
||||
|
||||
return msg
|
||||
|
||||
def getHighScoreDw():
|
||||
global dwHighScore
|
||||
# Load high score table
|
||||
try:
|
||||
with open('data/dopewar_hs.pkl', 'rb') as file:
|
||||
dwHighScore = pickle.load(file)
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: DopeWars: No high score table found")
|
||||
# high score pickle file is a touple of the nodeID and the high score
|
||||
dwHighScore = ({"userID": 4258675309, "cash": 100})
|
||||
# write a new high score file if one is not found
|
||||
with open('data/dopewar_hs.pkl', 'wb') as file:
|
||||
pickle.dump(dwHighScore, file)
|
||||
return dwHighScore
|
||||
|
||||
def render_game_screen(userID, day_play, total_day, loc_choice, event_number, price_list, cash_stolen, found_items):
|
||||
global dwCashDb, dwInventoryDb, dwLocationDb
|
||||
msg = ''
|
||||
# get the location for the user
|
||||
for i in range(0, len(dwLocationDb)):
|
||||
if dwLocationDb[i].get('userID') == userID:
|
||||
loc = dwLocationDb[i].get('location')
|
||||
|
||||
if event_number != -1:
|
||||
msg += event_list[event_number].text + f"\n"
|
||||
elif event_number == -1 and cash_stolen != 0 and cash_stolen != 'conf':
|
||||
msg += random.choice([f"You got high and spent ${str(cash_stolen)}💊💸\n",
|
||||
f"You got mugged and lost ${str(cash_stolen)}💸🔫\n",
|
||||
f"You got a new tattoo and spent ${str(cash_stolen)}💉💸\n",])
|
||||
elif event_number == -1 and cash_stolen == 'conf':
|
||||
msg += f"🚔Officer Bob stopped you and took all of your drugs.🚭\n"
|
||||
elif event_number == -1 and found_items != 'nothing':
|
||||
msg += found_items + f"\n"
|
||||
|
||||
# get the inventory for the user
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == userID:
|
||||
inventory = dwInventoryDb[i].get('inventory')
|
||||
|
||||
amount = check_inv(userID)
|
||||
# get the cash for the user
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == userID:
|
||||
cash = dwCashDb[i].get('cash')
|
||||
|
||||
msg += "🗺️" + loc[int(loc_choice) - 1] + " 📆" + str(day_play) + '/' + str(total_day) + " 🎒" + str(inventory) + "/100" + " 💵" + "{:,}".format(cash) + f"\n"
|
||||
|
||||
for i, drug in enumerate(my_drugs, 1):
|
||||
qty = amount[i-1]
|
||||
msg += f'#{str(i)}.{drug.name}${"{:,}".format(price_list[i-1])}({qty}) '
|
||||
|
||||
return msg
|
||||
|
||||
def dopeWarGameDay(nodeID, day_play, total_day):
|
||||
global dwCashDb, dwLocationDb, dwInventoryDb
|
||||
cash_stolen = 0
|
||||
found_items = 'nothing'
|
||||
|
||||
# roll for the event of the day
|
||||
event_number = generate_event()
|
||||
|
||||
# get the location for the user
|
||||
for i in range(0, len(dwLocationDb)):
|
||||
if dwLocationDb[i].get('userID') == nodeID:
|
||||
loc = dwLocationDb[i].get('location')
|
||||
loc_choice = dwLocationDb[i].get('loc_choice')
|
||||
|
||||
# rolls to see if event happens
|
||||
if event_number == -1 and random.randint(0, 100) > 80: # 20% chance to have an event
|
||||
if random.randint(0, 100) > 50: # 50% chance to have an officer encounter
|
||||
cash_stolen = officer(nodeID)
|
||||
else:
|
||||
# find items
|
||||
found_items = get_found_items(nodeID)
|
||||
|
||||
|
||||
price_list = price_change(event_number)
|
||||
|
||||
# set the price_list for the user
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
dwInventoryDb[i]['priceList'] = price_list
|
||||
|
||||
check_inv(nodeID)
|
||||
|
||||
# main game display print
|
||||
msg = render_game_screen(nodeID, day_play, total_day, loc_choice, event_number, price_list, cash_stolen, found_items)
|
||||
|
||||
return msg
|
||||
|
||||
def playDopeWars(nodeID, cmd):
|
||||
global dwGameDayDb, dwPlayerTracker, dwCashDb, dwInventoryDb, dwLocationDb, dwHighScore
|
||||
|
||||
inGame = False
|
||||
msg = ''
|
||||
|
||||
# check if the player is currently playing the game
|
||||
for i in range(0, len(dwGameDayDb)):
|
||||
if dwGameDayDb[i].get('userID') == nodeID:
|
||||
inGame = True
|
||||
|
||||
if not inGame:
|
||||
# initalize player in the database
|
||||
loc = generatelocations()
|
||||
dwInventoryDb.append({'userID': nodeID, 'inventory': 0, 'priceList': []})
|
||||
dwCashDb.append({'userID': nodeID, 'cash': starting_cash})
|
||||
dwLocationDb.append({'userID': nodeID, 'location': loc, 'loc_choice': 0})
|
||||
dwGameDayDb.append({'userID': nodeID, 'day': 0})
|
||||
dwPlayerTracker.append({'userID': nodeID, 'last_played': time.time(), 'cmd': 'start'})
|
||||
logger.debug("System: DopeWars: New player: " + str(nodeID))
|
||||
|
||||
# get the day for the user
|
||||
for i in range(0, len(dwGameDayDb)):
|
||||
if dwGameDayDb[i].get('userID') == nodeID:
|
||||
game_day = dwGameDayDb[i].get('day')
|
||||
|
||||
# get the player's last command
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
last_cmd = dwPlayerTracker[i].get('cmd')
|
||||
|
||||
# get the price_list for the user
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
price_list = dwInventoryDb[i].get('priceList')
|
||||
|
||||
# get the location for the user
|
||||
for i in range(0, len(dwLocationDb)):
|
||||
if dwLocationDb[i].get('userID') == nodeID:
|
||||
loc_choice = dwLocationDb[i].get('loc_choice')
|
||||
|
||||
# Pick Starting City
|
||||
if last_cmd == 'start':
|
||||
# print the location table
|
||||
msg = get_location_table(nodeID)
|
||||
|
||||
# set the player's last command to location to start the game
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker[i]['cmd'] = 'location'
|
||||
|
||||
if last_cmd == 'ask_bsf':
|
||||
msg = f'example buy:\nb,drug#,qty# or Sell: s,1,10 qty can be (m)ax\n f,p or end'
|
||||
menu_choice = cmd.lower()
|
||||
if ',' in menu_choice or '.' in menu_choice:
|
||||
#split the choice into a letter and a number for the buy/sell functions
|
||||
try:
|
||||
if '.' in menu_choice:
|
||||
menu_choice = menu_choice.split('.')
|
||||
if ',' in menu_choice:
|
||||
menu_choice = menu_choice.split(',')
|
||||
|
||||
if int(menu_choice[1]) not in range(1, 8):
|
||||
raise ValueError
|
||||
else:
|
||||
menu_choice[1] = int(menu_choice[1])
|
||||
if menu_choice[0] not in ['b', 's']:
|
||||
raise ValueError
|
||||
if menu_choice[2] != 'm':
|
||||
if int(menu_choice[2]) not in range(1, 101):
|
||||
raise ValueError
|
||||
else:
|
||||
menu_choice[2] = int(menu_choice[2])
|
||||
|
||||
except ValueError:
|
||||
msg = f'a value was bad, example dopeware Buy or Sell\n b,1,10 or s,1,m'
|
||||
return msg
|
||||
|
||||
if menu_choice[0] == 'b':
|
||||
# set last command to ask_bsf and buy
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
|
||||
msg = buy_func(nodeID, price_list, menu_choice[1], menu_choice[2])
|
||||
return msg
|
||||
|
||||
if menu_choice[0] == 's':
|
||||
# set last command to ask_bsf and sell
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
|
||||
msg = sell_func(nodeID, price_list, menu_choice[1], menu_choice[2])
|
||||
return msg
|
||||
elif 's' in menu_choice:
|
||||
msg = ''
|
||||
# sell everything we have in backpack
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
inventory = dwInventoryDb[i].get('inventory')
|
||||
if inventory == 0:
|
||||
msg = "You don't have anything to sell🚭"
|
||||
else:
|
||||
for i in range(1, (len(my_drugs) +1)):
|
||||
sell = sell_func(nodeID, price_list, i, 'm')
|
||||
# ignore starts with "You don't have any"
|
||||
if not sell.startswith("You don't have any"):
|
||||
msg += sell + '\n'
|
||||
# trim the last newline
|
||||
msg = msg[:-1]
|
||||
return msg
|
||||
elif 'f' in menu_choice:
|
||||
# set last command to location
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker[i]['cmd'] = 'location'
|
||||
last_cmd = 'location'
|
||||
|
||||
elif 'p' in menu_choice:
|
||||
# render_game_screen
|
||||
msg = render_game_screen(nodeID, game_day, total_days, loc_choice, -1, price_list, 0, 'nothing')
|
||||
return msg
|
||||
elif 'e' in menu_choice:
|
||||
msg = endGameDw(nodeID)
|
||||
return msg
|
||||
else:
|
||||
msg = f'example buy:\nb,drug#,qty# or Sell: s,1,10 qty can be (m)ax\n f,p or end'
|
||||
return msg
|
||||
|
||||
# Buy
|
||||
if last_cmd == 'buy':
|
||||
# ned to collect which drug # and qty to buy
|
||||
msg = buy_func(nodeID, price_list)
|
||||
return msg
|
||||
|
||||
# Sell
|
||||
if last_cmd == 'sell':
|
||||
msg = sell_func(nodeID, price_list)
|
||||
return msg
|
||||
|
||||
# Pick Location, and display main game screen
|
||||
if last_cmd == 'location':
|
||||
# validate the location choice
|
||||
try:
|
||||
loc_choice = int(cmd)
|
||||
if loc_choice not in range(1, 6):
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
loc_choice = random.randint(1, 6)
|
||||
|
||||
# set the player's location choice
|
||||
for i in range(0, len(dwLocationDb)):
|
||||
if dwLocationDb[i].get('userID') == nodeID:
|
||||
dwLocationDb[i]['loc_choice'] = loc_choice
|
||||
|
||||
# set the player's last command
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker[i]['cmd'] = 'display_main'
|
||||
|
||||
# increment the game_day
|
||||
game_day += 1
|
||||
for i in range(0, len(dwGameDayDb)):
|
||||
if dwGameDayDb[i].get('userID') == nodeID:
|
||||
dwGameDayDb[i]['day'] = game_day
|
||||
|
||||
# update the player's last played time
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker[i]['last_played'] = time.time()
|
||||
|
||||
last_cmd = 'display_main'
|
||||
|
||||
# Display Main Game Screen and ask for buy, sell, or fly
|
||||
if last_cmd == 'display_main':
|
||||
msg = dopeWarGameDay(nodeID, game_day, total_days)
|
||||
msg += f"\nBuy💸, Sell💰, (F)ly🛫? (P)riceList?"
|
||||
# set the player's last command
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker[i]['cmd'] = 'ask_bsf'
|
||||
|
||||
# Game end
|
||||
if game_day == total_days + 1:
|
||||
msg = endGameDw(nodeID)
|
||||
|
||||
return msg
|
||||
409
modules/games/golfsim.py
Normal file
409
modules/games/golfsim.py
Normal file
@@ -0,0 +1,409 @@
|
||||
# https://github.com/danfriedman30/pythongame
|
||||
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
import random
|
||||
import time
|
||||
import pickle
|
||||
from modules.log import *
|
||||
|
||||
# Clubs setup
|
||||
driver_distances = list(range(230, 280, 5))
|
||||
low_distances = list(range(185, 215, 5))
|
||||
mid_distances = list(range(130, 185, 5))
|
||||
high_distances = list(range(90, 135, 5))
|
||||
gap_wedge_distances = list(range(50, 85, 5))
|
||||
lob_wedge_distances = list(range(10, 50, 5))
|
||||
putt_outcomes = [1, 2, 3]
|
||||
|
||||
# Hole/Course Setup
|
||||
full_hole_range = list(range(130, 520, 5))
|
||||
par3_range = list(range(130, 255, 5))
|
||||
par4_range = list(range(255, 445, 5))
|
||||
par5_range = list(range(445, 520, 5))
|
||||
par3_4_range = par3_range + par4_range
|
||||
par3_5_range = par3_range + par5_range
|
||||
par4_5_range = par4_range + par5_range
|
||||
|
||||
# Player setup
|
||||
playingHole = False
|
||||
golfTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'hole': 0, 'distance_remaining': 0, 'hole_shots': 0, 'hole_strokes': 0, 'hole_to_par': 0, 'total_strokes': 0, 'total_to_par': 0, 'par': 0, 'hazard': ''}]
|
||||
|
||||
# Club functions
|
||||
def hit_driver():
|
||||
club_distance = random.choice(driver_distances)
|
||||
return club_distance
|
||||
|
||||
def hit_low_iron():
|
||||
club_distance = random.choice(low_distances)
|
||||
return club_distance
|
||||
|
||||
def hit_mid_iron():
|
||||
club_distance = random.choice(mid_distances)
|
||||
return club_distance
|
||||
|
||||
def hit_high_iron():
|
||||
club_distance = random.choice(high_distances)
|
||||
return club_distance
|
||||
|
||||
def hit_gap_wedge():
|
||||
club_distance = random.choice(gap_wedge_distances)
|
||||
return club_distance
|
||||
|
||||
def hit_lob_wedge():
|
||||
club_distance = random.choice(lob_wedge_distances)
|
||||
return club_distance
|
||||
|
||||
def finish_hole():
|
||||
finish = random.choice(putt_outcomes)
|
||||
return finish
|
||||
|
||||
def endGameGolf(nodeID):
|
||||
# pop player from tracker
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
golfTracker.pop(i)
|
||||
logger.debug("System: GolfSim: Player " + str(nodeID) + " has ended their round.")
|
||||
|
||||
def getScorecardGolf(scorecard):
|
||||
# Scorecard messages, convert score to message comment
|
||||
msg = ""
|
||||
if scorecard == 8:
|
||||
# Quadruple bogey
|
||||
msg += " +Quad Bogey☃️ "
|
||||
elif scorecard == 7:
|
||||
# Triple bogey
|
||||
msg += " +Triple Bogey "
|
||||
elif scorecard == 6:
|
||||
# Double bogey
|
||||
msg += " +Double Bogey "
|
||||
elif scorecard == 5:
|
||||
# Bogey
|
||||
msg += " +Bogey "
|
||||
elif scorecard > 0:
|
||||
# Over par
|
||||
msg += f" +Par {str(scorecard)} "
|
||||
elif scorecard == 0:
|
||||
# Even par
|
||||
msg += " Even Par💪 "
|
||||
elif scorecard == -1:
|
||||
# Birdie
|
||||
msg += " -Birdie🐦 "
|
||||
elif scorecard == -2:
|
||||
# Eagle
|
||||
msg += " -Eagle🦅 "
|
||||
elif scorecard == -3:
|
||||
# Albatross
|
||||
msg += " -Albatross🦅🦅 "
|
||||
else:
|
||||
# Under par
|
||||
msg += f" -Par {str(abs(scorecard))} "
|
||||
return msg
|
||||
|
||||
def getHighScoreGolf(nodeID, strokes, par):
|
||||
# check if player is in high score list
|
||||
try:
|
||||
with open('data/golfsim_hs.pkl', 'rb') as f:
|
||||
golfHighScore = pickle.load(f)
|
||||
except:
|
||||
logger.debug("System: GolfSim: High Score file not found.")
|
||||
golfHighScore = [{'nodeID': nodeID, 'strokes': strokes, 'par': par}]
|
||||
with open('data/golfsim_hs.pkl', 'wb') as f:
|
||||
pickle.dump(golfHighScore, f)
|
||||
|
||||
if strokes < golfHighScore[0]['strokes']:
|
||||
# player got new low score which is high score
|
||||
golfHighScore[0]['nodeID'] = nodeID
|
||||
golfHighScore[0]['strokes'] = strokes
|
||||
golfHighScore[0]['par'] = par
|
||||
with open('data/golfsim_hs.pkl', 'wb') as f:
|
||||
pickle.dump(golfHighScore, f)
|
||||
return golfHighScore
|
||||
|
||||
return 0
|
||||
|
||||
# Main game loop
|
||||
def playGolf(nodeID, message, finishedHole=False):
|
||||
msg = ''
|
||||
global golfTracker
|
||||
# Course setup
|
||||
par3_count = 0
|
||||
par4_count = 0
|
||||
par5_count = 0
|
||||
# Scorecard setup
|
||||
total_strokes = 0
|
||||
total_to_par = 0
|
||||
par = 0
|
||||
|
||||
# get player's last command from tracker if not new player
|
||||
last_cmd = ""
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
last_cmd = golfTracker[i]['cmd']
|
||||
hole = golfTracker[i]['hole']
|
||||
distance_remaining = golfTracker[i]['distance_remaining']
|
||||
hole_shots = golfTracker[i]['hole_shots']
|
||||
par = golfTracker[i]['par']
|
||||
total_strokes = golfTracker[i]['total_strokes']
|
||||
total_to_par = golfTracker[i]['total_to_par']
|
||||
|
||||
if last_cmd == "" or last_cmd == "new":
|
||||
# Start a new hole
|
||||
if hole <= 9:
|
||||
# Set up hole count restrictions on par
|
||||
if par3_count < 2 and par4_count < 5 and par5_count < 2:
|
||||
hole_length = random.choice(full_hole_range)
|
||||
if par3_count >= 2 and par4_count < 5 and par5_count < 2:
|
||||
hole_length = random.choice(par4_5_range)
|
||||
if par3_count >= 2 and par4_count < 5 and par5_count >= 2:
|
||||
hole_length = random.choice(par4_range)
|
||||
if par3_count < 2 and par4_count < 5 and par5_count >= 2:
|
||||
hole_length = random.choice(par3_4_range)
|
||||
if par3_count < 2 and par4_count >= 5 and par5_count >= 2:
|
||||
hole_length = random.choice(par3_range)
|
||||
if par3_count >= 2 and par4_count >= 5 and par5_count < 2:
|
||||
hole_length = random.choice(par5_range)
|
||||
if par3_count < 2 and par4_count >= 5 and par5_count < 2:
|
||||
hole_length = random.choice(par3_5_range)
|
||||
|
||||
# Set up par for the hole
|
||||
if hole_length <= 250:
|
||||
par = 3
|
||||
par3_count += 1
|
||||
elif hole_length > 250 and hole_length <= 440:
|
||||
par = 4
|
||||
par4_count += 1
|
||||
elif hole_length > 440:
|
||||
par = 5
|
||||
par5_count += 1
|
||||
|
||||
# roll for chance of hazard
|
||||
hazard_chance = random.randint(1, 100)
|
||||
weather_chance = random.randint(1, 100)
|
||||
# have low chances of hazards and weather
|
||||
hasHazard = False
|
||||
hazard = ""
|
||||
if hazard_chance < 25:
|
||||
# Further reduce chance of hazards with weather
|
||||
if weather_chance < 15:
|
||||
# randomly calculate a hazard for the hole sand, 🌊, 🌲, 🏘️, etc
|
||||
hazard = random.choice(["🏖️", "🌊", "🌲", "🏘️"])
|
||||
hasHazard = True
|
||||
|
||||
|
||||
# Set initial parameters before starting a hole
|
||||
distance_remaining = hole_length
|
||||
hole_shots = 0
|
||||
|
||||
# save player's current game state
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
golfTracker[i]['distance_remaining'] = distance_remaining
|
||||
golfTracker[i]['cmd'] = 'stroking'
|
||||
golfTracker[i]['par'] = par
|
||||
golfTracker[i]['total_strokes'] = total_strokes
|
||||
golfTracker[i]['total_to_par'] = total_to_par
|
||||
golfTracker[i]['hazard'] = hazard
|
||||
golfTracker[i]['hole'] = hole
|
||||
golfTracker[i]['last_played'] = time.time()
|
||||
golfTracker[i]['hole_shots'] = hole_shots
|
||||
|
||||
# Show player the hole information
|
||||
msg += "⛳️#" + str(hole) + " is a " + str(hole_length) + "-yard Par " + str(par) + "."
|
||||
if hasHazard:
|
||||
msg += "⚠️" + hazard + "."
|
||||
else:
|
||||
# add weather conditions with random choice from list, this is fluff
|
||||
msg += random.choice(["☀️", "💨", "☀️", "☀️", "⛅️", "☁️", "☀️"])
|
||||
|
||||
if not finishedHole:
|
||||
msg += f"\nChoose your club."
|
||||
|
||||
return msg
|
||||
|
||||
if last_cmd == 'stroking':
|
||||
|
||||
# Get player's current game state
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
distance_remaining = golfTracker[i]['distance_remaining']
|
||||
hole = golfTracker[i]['hole']
|
||||
hole_shots = golfTracker[i]['hole_shots']
|
||||
par = golfTracker[i]['par']
|
||||
total_strokes = golfTracker[i]['total_strokes']
|
||||
total_to_par = golfTracker[i]['total_to_par']
|
||||
hazard = golfTracker[i]['hazard']
|
||||
|
||||
# Start loop to be able to choose clubs while at least 20 yards away
|
||||
if distance_remaining >= 20:
|
||||
msg = ""
|
||||
club = message.lower()
|
||||
shot_distance = 0
|
||||
|
||||
pin_distance = distance_remaining
|
||||
|
||||
if club == "driver" or club.startswith("d"):
|
||||
shot_distance = hit_driver()
|
||||
msg += "🏌️Hit D " + str(shot_distance) + "yd. "
|
||||
distance_remaining = abs(distance_remaining - shot_distance)
|
||||
hole_shots += 1
|
||||
elif "low" in club or club.startswith("l"):
|
||||
shot_distance = hit_low_iron()
|
||||
msg += "🏌️Hit L Iron " + str(shot_distance) + "yd. "
|
||||
distance_remaining = abs(distance_remaining - shot_distance)
|
||||
hole_shots += 1
|
||||
elif "mid" in club or club.startswith("m"):
|
||||
shot_distance = hit_mid_iron()
|
||||
msg += "🏌️Hit M Iron " + str(shot_distance) + "yd. "
|
||||
distance_remaining = abs(distance_remaining - shot_distance)
|
||||
hole_shots += 1
|
||||
elif "high" in club or club.startswith("h"):
|
||||
shot_distance = hit_high_iron()
|
||||
msg += "🏌️Hit H Iron " + str(shot_distance) + "yd. "
|
||||
distance_remaining = abs(distance_remaining - shot_distance)
|
||||
hole_shots += 1
|
||||
elif "gap" in club or club.startswith("g"):
|
||||
shot_distance = hit_gap_wedge()
|
||||
msg += "🏌️Hit G Wedge " + str(shot_distance) + "yd ."
|
||||
distance_remaining = abs(distance_remaining - shot_distance)
|
||||
hole_shots += 1
|
||||
elif "wedge" in club or club.startswith("w"):
|
||||
shot_distance = hit_lob_wedge()
|
||||
msg += "🏌️Hit L Wedge " + str(shot_distance) + "yd. "
|
||||
distance_remaining = abs(distance_remaining - shot_distance)
|
||||
hole_shots += 1
|
||||
elif club == "caddy" or club.startswith("c"):
|
||||
# Show player the club distances
|
||||
msg += f"Caddy Guess:\nD:{hit_driver()} L:{hit_low_iron()} M:{hit_mid_iron()} H:{hit_high_iron()} G:{hit_gap_wedge()} W:{hit_lob_wedge()}"
|
||||
else:
|
||||
msg += f"Didnt get your club 🥪♣️🪩 choice, you have {distance_remaining}yds. to ⛳️"
|
||||
return msg
|
||||
|
||||
if distance_remaining - pin_distance > pin_distance or shot_distance > pin_distance:
|
||||
# Check for over-shooting the hole
|
||||
if distance_remaining > 20:
|
||||
# did it go off the "green"?
|
||||
msg += "Overshot the green!🚀"
|
||||
if distance_remaining == 0:
|
||||
msg += "🎯Perfect shot! "
|
||||
last_cmd = 'putt'
|
||||
elif distance_remaining < 20:
|
||||
# Roll Dice
|
||||
hole_in_one_chance = random.randint(1, 100)
|
||||
wind_factor = random.randint(1, 10)
|
||||
skill_factor = random.randint(1, 10)
|
||||
critter_factor = random.randint(1, 50)
|
||||
|
||||
# Check for hole in one
|
||||
if hole_in_one_chance <= 5 and wind_factor > 7 and skill_factor > 8:
|
||||
distance_remaining = 0
|
||||
# Check for critters
|
||||
if skill_factor > 8 and critter_factor < 40 and wind_factor > 2 and hole_in_one_chance > 5:
|
||||
msg += random.choice(["A 🐿️ steals your ball!😡 ","You Hit a 🦅 soring past ", "🐊 need we say more? ", "hit a 🪟 of a 🏡 "])
|
||||
distance_remaining = -1
|
||||
# Handle hazard
|
||||
if hazard == "🌊" and skill_factor < 7:
|
||||
msg += "In the water!🌊"
|
||||
distance_remaining = -1
|
||||
if hazard == "🏖️" and skill_factor < 5:
|
||||
msg += "In the sand!🏖️"
|
||||
distance_remaining = random.randint(5, 10)
|
||||
if hazard == "🌲" and skill_factor < 3:
|
||||
msg += "In the trees!🌲"
|
||||
distance_remaining += random.randint(5, 20)
|
||||
if hazard == "🏘️" and skill_factor < 2:
|
||||
msg += "In the parking lot!🚗"
|
||||
distance_remaining += random.randint(10, 30)
|
||||
|
||||
# Check we didnt go off the green or into a hazard
|
||||
if distance_remaining < 20:
|
||||
last_cmd = 'putt'
|
||||
else:
|
||||
last_cmd = 'stroking'
|
||||
else:
|
||||
msg += "\nYou have " + str(distance_remaining) + "yd. ⛳️"
|
||||
msg += "\nClub?[D, L, M, H, G, W]🏌️"
|
||||
|
||||
|
||||
# save player's current game state, keep stroking
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
golfTracker[i]['distance_remaining'] = distance_remaining
|
||||
golfTracker[i]['hole_shots'] = hole_shots
|
||||
golfTracker[i]['total_strokes'] = total_strokes
|
||||
golfTracker[i]['cmd'] = 'stroking'
|
||||
|
||||
return msg
|
||||
|
||||
if last_cmd == 'putt':
|
||||
# Finish the hole by putting
|
||||
critter = False
|
||||
if distance_remaining < 20:
|
||||
if distance_remaining == 0:
|
||||
putts = 0
|
||||
elif distance_remaining == -1:
|
||||
putts = 0
|
||||
critter = True
|
||||
else:
|
||||
putts = finish_hole()
|
||||
|
||||
# Calculate hole and round scores
|
||||
hole_strokes = hole_shots + putts
|
||||
hole_to_par = hole_strokes - par
|
||||
total_strokes += hole_strokes
|
||||
total_to_par += hole_to_par
|
||||
|
||||
|
||||
if not critter:
|
||||
# Show player hole/round scoring info
|
||||
if putts == 0 and hole_strokes == 1:
|
||||
msg += "🎯Hole in one!⛳️"
|
||||
elif putts == 0:
|
||||
msg += "You're in the hole at " + str(hole_strokes) + " strokes!"
|
||||
else:
|
||||
msg += "You're on the green! After " + str(putts) + " putt(s), you're in for " + str(hole_strokes) + " strokes."
|
||||
msg += getScorecardGolf(hole_to_par)
|
||||
|
||||
if hole not in [1, 10]:
|
||||
# Show player total scoring info for the round, except hole 1 and 10
|
||||
msg += "\nYou've hit a total of " + str(total_strokes) + " strokes today, for"
|
||||
msg += getScorecardGolf(total_to_par)
|
||||
|
||||
# Move to next hole
|
||||
hole += 1
|
||||
else:
|
||||
msg += f"Got a new ball at Pro-Shop, marshal put you @" # flow into same hole haha
|
||||
|
||||
# Scorecard reset
|
||||
hole_to_par = 0
|
||||
hole_strokes = 0
|
||||
hole_shots = 0
|
||||
|
||||
# Save player's current game state
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
golfTracker[i]['hole_strokes'] = hole_strokes
|
||||
golfTracker[i]['hole_to_par'] = hole_to_par
|
||||
golfTracker[i]['total_strokes'] = total_strokes
|
||||
golfTracker[i]['total_to_par'] = total_to_par
|
||||
golfTracker[i]['hole'] = hole
|
||||
golfTracker[i]['cmd'] = 'new'
|
||||
golfTracker[i]['last_played'] = time.time()
|
||||
|
||||
if hole >= 9:
|
||||
# Final score messages & exit prompt
|
||||
msg += f"🎉Finished 9-hole round⛳️"
|
||||
#HighScore Display
|
||||
highscore = getHighScoreGolf(nodeID, total_strokes, total_to_par)
|
||||
if highscore != 0:
|
||||
msg += " 🏆New Club Record🏆"
|
||||
# pop player from tracker
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
golfTracker.pop(i)
|
||||
logger.debug("System: GolfSim: Player " + str(nodeID) + " has finished their round.")
|
||||
else:
|
||||
# Show player the next hole
|
||||
msg += playGolf(nodeID, 'new', True)
|
||||
msg += "\n🏌️[D, L, M, H, G, W, End]🏌️"
|
||||
|
||||
return msg
|
||||
126
modules/games/joke.py
Normal file
126
modules/games/joke.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# This module is used to tell jokes to the user
|
||||
# The emoji table of contents is used to replace words in the joke with emojis
|
||||
# As a Ham, is this obsecuring the meaning of the joke? Or is it enhancing it?
|
||||
from dadjokes import Dadjoke # pip install dadjokes
|
||||
from modules.log import *
|
||||
|
||||
def tableOfContents():
|
||||
wordToEmojiMap = {
|
||||
'love': '❤️', 'heart': '❤️', 'happy': '😊', 'smile': '😊', 'sad': '😢', 'angry': '😠', 'mad': '😠', 'cry': '😢', 'laugh': '😂', 'funny': '😂', 'cool': '😎',
|
||||
'joy': '😂', 'kiss': '😘', 'hug': '🤗', 'wink': '😉', 'grin': '😁', 'bored': '😐', 'tired': '😴', 'sleepy': '😴', 'shocked': '😲', 'surprised': '😲',
|
||||
'confused': '😕', 'thinking': '🤔', 'sick': '🤢', 'party': '🎉', 'celebrate': '🎉', 'clap': '👏', 'thumbs up': '👍', 'thumbs down': '👎',
|
||||
'ok': '👌', 'wave': '👋', 'pray': '🙏', 'muscle': '💪', 'fire': '🔥', 'star': '⭐', 'sun': '☀️', 'moon': '🌙', 'rain': '🌧️', 'snow': '❄️', 'cloud': '☁️',
|
||||
'dog': '🐶', 'cat': '🐱', 'mouse': '🐭', 'rabbit': '🐰', 'fox': '🦊', 'bear': '🐻', 'panda': '🐼', 'koala': '🐨', 'tiger': '🐯', 'lion': '🦁', 'cow': '🐮',
|
||||
'pig': '🐷', 'frog': '🐸', 'monkey': '🐵', 'chicken': '🐔', 'penguin': '🐧', 'bird': '🐦', 'fish': '🐟', 'whale': '🐋', 'dolphin': '🐬', 'octopus': '🐙',
|
||||
'apple': '🍎', 'orange': '🍊', 'banana': '🍌', 'watermelon': '🍉', 'grape': '🍇', 'strawberry': '🍓', 'cherry': '🍒', 'peach': '🍑', 'pineapple': '🍍', 'mango': '🥭', 'coconut': '🥥',
|
||||
'tomato': '🍅', 'eggplant': '🍆', 'avocado': '🥑', 'broccoli': '🥦', 'cucumber': '🥒', 'corn': '🌽', 'carrot': '🥕', 'potato': '🥔', 'sweet potato': '🍠', 'chili': '🌶️', 'garlic': '🧄',
|
||||
'pizza': '🍕', 'burger': '🍔', 'fries': '🍟', 'hotdog': '🌭', 'popcorn': '🍿', 'donut': '🍩', 'cookie': '🍪', 'cake': '🎂', 'pie': '🍰', 'cupcake': '🧁', 'chocolate': '🍫',
|
||||
'candy': '🍬', 'lollipop': '🍭', 'pudding': '🍮', 'honey': '🍯', 'milk': '🍼', 'coffee': '☕', 'tea': '🍵', 'sake': '🍶', 'beer': '🍺', 'cheers': '🍻', 'champagne': '🥂',
|
||||
'wine': '🍷', 'whiskey': '🥃', 'cocktail': '🍸', 'tropical drink': '🍹', 'bottle': '🍾', 'soda': '🥤', 'chopsticks': '🥢', 'fork': '🍴', 'knife': '🔪', 'spoon': '🥄', 'kitchen knife': '🔪',
|
||||
'house': '🏠', 'home': '🏡', 'office': '🏢', 'post office': '🏣', 'hospital': '🏥', 'bank': '🏦', 'hotel': '🏨', 'love hotel': '🏩', 'convenience store': '🏪', 'school': '🏫', 'department store': '🏬',
|
||||
'factory': '🏭', 'castle': '🏯', 'palace': '🏰', 'church': '💒', 'tower': '🗼', 'statue of liberty': '🗽', 'mosque': '🕌', 'synagogue': '🕍', 'hindu temple': '🛕', 'kaaba': '🕋', 'shinto shrine': '⛩️',
|
||||
'railway': '🛤️', 'highway': '🛣️', 'map': '🗾', 'carousel': '🎠', 'ferris wheel': '🎡', 'roller coaster': '🎢', 'circus': '🎪', 'theater': '🎭', 'art': '🎨', 'slot machine': '🎰', 'dice': '🎲',
|
||||
'bowling': '🎳', 'video game': '🎮', 'dart': '🎯', 'billiard': '🎱', 'medal': '🎖️', 'trophy': '🏆', 'gold medal': '🥇', 'silver medal': '🥈', 'bronze medal': '🥉', 'soccer': '⚽', 'baseball': '⚾',
|
||||
'basketball': '🏀', 'volleyball': '🏐', 'football': '🏈', 'rugby': '🏉', 'tennis': '🎾', 'frisbee': '🥏', 'ping pong': '🏓', 'badminton': '🏸', 'boxing': '🥊', 'martial arts': '🥋',
|
||||
'goal': '🥅', 'golf': '⛳', 'skating': '⛸️', 'fishing': '🎣', 'diving': '🤿', 'running': '🎽', 'skiing': '🎿', 'sledding': '🛷', 'curling': '🥌', 'climbing': '🧗', 'yoga': '🧘',
|
||||
'surfing': '🏄', 'swimming': '🏊', 'water polo': '🤽', 'cycling': '🚴', 'mountain biking': '🚵', 'horse riding': '🏇', 'kneeling': '🧎', 'weightlifting': '🏋️', 'gymnastics': '🤸', 'wrestling': '🤼', 'handball': '🤾',
|
||||
'juggling': '🤹', 'meditation': '🧘', 'sauna': '🧖', 'rock climbing': '🧗', 'stop': '🛑', 'computer': '💻', 'phone': '📱', 'email': '📧', 'camera': '📷', 'video': '📹', 'music': '🎵',
|
||||
'guitar': '🎸', 'piano': '🎹', 'drum': '🥁', 'microphone': '🎤', 'headphone': '🎧', 'book': '📚', 'newspaper': '📰', 'magazine': '📖', 'pen': '🖊️', 'pencil': '✏️', 'paintbrush': '🖌️',
|
||||
'scissors': '✂️', 'ruler': '📏', 'globe': '🌍', 'earth': '🌎', 'star': '🌟', 'comet': '☄️', 'rocket': '🚀', 'airplane': '✈️', 'car': '🚗', 'bus': '🚌', 'train': '🚆',
|
||||
'bicycle': '🚲', 'motorcycle': '🏍️', 'boat': '🚤', 'ship': '🚢', 'helicopter': '🚁', 'tractor': '🚜', 'ambulance': '🚑', 'fire truck': '🚒', 'police car': '🚓', 'taxi': '🚕', 'truck': '🚚',
|
||||
'construction': '🚧', 'traffic light': '🚦', 'stop sign': '🛑', 'fuel': '⛽', 'battery': '🔋', 'light bulb': '💡', 'flashlight': '🔦', 'candle': '🕯️', 'lamp': '🛋️',
|
||||
'bed': '🛏️', 'sofa': '🛋️', 'chair': '🪑', 'table': '🛋️', 'toilet': '🚽', 'shower': '🚿', 'bathtub': '🛁', 'sink': '🚰', 'mirror': '🪞', 'door': '🚪', 'window': '🪟',
|
||||
'key': '🔑', 'lock': '🔒', 'hammer': '🔨', 'wrench': '🔧', 'screwdriver': '🪛', 'saw': '🪚', 'drill': '🛠️', 'toolbox': '🧰', 'paint roller': '🖌️', 'brush': '🖌️', 'broom': '🧹',
|
||||
'mop': '🧽', 'bucket': '🪣', 'vacuum': '🧹', 'washing machine': '🧺', 'dryer': '🧺', 'iron': '🧺', 'hanger': '🧺', 'laundry': '🧺', 'basket': '🧺', 'trash': '🗑️', 'recycle': '♻️',
|
||||
'plant': '🌱', 'tree': '🌳', 'flower': '🌸', 'leaf': '🍃', 'cactus': '🌵', 'mushroom': '🍄', 'herb': '🌿', 'bamboo': '🎍', 'rose': '🌹', 'tulip': '🌷', 'sunflower': '🌻',
|
||||
'hibiscus': '🌺', 'cherry blossom': '🌸', 'bouquet': '💐', 'seedling': '🌱', 'palm tree': '🌴', 'evergreen tree': '🌲', 'deciduous tree': '🌳', 'fallen leaf': '🍂', 'maple leaf': '🍁',
|
||||
'ear of rice': '🌾', 'shamrock': '☘️', 'four leaf clover': '🍀', 'grapes': '🍇', 'melon': '🍈', 'watermelon': '🍉', 'tangerine': '🍊', 'lemon': '🍋', 'banana': '🍌', 'pineapple': '🍍',
|
||||
'mango': '🥭', 'apple': '🍎', 'green apple': '🍏', 'pear': '🍐', 'peach': '🍑', 'cherries': '🍒', 'strawberry': '🍓', 'kiwi': '🥝', 'tomato': '🍅', 'coconut': '🥥', 'avocado': '🥑',
|
||||
'eggplant': '🍆', 'potato': '🥔', 'carrot': '🥕', 'corn': '🌽', 'hot pepper': '🌶️', 'cucumber': '🥒', 'leafy green': '🥬', 'broccoli': '🥦', 'garlic': '🧄', 'onion': '🧅',
|
||||
'peanuts': '🥜', 'chestnut': '🌰', 'bread': '🍞', 'croissant': '🥐', 'baguette': '🥖', 'flatbread': '🥙', 'pretzel': '🥨', 'bagel': '🥯', 'pancakes': '🥞', 'waffle': '🧇', 'cheese': '🧀',
|
||||
'meat': '🍖', 'poultry': '🍗', 'bacon': '🥓', 'hamburger': '🍔', 'fries': '🍟', 'pizza': '🍕', 'hot dog': '🌭', 'sandwich': '🥪', 'taco': '🌮', 'burrito': '🌯', 'tamale': '🫔',
|
||||
'stuffed flatbread': '🥙', 'falafel': '🧆', 'egg': '🥚', 'fried egg': '🍳', 'shallow pan of food': '🥘', 'pot of food': '🍲', 'fondue': '🫕', 'bowl with spoon': '🥣', 'green salad': '🥗',
|
||||
'popcorn': '🍿', 'butter': '🧈', 'salt': '🧂', 'canned food': '🥫', 'bento box': '🍱', 'rice cracker': '🍘', 'rice ball': '🍙', 'cooked rice': '🍚', 'curry rice': '🍛', 'steaming bowl': '🍜',
|
||||
'spaghetti': '🍝', 'roasted sweet potato': '🍠', 'oden': '🍢', 'sushi': '🍣', 'fried shrimp': '🍤', 'fish cake': '🍥', 'moon cake': '🥮', 'dango': '🍡', 'dumpling': '🥟', 'fortune cookie': '🥠',
|
||||
'takeout box': '🥡', 'crab': '🦀', 'lobster': '🦞', 'shrimp': '🦐', 'squid': '🦑', 'oyster': '🦪', 'ice cream': '🍨', 'shaved ice': '🍧', 'ice cream cone': '🍦', 'doughnut': '🍩', 'cookie': '🍪',
|
||||
'birthday cake': '🎂', 'shortcake': '🍰', 'cupcake': '🧁', 'pie': '🥧', 'chocolate bar': '🍫', 'candy': '🍬', 'lollipop': '🍭', 'custard': '🍮', 'honey pot': '🍯', 'baby bottle': '🍼',
|
||||
'glass of milk': '🥛', 'hot beverage': '☕', 'teapot': '🫖', 'teacup without handle': '🍵', 'sake': '🍶', 'bottle with popping cork': '🍾', 'wine glass': '🍷', 'cocktail glass': '🍸', 'tropical drink': '🍹',
|
||||
'beer mug': '🍺', 'clinking beer mugs': '🍻', 'clinking glasses': '🥂', 'tumbler glass': '🥃', 'cup with straw': '🥤', 'bubble tea': '🧋', 'beverage box': '🧃', 'mate': '🧉', 'ice': '🧊',
|
||||
'chopsticks': '🥢', 'fork and knife': '🍴', 'spoon': '🥄', 'kitchen knife': '🔪', 'amphora': '🏺', 'globe showing Europe-Africa': '🌍', 'globe showing Americas': '🌎', 'globe showing Asia-Australia': '🌏',
|
||||
'globe with meridians': '🌐', 'world map': '🗺️', 'mountain': '⛰️', 'volcano': '🌋', 'mount fuji': '🗻', 'camping': '🏕️', 'beach with umbrella': '🏖️', 'desert': '🏜️', 'desert island': '🏝️',
|
||||
'national park': '🏞️', 'stadium': '🏟️', 'classical building': '🏛️', 'building construction': '🏗️', 'brick': '🧱', 'rock': '🪨', 'wood': '🪵', 'hut': '🛖', 'houses': '🏘️', 'derelict house': '🏚️',
|
||||
'house with garden': '🏡', 'office building': '🏢', 'japanese post office': '🏣', 'post office': '🏤', 'hospital': '🏥', 'bank': '🏦', 'hotel': '🏨', 'love hotel': '🏩', 'convenience store': '🏪',
|
||||
'school': '🏫', 'department store': '🏬', 'factory': '🏭', 'japanese castle': '🏯', 'castle': '🏰', 'wedding': '💒', 'tokyo tower': '🗼', 'statue of liberty': '🗽', 'church': '⛪', 'mosque': '🕌',
|
||||
'hindu temple': '🛕', 'synagogue': '🕍', 'shinto shrine': '⛩️', 'kaaba': '🕋', 'fountain': '⛲', 'tent': '⛺', 'foggy': '🌁', 'night with stars': '🌃', 'sunrise over mountains': '🌄', 'sunrise': '🌅',
|
||||
'cityscape at dusk': '🌆', 'sunset': '🌇', 'cityscape': '🏙️', 'bridge at night': '🌉', 'hot springs': '♨️', 'carousel horse': '🎠', 'ferris wheel': '🎡', 'roller coaster': '🎢', 'barber pole': '💈',
|
||||
'robot': '🤖', 'alien': '👽', 'ghost': '👻', 'skull': '💀', 'pumpkin': '🎃', 'clown': '🤡', 'wizard': '🧙', 'elf': '🧝', 'fairy': '🧚', 'mermaid': '🧜', 'vampire': '🧛',
|
||||
'zombie': '🧟', 'genie': '🧞', 'superhero': '🦸', 'supervillain': '🦹', 'mage': '🧙', 'knight': '🛡️', 'ninja': '🥷', 'pirate': '🏴☠️', 'angel': '👼', 'devil': '😈', 'dragon': '🐉',
|
||||
'unicorn': '🦄', 'phoenix': '🦅', 'griffin': '🦅', 'centaur': '🐎', 'minotaur': '🐂', 'cyclops': '👁️', 'medusa': '🐍', 'sphinx': '🦁', 'kraken': '🦑', 'yeti': '❄️', 'sasquatch': '🦧',
|
||||
'loch ness monster': '🦕', 'chupacabra': '🐐', 'banshee': '👻', 'golem': '🗿', 'djinn': '🧞', 'basilisk': '🐍', 'hydra': '🐉', 'cerberus': '🐶', 'chimera': '🐐', 'manticore': '🦁', 'wyvern': '🐉',
|
||||
'pegasus': '🦄', 'hippogriff': '🦅', 'kelpie': '🐎', 'selkie': '🦭', 'kitsune': '🦊', 'tanuki': '🦝', 'tengu': '🦅', 'oni': '👹', 'yokai': '👻', 'kappa': '🐢', 'yurei': '👻',
|
||||
'kami': '👼', 'shinigami': '💀', 'bakemono': '👹', 'tsukumogami': '🧸', 'noppera-bo': '👤', 'rokurokubi': '🧛', 'yuki-onna': '❄️', 'jorogumo': '🕷️', 'nue': '🐍', 'ubume': '👼',
|
||||
'atom': '⚛️', 'dna': '🧬', 'microscope': '🔬', 'telescope': '🔭', 'rocket': '🚀', 'satellite': '🛰️', 'spaceship': '🛸', 'planet': '🪐', 'black hole': '🕳️', 'galaxy': '🌌',
|
||||
'comet': '☄️', 'constellation': '🌠', 'lightning': '⚡', 'magnet': '🧲', 'battery': '🔋', 'computer': '💻', 'keyboard': '⌨️', 'mouse': '🖱️', 'printer': '🖨️', 'floppy disk': '💾',
|
||||
'cd': '💿', 'dvd': '📀', 'smartphone': '📱', 'tablet': '📲', 'watch': '⌚', 'camera': '📷', 'video camera': '📹', 'projector': '📽️', 'radio': '📻', 'television': '📺',
|
||||
'satellite dish': '📡', 'game controller': '🎮', 'joystick': '🕹️', 'vr headset': '🕶️', 'headphones': '🎧', 'speaker': '🔊', 'flashlight': '🔦', 'circuit': '🔌', 'chip': '💻',
|
||||
'server': '🖥️', 'database': '💾', 'cloud': '☁️', 'network': '🌐', 'code': '💻', 'bug': '🐛', 'virus': '🦠', 'bacteria': '🦠', 'lab coat': '🥼', 'safety goggles': '🥽',
|
||||
'test tube': '🧪', 'petri dish': '🧫', 'beaker': '🧪', 'bunsen burner': '🔥', 'graduated cylinder': '🧪', 'pipette': '🧪', 'scalpel': '🔪', 'syringe': '💉', 'pill': '💊',
|
||||
'stethoscope': '🩺', 'thermometer': '🌡️', 'x-ray': '🩻', 'brain': '🧠', 'heart': '❤️', 'lung': '🫁', 'bone': '🦴', 'muscle': '💪', 'robot arm': '🦾', 'robot leg': '🦿',
|
||||
'prosthetic arm': '🦾', 'prosthetic leg': '🦿', 'wheelchair': '🦽', 'crutch': '🦯', 'hearing aid': '🦻', 'glasses': '👓', 'magnifying glass': '🔍', 'circus tent': '🎪',
|
||||
'duck': '🦆', 'eagle': '🦅', 'owl': '🦉', 'bat': '🦇', 'shark': '🦈', 'butterfly': '🦋', 'snail': '🐌', 'bee': '🐝', 'beetle': '🐞', 'ant': '🐜', 'cricket': '🦗',
|
||||
'spider': '🕷️', 'scorpion': '🦂', 'turkey': '🦃', 'peacock': '🦚', 'parrot': '🦜', 'swan': '🦢', 'flamingo': '🦩', 'dodo': '🦤', 'sloth': '🦥', 'otter': '🦦',
|
||||
'skunk': '🦨', 'kangaroo': '🦘', 'badger': '🦡', 'beaver': '🦫', 'bison': '🦬', 'mammoth': '🦣', 'raccoon': '🦝', 'hedgehog': '🦔', 'squirrel': '🐿️', 'chipmunk': '🐿️',
|
||||
'porcupine': '🦔', 'llama': '🦙', 'giraffe': '🦒', 'zebra': '🦓', 'hippopotamus': '🦛', 'rhinoceros': '🦏', 'gorilla': '🦍', 'orangutan': '🦧', 'elephant': '🐘', 'camel': '🐫',
|
||||
'llama': '🦙', 'alpaca': '🦙', 'buffalo': '🐃', 'ox': '🐂', 'deer': '🦌', 'moose': '🦌', 'reindeer': '🦌', 'goat': '🐐', 'sheep': '🐑', 'ram': '🐏', 'lamb': '🐑', 'horse': '🐴',
|
||||
'unicorn': '🦄', 'zebra': '🦓', 'cow': '🐄', 'pig': '🐖', 'boar': '🐗', 'mouse': '🐁', 'rat': '🐀', 'hamster': '🐹', 'rabbit': '🐇', 'chipmunk': '🐿️', 'beaver': '🦫', 'hedgehog': '🦔',
|
||||
'bat': '🦇', 'bear': '🐻', 'koala': '🐨', 'panda': '🐼', 'sloth': '🦥', 'otter': '🦦', 'skunk': '🦨', 'kangaroo': '🦘', 'badger': '🦡', 'turkey': '🦃', 'chicken': '🐔', 'rooster': '🐓',
|
||||
'peacock': '🦚', 'parrot': '🦜', 'swan': '🦢', 'flamingo': '🦩', 'dodo': '🦤', 'crocodile': '🐊', 'turtle': '🐢', 'lizard': '🦎', 'snake': '🐍', 'dragon': '🐉', 'sauropod': '🦕', 't-rex': '🦖',
|
||||
'whale': '🐋', 'dolphin': '🐬', 'fish': '🐟', 'blowfish': '🐡', 'shark': '🦈', 'octopus': '🐙', 'shell': '🐚', 'crab': '🦀', 'lobster': '🦞', 'shrimp': '🦐', 'squid': '🦑', 'snail': '🐌', 'butterfly': '🦋',
|
||||
'bee': '🐝', 'beetle': '🐞', 'ant': '🐜', 'cricket': '🦗', 'spider': '🕷️', 'scorpion': '🦂', 'mosquito': '🦟', 'microbe': '🦠', 'locomotive': '🚂', 'arm': '💪', 'leg': '🦵', 'sponge': '🧽',
|
||||
'toothbrush': '🪥', 'broom': '🧹', 'basket': '🧺', 'roll of paper': '🧻', 'bucket': '🪣', 'soap': '🧼', 'toilet paper': '🧻', 'shower': '🚿', 'bathtub': '🛁', 'razor': '🪒', 'lotion': '🧴',
|
||||
'letter': '✉️', 'envelope': '✉️', 'mail': '📬', 'post': '📮', 'golf': '⛳️', 'golfing': '⛳️', 'office': '🏢', 'meeting': '📅', 'presentation': '📊', 'report': '📄', 'document': '📄',
|
||||
'file': '📁', 'folder': '📂', 'sports': '🏅', 'athlete': '🏃', 'competition': '🏆', 'race': '🏁', 'tournament': '🏆', 'champion': '🏆', 'medal': '🏅', 'victory': '🏆', 'win': '🏆', 'lose': '😞',
|
||||
'draw': '🤝', 'team': '👥', 'player': '👤', 'coach': '👨🏫', 'referee': '🧑⚖️', 'stadium': '🏟️', 'arena': '🏟️', 'field': '🏟️', 'court': '🏟️', 'track': '🏟️', 'gym': '🏋️', 'fitness': '🏋️', 'exercise': '🏋️',
|
||||
'workout': '🏋️', 'training': '🏋️', 'practice': '🏋️', 'game': '🎮', 'match': '🎮', 'score': '🏅', 'goal': '🥅', 'point': '🏅', 'basket': '🏀', 'home run': '⚾️', 'strike': '🎳', 'spare': '🎳', 'frame': '🎳',
|
||||
'inning': '⚾️', 'quarter': '🏈', 'half': '🏈', 'overtime': '🏈', 'penalty': '⚽️', 'foul': '⚽️', 'timeout': '⏱️', 'substitute': '🔄', 'bench': '🪑', 'sideline': '🏟️', 'dugout': '⚾️', 'locker room': '🚪', 'shower': '🚿',
|
||||
'uniform': '👕', 'jersey': '👕', 'cleats': '👟', 'helmet': '⛑️', 'pads': '🛡️', 'gloves': '🧤', 'bat': '⚾️', 'ball': '⚽️', 'puck': '🏒', 'stick': '🏒', 'net': '🥅', 'hoop': '🏀', 'goalpost': '🥅', 'whistle': '🔔',
|
||||
'scoreboard': '📊', 'fans': '👥', 'crowd': '👥', 'cheer': '📣', 'boo': '😠', 'applause': '👏', 'celebration': '🎉', 'parade': '🎉', 'trophy': '🏆', 'medal': '🏅', 'ribbon': '🎀', 'cup': '🏆', 'championship': '🏆',
|
||||
'league': '🏆', 'season': '🏆', 'playoffs': '🏆', 'finals': '🏆', 'champion': '🏆', 'runner-up': '🥈', 'third place': '🥉', 'snowman': '☃️', 'snowmen': '⛄️'
|
||||
}
|
||||
|
||||
return wordToEmojiMap
|
||||
|
||||
def sendWithEmoji(message):
|
||||
# this will take a string of text and replace any word or phrase that is in the word list with the corresponding emoji
|
||||
wordToEmojiMap = tableOfContents()
|
||||
# type format to clean it up
|
||||
words = message.split()
|
||||
i = 0
|
||||
while i < len(words):
|
||||
for phrase in sorted(wordToEmojiMap.keys(), key=len, reverse=True):
|
||||
phrase_words = phrase.split()
|
||||
# Strip punctuation from the words
|
||||
stripped_words = [word.lower().strip('.,!?') for word in words[i:i+len(phrase_words)]]
|
||||
if stripped_words == phrase_words:
|
||||
logger.debug(f"System: Replaced the phrase '{phrase}' with '{wordToEmojiMap[phrase]}'")
|
||||
words[i:i+len(phrase_words)] = [wordToEmojiMap[phrase]]
|
||||
i += len(phrase_words) - 1
|
||||
break
|
||||
# Check for plural forms
|
||||
elif stripped_words == [word + 's' for word in phrase_words]:
|
||||
logger.debug(f"System: Replaced the plural phrase '{' '.join([word + 's' for word in phrase_words])}' with '{wordToEmojiMap[phrase]}'")
|
||||
words[i:i+len(phrase_words)] = [wordToEmojiMap[phrase]]
|
||||
i += len(phrase_words) - 1
|
||||
break
|
||||
i += 1
|
||||
return ' '.join(words)
|
||||
|
||||
def tell_joke(nodeID=0):
|
||||
dadjoke = Dadjoke()
|
||||
|
||||
if dad_jokes_emojiJokes:
|
||||
renderedLaugh = sendWithEmoji(dadjoke.joke)
|
||||
else:
|
||||
renderedLaugh = dadjoke.joke
|
||||
return renderedLaugh
|
||||
|
||||
577
modules/games/lemonade.py
Normal file
577
modules/games/lemonade.py
Normal file
@@ -0,0 +1,577 @@
|
||||
# Port of https://github.com/tigerpointe/Lemonade-Stand/blob/main/lemonade.py MIT License Copyright (c) 2023 TigerPointe Software, LLC
|
||||
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
from collections import OrderedDict # ordered dictionaries
|
||||
from random import randrange, uniform # random numbers
|
||||
from types import SimpleNamespace # namespaces support
|
||||
import pickle # pickle file support
|
||||
import time # time functions
|
||||
from modules.log import * # mesh-bot logging
|
||||
|
||||
import locale # culture specific locale
|
||||
import math # math functions
|
||||
import re # regular expressions
|
||||
|
||||
# Set all of the locale category elements as default
|
||||
# ex. print(locale.currency(12345.67, grouping=True))
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
lemon_starting_cash = 30.00
|
||||
lemon_total_weeks = 7
|
||||
|
||||
lemonadeTracker = [{'nodeID': 0, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': lemon_starting_cash, 'start': lemon_starting_cash, 'cmd': 'new', 'time': time.time()}]
|
||||
lemonadeCups = [{'nodeID': 0, 'cost': 2.50, 'count': 25, 'min': 0.99, 'unit': 0.00}]
|
||||
lemonadeLemons = [{'nodeID': 0, 'cost': 4.00, 'count': 8, 'min': 2.00, 'unit': 0.00}]
|
||||
lemonadeSugar = [{'nodeID': 0, 'cost': 3.00, 'count': 15, 'min': 1.50, 'unit': 0.00}]
|
||||
lemonadeWeeks = [{'nodeID': 0, 'current': 1, 'total': lemon_total_weeks, 'sales': 99, 'potential': 0, 'unit': 0.00, 'price': 0.00, 'total_sales': 0}]
|
||||
lemonadeScore = [{'nodeID': 0, 'value': 0.00, 'total': 0.00}]
|
||||
|
||||
def get_sales_amount(potential, unit, price):
|
||||
"""Gets the sales amount.
|
||||
Multiply the potential sales by a ratio of unit cost to actual price; the
|
||||
exponent results in the values falling along a curve, rather than along a
|
||||
straight line, resulting in more realistic sales values at each price.
|
||||
Parameters
|
||||
potential : Potential sales
|
||||
unit : Unit cost
|
||||
price : Actual price
|
||||
"""
|
||||
return math.floor(potential * (unit / (price ** 1.5)))
|
||||
|
||||
def getHighScoreLemon():
|
||||
high_score = {"userID": 0, "cash": 0, "success": 0}
|
||||
# Load high score table
|
||||
try:
|
||||
with open('data/lemonstand.pkl', 'rb') as file:
|
||||
high_score = pickle.load(file)
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: Lemonade: No high score table found")
|
||||
# write a new high score file if one is not found
|
||||
with open('data/lemonstand.pkl', 'wb') as file:
|
||||
pickle.dump(high_score, file)
|
||||
return high_score
|
||||
|
||||
def start_lemonade(nodeID, message, celsius=False):
|
||||
global lemonadeTracker, lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore
|
||||
potential = 0
|
||||
unit = 0.0
|
||||
price = 0.0
|
||||
total_sales = 0
|
||||
|
||||
high_score = getHighScoreLemon()
|
||||
|
||||
def saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score):
|
||||
# save playerDB values
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cups'] = inventory.cups
|
||||
lemonadeTracker[i]['lemons'] = inventory.lemons
|
||||
lemonadeTracker[i]['sugar'] = inventory.sugar
|
||||
lemonadeTracker[i]['cash'] = inventory.cash
|
||||
lemonadeTracker[i]['start'] = inventory.start
|
||||
for i in range(len(lemonadeCups)):
|
||||
if lemonadeCups[i]['nodeID'] == nodeID:
|
||||
lemonadeCups[i]['cost'] = cups.cost
|
||||
lemonadeCups[i]['unit'] = cups.unit
|
||||
for i in range(len(lemonadeLemons)):
|
||||
if lemonadeLemons[i]['nodeID'] == nodeID:
|
||||
lemonadeLemons[i]['cost'] = lemons.cost
|
||||
lemonadeLemons[i]['unit'] = lemons.unit
|
||||
for i in range(len(lemonadeSugar)):
|
||||
if lemonadeSugar[i]['nodeID'] == nodeID:
|
||||
lemonadeSugar[i]['cost'] = sugar.cost
|
||||
lemonadeSugar[i]['unit'] = sugar.unit
|
||||
for i in range(len(lemonadeWeeks)):
|
||||
if lemonadeWeeks[i]['nodeID'] == nodeID:
|
||||
lemonadeWeeks[i]['current'] = weeks.current
|
||||
lemonadeWeeks[i]['total'] = weeks.total
|
||||
lemonadeWeeks[i]['sales'] = weeks.sales
|
||||
lemonadeWeeks[i]['potential'] = potential
|
||||
lemonadeWeeks[i]['unit'] = unit
|
||||
lemonadeWeeks[i]['price'] = price
|
||||
lemonadeWeeks[i]['total_sales'] = weeks.total_sales
|
||||
for i in range(len(lemonadeScore)):
|
||||
if lemonadeScore[i]['nodeID'] == nodeID:
|
||||
lemonadeScore[i]['value'] = score.value
|
||||
lemonadeScore[i]['total'] = score.total
|
||||
|
||||
def endGame(nodeID):
|
||||
# remove the player from the tracker
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker.pop(i)
|
||||
for i in range(len(lemonadeCups)):
|
||||
if lemonadeCups[i]['nodeID'] == nodeID:
|
||||
lemonadeCups.pop(i)
|
||||
for i in range(len(lemonadeLemons)):
|
||||
if lemonadeLemons[i]['nodeID'] == nodeID:
|
||||
lemonadeLemons.pop(i)
|
||||
for i in range(len(lemonadeSugar)):
|
||||
if lemonadeSugar[i]['nodeID'] == nodeID:
|
||||
lemonadeSugar.pop(i)
|
||||
for i in range(len(lemonadeWeeks)):
|
||||
if lemonadeWeeks[i]['nodeID'] == nodeID:
|
||||
lemonadeWeeks.pop(i)
|
||||
for i in range(len(lemonadeScore)):
|
||||
if lemonadeScore[i]['nodeID'] == nodeID:
|
||||
lemonadeScore.pop(i)
|
||||
logger.debug("System: Lemonade: Game Over for " + str(nodeID))
|
||||
|
||||
# Check for end of game
|
||||
if message.lower().startswith("e"):
|
||||
endGame(nodeID)
|
||||
return "Goodbye!👋"
|
||||
|
||||
title="LemonStand🍋"
|
||||
# Define the temperature unit symbols
|
||||
fahrenheit_unit = "ºF"
|
||||
celsius_unit = "ºC"
|
||||
|
||||
# Inventory data (contains the item levels)
|
||||
inventoryd = {
|
||||
'cups' : 0,
|
||||
'lemons' : 0,
|
||||
'sugar' : 0,
|
||||
'cash' : lemon_starting_cash,
|
||||
'start' : lemon_starting_cash
|
||||
}
|
||||
inventory = SimpleNamespace(**inventoryd)
|
||||
|
||||
# Cups data (includes a calculated cost per unit)
|
||||
cupsd = {
|
||||
'cost' : 2.50, # current price
|
||||
'count' : 25, # servings per box
|
||||
'min' : 0.99, # minimum price
|
||||
'unit' : 0.00 # unit price
|
||||
}
|
||||
cups = SimpleNamespace(**cupsd)
|
||||
cups.unit = round(cups.cost / cups.count, 2)
|
||||
|
||||
# Lemons data (includes a calculated cost per unit)
|
||||
lemonsd = {
|
||||
'cost' : 4.00, # current price
|
||||
'count' : 8, # servings per bag
|
||||
'min' : 2.00, # minimum price
|
||||
'unit' : 0.00 # unit price
|
||||
}
|
||||
lemons = SimpleNamespace(**lemonsd)
|
||||
lemons.unit = round(lemons.cost / lemons.count, 2)
|
||||
|
||||
# Sugar data (includes a calculated cost per unit)
|
||||
sugard = {
|
||||
'cost' : 3.00, # current price
|
||||
'count' : 15, # servings per bag
|
||||
'min' : 1.50, # minimum price
|
||||
'unit' : 0.00 # unit price
|
||||
}
|
||||
sugar = SimpleNamespace(**sugard)
|
||||
sugar.unit = round(sugar.cost / sugar.count, 2)
|
||||
|
||||
# Weeks data (measures the session duration)
|
||||
weeksd = {
|
||||
'current' : 1, # start with the 1st week
|
||||
'total' : 12, # span the 12 weeks of Summer
|
||||
'sales' : 99, # 99 maximum sales per week
|
||||
'total_sales' : 0, # total sales
|
||||
'summary' : [] # empty array
|
||||
}
|
||||
weeks = SimpleNamespace(**weeksd)
|
||||
|
||||
# Forecast data (includes percentage values, UTF8 glyphs and display names)
|
||||
forecastd = OrderedDict()
|
||||
forecastd['sunny'] = [1.00, 0x2600, "Sunny"]
|
||||
forecastd['partly'] = [0.90, 0x26C5, "Partly Sunny"]
|
||||
forecastd['cloudy'] = [0.70, 0x2601, "Mostly Cloudy"]
|
||||
forecastd['rainy'] = [0.40, 0x2602, "Rainy"]
|
||||
forecastd['stormy'] = [0.10, 0x26C8, "Stormy"]
|
||||
|
||||
# Temperature data (uses Fahrenheit as the percentage values)
|
||||
temperatured = {
|
||||
'min' : 69,
|
||||
'max' : 100,
|
||||
'units' : fahrenheit_unit,
|
||||
'forecast' : None,
|
||||
'value' : None
|
||||
}
|
||||
temperature = SimpleNamespace(**temperatured)
|
||||
|
||||
# Score data (based on actual vs. maximum net sales)
|
||||
scored = {
|
||||
'value' : 0.00,
|
||||
'total' : 0.00
|
||||
}
|
||||
score = SimpleNamespace(**scored)
|
||||
|
||||
# Check for Celsius
|
||||
if (celsius):
|
||||
temperature.units = celsius_unit
|
||||
|
||||
# load playerDB values
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
inventory.cups = lemonadeTracker[i]['cups']
|
||||
inventory.lemons = lemonadeTracker[i]['lemons']
|
||||
inventory.sugar = lemonadeTracker[i]['sugar']
|
||||
inventory.cash = lemonadeTracker[i]['cash']
|
||||
inventory.start = lemonadeTracker[i]['start']
|
||||
last_cmd = lemonadeTracker[i]['cmd']
|
||||
for i in range(len(lemonadeCups)):
|
||||
if lemonadeCups[i]['nodeID'] == nodeID:
|
||||
cups.cost = lemonadeCups[i]['cost']
|
||||
cups.unit = lemonadeCups[i]['unit']
|
||||
for i in range(len(lemonadeLemons)):
|
||||
if lemonadeLemons[i]['nodeID'] == nodeID:
|
||||
lemons.cost = lemonadeLemons[i]['cost']
|
||||
lemons.unit = lemonadeLemons[i]['unit']
|
||||
for i in range(len(lemonadeSugar)):
|
||||
if lemonadeSugar[i]['nodeID'] == nodeID:
|
||||
sugar.cost = lemonadeSugar[i]['cost']
|
||||
sugar.unit = lemonadeSugar[i]['unit']
|
||||
for i in range(len(lemonadeWeeks)):
|
||||
if lemonadeWeeks[i]['nodeID'] == nodeID:
|
||||
weeks.current = lemonadeWeeks[i]['current']
|
||||
weeks.total = lemonadeWeeks[i]['total']
|
||||
weeks.sales = lemonadeWeeks[i]['sales']
|
||||
potential = lemonadeWeeks[i]['potential']
|
||||
unit = lemonadeWeeks[i]['unit']
|
||||
price = lemonadeWeeks[i]['price']
|
||||
weeks.total_sales = lemonadeWeeks[i]['total_sales']
|
||||
for i in range(len(lemonadeScore)):
|
||||
if lemonadeScore[i]['nodeID'] == nodeID:
|
||||
score.value = lemonadeScore[i]['value']
|
||||
score.total = lemonadeScore[i]['total']
|
||||
|
||||
# Start the main loop
|
||||
if (weeks.current <= weeks.total):
|
||||
|
||||
if "new" in last_cmd:
|
||||
# set the last command to cups in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "cups"
|
||||
# Create a new display buffer for the text messages
|
||||
buffer= ""
|
||||
|
||||
# the current week number
|
||||
buffer += title + "Week #" + str(weeks.current) + "of" + str(weeks.total)
|
||||
|
||||
# Generate a random weather forecast and temperature and display
|
||||
temperature.forecast = randrange(0, len(forecastd))
|
||||
temperature.value = randrange(temperature.min, temperature.max)
|
||||
formatted = str(temperature.value)
|
||||
if (temperature.units == celsius_unit):
|
||||
formatted = str(round(((temperature.value - 32) * (5/9))))
|
||||
glyph = chr(forecastd[list(forecastd)[temperature.forecast]][1])
|
||||
buffer += ". " + \
|
||||
formatted + temperature.units + " " + \
|
||||
forecastd[list(forecastd)[temperature.forecast]][2] + \
|
||||
" " + glyph
|
||||
|
||||
# Calculate the potential sales as a percentage of the maximum value
|
||||
# (lower temperature = fewer sales, severe weather = fewer sales)
|
||||
forecast = forecastd[list(forecastd)[temperature.forecast]][0]
|
||||
potential = math.floor(weeks.sales * \
|
||||
(temperature.value / 100) * \
|
||||
forecast)
|
||||
|
||||
# Update the cups cost
|
||||
cups.cost = cups.cost + round(uniform(-1.50, 1.50), 2)
|
||||
if (cups.cost < cups.min):
|
||||
cups.cost = cups.min
|
||||
cups.unit = round(cups.cost / cups.count, 2)
|
||||
|
||||
# Update the lemons cost
|
||||
lemons.cost = lemons.cost + round(uniform(-1.50, 1.50), 2)
|
||||
if (lemons.cost < lemons.min):
|
||||
lemons.cost = lemons.min
|
||||
lemons.unit = round(lemons.cost / lemons.count, 2)
|
||||
|
||||
# Update the sugar cost
|
||||
sugar.cost = sugar.cost + round(uniform(-1.50, 1.50), 2)
|
||||
if (sugar.cost < sugar.min):
|
||||
sugar.cost = sugar.min
|
||||
sugar.unit = round(sugar.cost / sugar.count, 2)
|
||||
|
||||
# Calculate the unit cost and display the estimated sales from the forecast potential
|
||||
unit = cups.unit + lemons.unit + sugar.unit
|
||||
buffer += " SupplyCost" + locale.currency(unit, grouping=True) + " a cup."
|
||||
buffer += " Sales Potential:" + str(potential) + " cups."
|
||||
|
||||
# Display the current inventory
|
||||
buffer += " Inventory:"
|
||||
buffer += "🥤:" + str(inventory.cups)
|
||||
buffer += "🍋:" + str(inventory.lemons)
|
||||
buffer += "🍚:" + str(inventory.sugar)
|
||||
|
||||
# Display the updated item prices
|
||||
buffer += f"\nPrices: "
|
||||
buffer += "🥤:" + \
|
||||
locale.currency(cups.cost, grouping=True) + " 📦 of " + str(cups.count) + "."
|
||||
buffer += " 🍋:" + \
|
||||
locale.currency(lemons.cost, grouping=True) + " 🧺 of " + str(lemons.count) + "."
|
||||
buffer += " 🍚:" + \
|
||||
locale.currency(sugar.cost, grouping=True) + " bag for " + str(sugar.count) + "🥤."
|
||||
|
||||
# Display the current cash
|
||||
gainloss = inventory.cash - inventory.start
|
||||
buffer += " 💵:" + \
|
||||
locale.currency(inventory.cash, grouping=True)
|
||||
|
||||
|
||||
# if the player is in the red
|
||||
pnl = locale.currency(gainloss, grouping=True)
|
||||
if "0.00" not in pnl:
|
||||
if pnl.startswith("-"):
|
||||
buffer += "📊P&L📉" + pnl
|
||||
else:
|
||||
buffer += "📊P&L📈" + pnl
|
||||
|
||||
buffer += f"\n🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
return buffer
|
||||
|
||||
if "cups" in last_cmd:
|
||||
# Read the number of cup boxes to purchase
|
||||
newcups = -1
|
||||
if "n" in message.lower():
|
||||
message = "0"
|
||||
try:
|
||||
newcups = int(message)
|
||||
if (newcups > 0):
|
||||
cost = round(newcups * cups.cost, 2)
|
||||
if (cost > inventory.cash):
|
||||
return "You do not have enough 💵."
|
||||
inventory.cups += (newcups * cups.count)
|
||||
inventory.cash -= cost
|
||||
msg = "Purchased " + str(newcups) + " 📦 "
|
||||
msg += str(inventory.cups) + " 🥤 in inventory. " + locale.currency(inventory.cash, grouping=True) + f" remaining"
|
||||
else:
|
||||
msg = "No 🥤 were purchased"
|
||||
except Exception as e:
|
||||
return "invalid input, enter the number of 🥤 to purchase or (N)one"
|
||||
|
||||
# set the last command to lemons in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "lemons"
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
msg += f"\n 🍋 to buy? Have {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
|
||||
return msg
|
||||
|
||||
|
||||
if "lemons" in last_cmd:
|
||||
# Read the number of lemon bags to purchase
|
||||
newlemons = -1
|
||||
if "n" in message.lower():
|
||||
message = "0"
|
||||
try:
|
||||
newlemons = int(message)
|
||||
if (newlemons > 0):
|
||||
cost = round(newlemons * lemons.cost, 2)
|
||||
if (cost > inventory.cash):
|
||||
return "You do not have enough cash."
|
||||
inventory.lemons += (newlemons * lemons.count)
|
||||
inventory.cash -= cost
|
||||
msg = "Purchased " + str(newlemons) + " 🧺 "
|
||||
msg += str(inventory.lemons) + " 🍋 in inventory. " + locale.currency(inventory.cash, grouping=True) + f" remaining"
|
||||
else:
|
||||
msg = "No 🍋 were purchased"
|
||||
except Exception as e:
|
||||
newlemons = -1
|
||||
return "⛔️invalid input, enter the number of 🍋 to purchase"
|
||||
|
||||
# set the last command to sugar in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "sugar"
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
msg += f"\n 🍚 to buy? You have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
|
||||
return msg
|
||||
|
||||
if "sugar" in last_cmd:
|
||||
# Read the number of sugar bags to purchase
|
||||
newsugar = -1
|
||||
if "n" in message.lower():
|
||||
message = "0"
|
||||
try:
|
||||
newsugar = int(message)
|
||||
if (newsugar > 0):
|
||||
cost = round(newsugar * sugar.cost, 2)
|
||||
if (cost > inventory.cash):
|
||||
return "You do not have enough cash."
|
||||
inventory.sugar += (newsugar * sugar.count)
|
||||
inventory.cash -= cost
|
||||
msg = " Purchased " + str(newsugar) + " bag(s) of 🍚 for " + locale.currency(cost, grouping=True)
|
||||
msg += ". " + str(inventory.sugar) + f"🥤🍚 in inventory."
|
||||
else:
|
||||
msg = "No additional 🍚 was purchased"
|
||||
except Exception as e:
|
||||
return "⛔️invalid input, enter the number of 🍚 bags to purchase"
|
||||
|
||||
msg += f"Cost of goods is {locale.currency(unit, grouping=True)}"
|
||||
msg += f"per 🥤 {locale.currency(inventory.cash, grouping=True)} 💵 remaining."
|
||||
msg += f"\nPrice to Sell? or (G)rocery to buy more 🥤🍋🍚"
|
||||
|
||||
# set the last command to price in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "price"
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
return msg
|
||||
|
||||
if "price" in last_cmd:
|
||||
# set the last command to sales in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "sales"
|
||||
if "g" in message.lower():
|
||||
lemonadeTracker[i]['cmd'] = "cups"
|
||||
msg = f"#of🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
|
||||
return msg
|
||||
else:
|
||||
last_cmd = "sales"
|
||||
|
||||
# Read the actual price
|
||||
price = 0.00
|
||||
while (price <= 0.00):
|
||||
try:
|
||||
raw = message
|
||||
price = float(re.sub("[^0-9.-]", "", raw) or 0.00)
|
||||
if (price <= 0.00):
|
||||
return "The price must be greater than zero."
|
||||
except Exception as e:
|
||||
price = 0.00
|
||||
last_cmd = "price"
|
||||
return "⛔️Invalid input, enter the price of the lemonade per 🥤"
|
||||
|
||||
# this isnt sent to the user, not needed
|
||||
#msg = " Setting the price at " + locale.currency(price, grouping=True)
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
|
||||
|
||||
if "sales" in last_cmd:
|
||||
# Calculate the weekly sales based on price and lowest inventory level
|
||||
# (higher markup price = fewer sales, limited by the inventory on-hand)
|
||||
sales = get_sales_amount(potential, unit, price)
|
||||
sales = min(potential, sales, \
|
||||
inventory.cups, inventory.lemons, \
|
||||
inventory.sugar) # "min" returns lowest value
|
||||
margin = price - unit
|
||||
gross = sales * price
|
||||
net = sales * margin
|
||||
|
||||
# Add a new row to the summary
|
||||
weeks.summary.append({ 'sales' : sales, 'price' : price })
|
||||
|
||||
# Update the inventory levels to reflect consumption
|
||||
inventory.cups = inventory.cups - sales
|
||||
inventory.lemons = inventory.lemons - sales
|
||||
inventory.sugar = inventory.sugar - sales
|
||||
inventory.cash = inventory.cash + gross
|
||||
gainloss= inventory.cash - inventory.start
|
||||
|
||||
# Display the calculated sales information
|
||||
msg = "Results Week📊#" + str(weeks.current) + "of" + str(weeks.total)
|
||||
msg += " Cost/Price:" + locale.currency(unit, grouping=True) + "/" + locale.currency(price, grouping=True)
|
||||
msg += " P.Margin:" + locale.currency(margin, grouping=True)
|
||||
msg += " T.Sales:" + str(sales) + "@" + locale.currency(price, grouping=True)
|
||||
msg += " G.Profit: " + locale.currency(gross, grouping=True)
|
||||
msg += " N.Profit:" + locale.currency(net, grouping=True)
|
||||
|
||||
# Display the updated inventory levels
|
||||
msg += "\nRemaining"
|
||||
msg += " 🥤:" + str(inventory.cups)
|
||||
msg += " 🍋:" + str(inventory.lemons)
|
||||
msg += " 🍚:" + str(inventory.sugar)
|
||||
msg += " 💵:" + locale.currency(inventory.cash, grouping=True)
|
||||
# Display the gain/loss
|
||||
pnl = locale.currency(gainloss, grouping=True)
|
||||
if "0.00" not in pnl:
|
||||
if pnl.startswith("-"):
|
||||
msg += "📊P&L📉" + pnl
|
||||
else:
|
||||
msg += "📊P&L📈" + pnl
|
||||
|
||||
# Display the weekly sales summary
|
||||
pad_week = len(str(weeks.total))
|
||||
pad_sale = len(str(weeks.sales))
|
||||
total = 0
|
||||
msg += "\nWeekly📊"
|
||||
for i in range(len(weeks.summary)):
|
||||
msg += "#" + str(weeks.current).rjust(pad_week) + ". " + str(weeks.summary[i]['sales']).rjust(pad_sale) + \
|
||||
" sold x " + locale.currency(weeks.summary[i]['price'], grouping=True) + "ea. "
|
||||
total = total + weeks.summary[i]['sales']
|
||||
|
||||
# Update the total sales for the game
|
||||
weeks.total_sales += total
|
||||
|
||||
# Loop through a range of prices to find the highest net profit
|
||||
maxsales = 0
|
||||
maxprice = 0.00
|
||||
maxgross = 0.00
|
||||
maxnet = 0.00
|
||||
minnet = net
|
||||
for i in range(25, 2500, 25):
|
||||
price = i / 100 # range uses integers, not currency (floats)
|
||||
sales = get_sales_amount(potential, unit, price)
|
||||
margin = price - unit
|
||||
gross = sales * price
|
||||
net = sales * margin
|
||||
if (sales > 0) and \
|
||||
(sales <= potential) and \
|
||||
(unit <= price):
|
||||
if (net > maxnet):
|
||||
maxsales = sales
|
||||
maxprice = price
|
||||
maxgross = gross
|
||||
maxnet = net
|
||||
if (maxnet > minnet):
|
||||
msg += "Sales could have been:"
|
||||
msg += " " + str(maxsales) + " sold x " + locale.currency(maxprice, grouping=True) + "ea. @" + \
|
||||
locale.currency(maxgross, grouping=True) + " for a net profit of " + locale.currency(maxnet, grouping=True)
|
||||
if (inventory.cups <= 0):
|
||||
msg += " You ran out of cups.🥤"
|
||||
if (inventory.lemons <= 0):
|
||||
msg += " You ran out of lemons.🍋"
|
||||
if (inventory.sugar <= 0):
|
||||
msg += " You ran out of sugar.🍚"
|
||||
else:
|
||||
msg += "\nCongratulations 🍋🍋 your sales were perfect!🎉"
|
||||
|
||||
# Increment the score counters
|
||||
score.value = score.value + minnet
|
||||
score.total = score.total + maxnet
|
||||
|
||||
|
||||
# Increment the week number
|
||||
if (weeks.current == weeks.total):
|
||||
# end of the game
|
||||
success = round((score.value / score.total) * 100)
|
||||
msg += "\nYou've made " + locale.currency(score.value, grouping=True) + " out of a possible " + \
|
||||
locale.currency(score.total, grouping=True) + " for a score of " + str(success) + "% "
|
||||
msg += "You've sold " + str(weeks.total_sales) + " total 🥤🍋"
|
||||
|
||||
# check for high score
|
||||
high_score = getHighScoreLemon()
|
||||
if (inventory.cash > int(high_score['cash'])):
|
||||
msg += "\nCongratulations! You've set a new high score!🎉💰🍋"
|
||||
high_score['cash'] = inventory.cash
|
||||
high_score['success'] = success
|
||||
high_score['userID'] = nodeID
|
||||
with open('data/lemonstand.pkl', 'wb') as file:
|
||||
pickle.dump(high_score, file)
|
||||
endGame(nodeID)
|
||||
|
||||
else:
|
||||
# keep playing
|
||||
# set the last command to new in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "new"
|
||||
lemonadeTracker[i]['time'] = time.time()
|
||||
|
||||
weeks.current = weeks.current + 1
|
||||
|
||||
msg += f"Play another week🥤? or (E)nd Game"
|
||||
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
return msg
|
||||
255
modules/games/meshtrekker.py
Normal file
255
modules/games/meshtrekker.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
Mesh Trekker Game
|
||||
Game Rules:
|
||||
1. Players compete to cover the most distance over time using their Meshtastic devices.
|
||||
2. The game tracks players' movements via GPS coordinates sent by their devices.
|
||||
3. Total distance traveled is calculated and summed over time for each player.
|
||||
4. Leaderboards show top distances for daily, weekly, and all-time periods.
|
||||
5. Players can form teams, with team distances being the sum of all team members' distances.
|
||||
6. Special achievements are awarded for milestones (e.g., 10km, 50km, 100km total distance).
|
||||
7. The game runs continuously, allowing players to participate at their own pace.
|
||||
8. Players can use the 'whereami' command to check their current location and update their position in the game.
|
||||
"""
|
||||
|
||||
import pickle
|
||||
from modules.log import *
|
||||
from datetime import datetime, timedelta
|
||||
from geopy.distance import geodesic
|
||||
|
||||
class MeshTrekkerError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
pass
|
||||
|
||||
class DataLoadError(MeshTrekkerError):
|
||||
"""Raised when there's an error loading data."""
|
||||
pass
|
||||
|
||||
class DataSaveError(MeshTrekkerError):
|
||||
"""Raised when there's an error saving data."""
|
||||
pass
|
||||
|
||||
class InvalidGPSDataError(MeshTrekkerError):
|
||||
"""Raised when invalid GPS data is provided."""
|
||||
pass
|
||||
|
||||
class MeshTrekker:
|
||||
def __init__(self, data_file='mesh_trekker_data.pkl'):
|
||||
self.data_file = data_file
|
||||
try:
|
||||
self.data = self.load_data()
|
||||
except DataLoadError as e:
|
||||
logger.error(f"Failed to load data: {e}")
|
||||
self.data = self.initialize_data()
|
||||
|
||||
def initialize_data(self):
|
||||
return {
|
||||
'gps_data': {},
|
||||
'user_distances': {},
|
||||
'teams': {},
|
||||
'achievements': {},
|
||||
}
|
||||
|
||||
def load_data(self):
|
||||
try:
|
||||
with open(self.data_file, 'rb') as f:
|
||||
return pickle.load(f)
|
||||
except (pickle.PickleError, EOFError, FileNotFoundError) as e:
|
||||
logger.info(f"Data file {self.data_file} not found. Initializing new data.")
|
||||
return self.initialize_data()
|
||||
|
||||
def save_data(self):
|
||||
try:
|
||||
with open(self.data_file, 'wb') as f:
|
||||
pickle.dump(self.data, f)
|
||||
except (pickle.PickleError, IOError) as e:
|
||||
raise DataSaveError(f"Error saving data: {e}")
|
||||
|
||||
def validate_gps_data(self, latitude, longitude, timestamp):
|
||||
try:
|
||||
lat = float(latitude)
|
||||
lon = float(longitude)
|
||||
if not -90 <= lat <= 90:
|
||||
raise InvalidGPSDataError(f"Invalid latitude: {latitude}")
|
||||
if not -180 <= lon <= 180:
|
||||
raise InvalidGPSDataError(f"Invalid longitude: {longitude}")
|
||||
if not isinstance(timestamp, datetime):
|
||||
raise InvalidGPSDataError(f"Invalid timestamp: {timestamp}")
|
||||
except ValueError:
|
||||
raise InvalidGPSDataError(f"Invalid GPS data: latitude={latitude}, longitude={longitude}")
|
||||
|
||||
def process_gps_data(self, user_id, latitude, longitude, timestamp):
|
||||
try:
|
||||
self.validate_gps_data(latitude, longitude, timestamp)
|
||||
|
||||
if user_id not in self.data['gps_data']:
|
||||
self.data['gps_data'][user_id] = []
|
||||
|
||||
self.data['gps_data'][user_id].append((float(latitude), float(longitude), timestamp))
|
||||
|
||||
if len(self.data['gps_data'][user_id]) > 1:
|
||||
last_lat, last_lon, last_time = self.data['gps_data'][user_id][-2]
|
||||
last_point = (last_lat, last_lon)
|
||||
new_point = (float(latitude), float(longitude))
|
||||
|
||||
distance = geodesic(last_point, new_point).kilometers
|
||||
|
||||
if user_id not in self.data['user_distances']:
|
||||
self.data['user_distances'][user_id] = (0, timestamp)
|
||||
|
||||
total_distance, _ = self.data['user_distances'][user_id]
|
||||
new_total_distance = total_distance + distance
|
||||
self.data['user_distances'][user_id] = (new_total_distance, timestamp)
|
||||
|
||||
self.check_achievements(user_id, new_total_distance)
|
||||
|
||||
self.save_data()
|
||||
return new_total_distance
|
||||
except InvalidGPSDataError as e:
|
||||
logger.error(f"Invalid GPS data for user {user_id}: {e}")
|
||||
except DataSaveError as e:
|
||||
logger.error(f"Failed to save data after processing GPS for user {user_id}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error processing GPS data for user {user_id}: {e}")
|
||||
return None
|
||||
|
||||
def get_leaderboard(self, timeframe='all'):
|
||||
try:
|
||||
now = datetime.now()
|
||||
if timeframe == 'daily':
|
||||
start_time = now - timedelta(days=1)
|
||||
elif timeframe == 'weekly':
|
||||
start_time = now - timedelta(weeks=1)
|
||||
else:
|
||||
start_time = datetime.min
|
||||
|
||||
leaderboard = []
|
||||
for user_id, (distance, last_updated) in self.data['user_distances'].items():
|
||||
if last_updated > start_time:
|
||||
leaderboard.append((user_id, distance))
|
||||
|
||||
return sorted(leaderboard, key=lambda x: x[1], reverse=True)[:10]
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating leaderboard: {e}")
|
||||
return []
|
||||
|
||||
def get_team_leaderboard(self):
|
||||
try:
|
||||
team_distances = {}
|
||||
for team_name, members in self.data['teams'].items():
|
||||
team_distance = sum(self.data['user_distances'].get(member, (0, None))[0] for member in members)
|
||||
team_distances[team_name] = team_distance
|
||||
|
||||
return sorted(team_distances.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating team leaderboard: {e}")
|
||||
return []
|
||||
|
||||
def get_user_stats(self, user_id):
|
||||
try:
|
||||
distance, last_updated = self.data['user_distances'].get(user_id, (0, None))
|
||||
achievements = self.data['achievements'].get(user_id, [])
|
||||
return {
|
||||
'distance': distance,
|
||||
'last_updated': last_updated,
|
||||
'achievements': achievements
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving stats for user {user_id}: {e}")
|
||||
return None
|
||||
|
||||
def create_team(self, team_name, user_id):
|
||||
try:
|
||||
if team_name not in self.data['teams']:
|
||||
self.data['teams'][team_name] = [user_id]
|
||||
self.save_data()
|
||||
return True
|
||||
return False
|
||||
except DataSaveError as e:
|
||||
logger.error(f"Failed to save data after creating team {team_name}: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating team {team_name}: {e}")
|
||||
return False
|
||||
|
||||
def join_team(self, team_name, user_id):
|
||||
try:
|
||||
if team_name in self.data['teams'] and user_id not in self.data['teams'][team_name]:
|
||||
self.data['teams'][team_name].append(user_id)
|
||||
self.save_data()
|
||||
return True
|
||||
return False
|
||||
except DataSaveError as e:
|
||||
logger.error(f"Failed to save data after user {user_id} joined team {team_name}: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error joining team {team_name} for user {user_id}: {e}")
|
||||
return False
|
||||
|
||||
def check_achievements(self, user_id, total_distance):
|
||||
try:
|
||||
if user_id not in self.data['achievements']:
|
||||
self.data['achievements'][user_id] = []
|
||||
|
||||
milestones = [10, 50, 100, 500, 1000] # in km
|
||||
new_achievements = []
|
||||
for milestone in milestones:
|
||||
if total_distance >= milestone and milestone not in self.data['achievements'][user_id]:
|
||||
self.data['achievements'][user_id].append(milestone)
|
||||
new_achievements.append(milestone)
|
||||
logger.info(f"User {user_id} achieved {milestone}km milestone!")
|
||||
return new_achievements
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking achievements for user {user_id}: {e}")
|
||||
return []
|
||||
|
||||
def get_achievements(self, user_id):
|
||||
try:
|
||||
return self.data['achievements'].get(user_id, [])
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving achievements for user {user_id}: {e}")
|
||||
return []
|
||||
|
||||
# Initialize the game
|
||||
game = MeshTrekker()
|
||||
|
||||
def handle_meshtrekker(user_id, deviceID, channel_number, location_info=(0,0)):
|
||||
# Process GPS data from Meshtastic devices
|
||||
latitude, longitude = location_info.split(": ")[1].split(", ")
|
||||
|
||||
current_time = datetime.now()
|
||||
new_distance = game.process_gps_data(user_id, latitude, longitude, current_time)
|
||||
|
||||
|
||||
# # Create and join teams
|
||||
# game.create_team("Team A", "user1")
|
||||
# game.join_team("Team A", "user2")
|
||||
|
||||
# # Get individual leaderboard
|
||||
# print("\nAll-time individual leaderboard:")
|
||||
# for user, distance in game.get_leaderboard():
|
||||
# print(f"{user}: {distance:.2f} km")
|
||||
|
||||
# # Get team leaderboard
|
||||
# print("\nTeam leaderboard:")
|
||||
# for team, distance in game.get_team_leaderboard():
|
||||
# print(f"{team}: {distance:.2f} km")
|
||||
|
||||
# # Get user stats
|
||||
# user_stats = game.get_user_stats("user1")
|
||||
# print(f"\nUser1 stats: {user_stats}")
|
||||
|
||||
# # Get achievements
|
||||
# achievements = game.get_achievements("user1")
|
||||
# print(f"User1 achievements: {achievements}")
|
||||
|
||||
|
||||
if new_distance is not None:
|
||||
new_achievements = game.check_achievements(user_id, new_distance)
|
||||
response = f"{location_info}\nTotal distance: {new_distance:.2f} km"
|
||||
if new_achievements:
|
||||
response += f"\nNew achievements: {', '.join([f'{a}km' for a in new_achievements])}"
|
||||
else:
|
||||
response = f"{location_info}\nFailed to update distance. Please try again."
|
||||
|
||||
return response
|
||||
|
||||
340
modules/games/mmind.py
Normal file
340
modules/games/mmind.py
Normal file
@@ -0,0 +1,340 @@
|
||||
# https://github.com/pwdkramer/pythonMastermind/blob/main/main.py
|
||||
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
import random
|
||||
import time
|
||||
import pickle
|
||||
from modules.log import *
|
||||
|
||||
mindTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'secret_code': '', 'diff': 'n', 'turns': 1}]
|
||||
|
||||
def chooseDifficultyMMind(message):
|
||||
usrInput = message.lower()
|
||||
msg = ''
|
||||
valid_colorsMMind = "RYGB"
|
||||
|
||||
if not usrInput.startswith("n") and not usrInput.startswith("h") and not usrInput.startswith("x"):
|
||||
# default to normal difficulty
|
||||
usrInput = "n"
|
||||
|
||||
if usrInput == "n":
|
||||
msg += f"The colors to choose from are:\nR🔴, Y🟡, G🟢, B🔵"
|
||||
elif usrInput == "h":
|
||||
valid_colorsMMind += "OP"
|
||||
msg += f"The colors to choose from are\nR🔴, Y🟡, G🟢, B🔵, O🟠, P🟣"
|
||||
elif usrInput == "x":
|
||||
valid_colorsMMind += "OPWK"
|
||||
msg += f"The colors to choose from are\nR🔴, Y🟡, G🟢, B🔵, O🟠, P🟣, W⚪, K⚫"
|
||||
return msg
|
||||
|
||||
|
||||
#possible colors on nomral: Red, Yellow, Green, Blue
|
||||
#added colors on hard: Orange, Purple
|
||||
def makeCodeMMind(diff):
|
||||
secret_code = ""
|
||||
for i in range(4):
|
||||
if diff == "n":
|
||||
roll = random.randrange(1, 5)
|
||||
elif diff == "h":
|
||||
roll = random.randrange(1,7)
|
||||
elif diff == "x":
|
||||
roll = random.randrange(1,9)
|
||||
else:
|
||||
print("Difficulty error in makeCode()")
|
||||
if roll == 1:
|
||||
secret_code += "R"
|
||||
elif roll == 2:
|
||||
secret_code += "Y"
|
||||
elif roll == 3:
|
||||
secret_code += "G"
|
||||
elif roll == 4:
|
||||
secret_code += "B"
|
||||
elif roll == 5:
|
||||
secret_code += "O"
|
||||
elif roll == 6:
|
||||
secret_code += "P"
|
||||
elif roll == 7:
|
||||
secret_code += "W"
|
||||
elif roll == 8:
|
||||
secret_code += "K"
|
||||
else:
|
||||
print("Error with range of roll in makeCode()")
|
||||
return secret_code
|
||||
|
||||
#get guess from user
|
||||
def getGuessMMind(diff, guess):
|
||||
msg = ''
|
||||
if diff == "n":
|
||||
valid_colorsMMind = "RYGB"
|
||||
elif diff == "h":
|
||||
valid_colorsMMind = "RYGBOP"
|
||||
elif diff == "x":
|
||||
valid_colorsMMind = "RYGBOPWK"
|
||||
|
||||
user_guess = guess.upper()
|
||||
valid_guess = True
|
||||
if len(user_guess) != 4:
|
||||
valid_guess = False
|
||||
for i in range(len(user_guess)):
|
||||
if user_guess[i] not in valid_colorsMMind:
|
||||
valid_guess = False
|
||||
if valid_guess == False:
|
||||
user_guess = "XXXX"
|
||||
return user_guess
|
||||
|
||||
def getHighScoreMMind(nodeID, turns, diff):
|
||||
# check if player is in high score list and pick the lowest score
|
||||
try:
|
||||
with open('mmind_hs.pkl', 'rb') as f:
|
||||
mindHighScore = pickle.load(f)
|
||||
except:
|
||||
logger.debug("System: MasterMind: High Score file not found.")
|
||||
mindHighScore = [{'nodeID': nodeID, 'turns': turns, 'diff': diff}]
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
|
||||
if nodeID == 0:
|
||||
# just return the high score
|
||||
return mindHighScore
|
||||
|
||||
# calculate lowest score
|
||||
lowest_score = mindHighScore[0]['turns']
|
||||
|
||||
if mindHighScore[0]['diff'] == "n" and diff == "n":
|
||||
if lowest_score > turns:
|
||||
# update the high score for normal if new score is lower
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "n" and diff == "h":
|
||||
# update the high score for hard if normal is the only high score
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "h" and diff == "h":
|
||||
if lowest_score > turns:
|
||||
# update the high score for hard if new score is lower
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "n" or mindHighScore[0]['diff'] == "h" and diff == "x":
|
||||
# update the high score for expert if normal or high is the only high score
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "x" and diff == "x":
|
||||
if lowest_score > turns:
|
||||
# update the high score for expert if new score is lower
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
return 0
|
||||
|
||||
|
||||
def getEmojiMMind(secret_code):
|
||||
# for each letter in the secret code, convert to emoji for display
|
||||
secret_code = secret_code.upper()
|
||||
secret_code_emoji = ""
|
||||
for i in range(len(secret_code)):
|
||||
if secret_code[i] == "R":
|
||||
secret_code_emoji += "🔴"
|
||||
elif secret_code[i] == "Y":
|
||||
secret_code_emoji += "🟡"
|
||||
elif secret_code[i] == "G":
|
||||
secret_code_emoji += "🟢"
|
||||
elif secret_code[i] == "B":
|
||||
secret_code_emoji += "🔵"
|
||||
elif secret_code[i] == "O":
|
||||
secret_code_emoji += "🟠"
|
||||
elif secret_code[i] == "P":
|
||||
secret_code_emoji += "🟣"
|
||||
elif secret_code[i] == "W":
|
||||
secret_code_emoji += "⚪"
|
||||
elif secret_code[i] == "K":
|
||||
secret_code_emoji += "⚫"
|
||||
elif secret_code[i] == "X":
|
||||
secret_code_emoji += "❌"
|
||||
return secret_code_emoji
|
||||
|
||||
#compare userGuess with secret code and provide feedback
|
||||
def compareCodeMMind(secret_code, user_guess):
|
||||
game_won = False
|
||||
perfect_pins = 0
|
||||
wrong_position = 0
|
||||
msg = ''
|
||||
#logger.debug("System: MasterMind: secret_code: " + str(secret_code) + " user_guess: " + str(user_guess))
|
||||
if secret_code == user_guess: #correct guess, user wins
|
||||
perfect_pins = 4
|
||||
game_won = True
|
||||
else:
|
||||
# check for perfect pins and right color wrong position
|
||||
temp_code = []
|
||||
temp_guess = []
|
||||
# Check for perfect pins
|
||||
for i in range(len(user_guess)):
|
||||
if user_guess[i] == secret_code[i]:
|
||||
perfect_pins += 1
|
||||
else:
|
||||
temp_code.append(secret_code[i])
|
||||
temp_guess.append(user_guess[i])
|
||||
|
||||
# Check for right color wrong position
|
||||
for guess in temp_guess:
|
||||
if guess in temp_code:
|
||||
wrong_position += 1
|
||||
temp_code.remove(guess) # Remove the first occurrence of the matched color
|
||||
# display feedback
|
||||
if game_won:
|
||||
msg += f"Correct{getEmojiMMind(user_guess)}\n"
|
||||
else:
|
||||
msg += f"Guess{getEmojiMMind(user_guess)}\n"
|
||||
|
||||
if perfect_pins > 0 and game_won == False:
|
||||
msg += "✅ color ✅ position: {}".format(perfect_pins)
|
||||
|
||||
if wrong_position > 0:
|
||||
if "✅" in msg: msg += f"\n"
|
||||
msg += "✅ color 🚫 position: {}".format(wrong_position)
|
||||
|
||||
if "✅" not in msg and game_won == False:
|
||||
msg += "🚫No pins in your guess😿 are in the code!"
|
||||
|
||||
return msg
|
||||
|
||||
#game loop with turn counter
|
||||
def playGameMMind(diff, secret_code, turn_count, nodeID, message):
|
||||
msg = ''
|
||||
won = False
|
||||
if turn_count <= 10:
|
||||
user_guess = getGuessMMind(diff, message)
|
||||
if user_guess == "XXXX":
|
||||
msg += f"⛔️Invalid guess. Please enter 4 valid colors letters.\n🔴🟢🔵🔴 is RGBR"
|
||||
return msg
|
||||
check_guess = compareCodeMMind(secret_code, user_guess)
|
||||
|
||||
# display turn count and feedback
|
||||
msg += "Turn {}:".format(turn_count)
|
||||
if check_guess.startswith("Correct"):
|
||||
won = True
|
||||
msg += check_guess
|
||||
|
||||
if won == True:
|
||||
msg += f"\n🎉🧠 you win 🥷🤯"
|
||||
# get high score
|
||||
high_score = getHighScoreMMind(nodeID, turn_count, diff)
|
||||
if high_score != 0:
|
||||
msg += f"\n🏆 High Score:{high_score[0]['turns']} turns, Difficulty:{high_score[0]['diff'].upper()}"
|
||||
|
||||
msg += "\nWould you like to play again?\n(N)ormal, (H)ard, e(X)pert (E)nd?"
|
||||
# reset turn count in tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['turns'] = 1
|
||||
mindTracker[i]['secret_code'] = ''
|
||||
mindTracker[i]['cmd'] = 'new'
|
||||
else:
|
||||
# increment turn count and keep playing
|
||||
turn_count += 1
|
||||
# store turn count in tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['turns'] = turn_count
|
||||
elif won == False:
|
||||
msg += f"🙉Game Over🙈\nThe code was: {getEmojiMMind(secret_code)}"
|
||||
msg += "\nYou have run out of turns.😿"
|
||||
msg += "\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
|
||||
# reset turn count in tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['turns'] = 1
|
||||
mindTracker[i]['secret_code'] = ''
|
||||
mindTracker[i]['cmd'] = 'new'
|
||||
|
||||
return msg
|
||||
|
||||
def endGameMMind(nodeID):
|
||||
global mindTracker
|
||||
# remove player from tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
del mindTracker[i]
|
||||
logger.debug("System: MasterMind: Player removed: " + str(nodeID))
|
||||
break
|
||||
|
||||
#main game
|
||||
def start_mMind(nodeID, message):
|
||||
global mindTracker
|
||||
last_cmd = ""
|
||||
msg = ''
|
||||
|
||||
# get player's last command from tracker if not new player
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
last_cmd = mindTracker[i]['cmd']
|
||||
|
||||
if last_cmd == "new":
|
||||
if message.lower().startswith("n") or message.lower().startswith("h") or message.lower().startswith("x"):
|
||||
diff = message.lower()[0]
|
||||
else:
|
||||
diff = "n"
|
||||
|
||||
# set player's last command to makeCode
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['cmd'] = 'makeCode'
|
||||
mindTracker[i]['diff'] = diff
|
||||
# Return color message to player
|
||||
msg += chooseDifficultyMMind(message.lower()[0])
|
||||
return msg
|
||||
|
||||
if last_cmd == "makeCode":
|
||||
# get difficulty from tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
diff = mindTracker[i]['diff']
|
||||
|
||||
secret_code = makeCodeMMind(diff)
|
||||
last_cmd = "playGame"
|
||||
# set player's last command to playGame
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['cmd'] = 'playGame'
|
||||
mindTracker[i]['secret_code'] = secret_code
|
||||
mindTracker[i]['last_played'] = time.time()
|
||||
|
||||
if last_cmd == "playGame":
|
||||
# get difficulty, secret code, and turn count from tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
diff = mindTracker[i]['diff']
|
||||
secret_code = mindTracker[i]['secret_code']
|
||||
turn_count = mindTracker[i]['turns']
|
||||
|
||||
msg += playGameMMind(diff, secret_code, turn_count, nodeID=nodeID, message=message)
|
||||
|
||||
return msg
|
||||
450
modules/games/videopoker.py
Normal file
450
modules/games/videopoker.py
Normal file
@@ -0,0 +1,450 @@
|
||||
# Port of https://github.com/devtronvarma/Video-Poker-Terminal-Game
|
||||
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
import random
|
||||
import time
|
||||
import pickle
|
||||
from modules.log import *
|
||||
|
||||
vpStartingCash = 20
|
||||
vpTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0}]
|
||||
|
||||
# Define the Card class
|
||||
class CardVP:
|
||||
|
||||
card_values = { # value of the ace is high until it needs to be low
|
||||
2: 2,
|
||||
3: 3,
|
||||
4: 4,
|
||||
5: 5,
|
||||
6: 6,
|
||||
7: 7,
|
||||
8: 8,
|
||||
9: 9,
|
||||
10: 10,
|
||||
'Jack': 11,
|
||||
'Queen': 12,
|
||||
'King': 13,
|
||||
'Ace': 14
|
||||
}
|
||||
|
||||
def __init__(self, suit, rank):
|
||||
"""
|
||||
:param suit: The face of the card, e.g. Spade or Diamond
|
||||
:param rank: The value of the card, e.g 3 or King
|
||||
"""
|
||||
self.suit = suit.capitalize()
|
||||
self.rank = rank
|
||||
self.points = self.card_values[rank]
|
||||
|
||||
# Function to output ascii version of the cards in a hand in the terminal
|
||||
def drawCardsVp(*cards, return_string=True):
|
||||
"""
|
||||
Instead of a boring text version of the card we render an ASCII image of the card.
|
||||
:param cards: One or more card objects
|
||||
:param return_string: By default we return the string version of the card, but the dealer hide the 1st card and we
|
||||
keep it as a list so that the dealer can add a hidden card in front of the list
|
||||
"""
|
||||
# we will use this to prints the appropriate icons for each card
|
||||
suits_name = ['Spades', 'Diamonds', 'Hearts', 'Clubs']
|
||||
suits_symbols = ['♠️', '♦️', '♥️', '♣️']
|
||||
|
||||
# create an empty list of list, each sublist is a line 2 lines for the card
|
||||
lines = [[] for i in range(1)]
|
||||
|
||||
for index, card in enumerate(cards):
|
||||
# "King" should be "K" and "10" should still be "10"
|
||||
if card.rank == 10: # ten is the only one who's rank is 2 char long
|
||||
rank = str(card.rank)
|
||||
else:
|
||||
rank = str(card.rank)[0] # some have a rank of 'King' this changes that to a simple 'K' ("King" doesn't fit)
|
||||
# get the cards suit in two steps
|
||||
suit = suits_name.index(card.suit)
|
||||
suit = suits_symbols[suit]
|
||||
|
||||
# add the individual card on a line by line basis
|
||||
lines[0].append('{}{} '.format(rank, suit))
|
||||
|
||||
result = []
|
||||
#result.append('1 2 3 4 5') # add the index for the cards to top row
|
||||
for index, line in enumerate(lines):
|
||||
result.append(''.join(lines[index]))
|
||||
|
||||
# hidden cards do not use string
|
||||
if return_string:
|
||||
return '\n'.join(result)
|
||||
else:
|
||||
return result
|
||||
|
||||
# Define Deck class
|
||||
class DeckVP:
|
||||
|
||||
def __init__(self):
|
||||
self.cards = []
|
||||
self.build()
|
||||
|
||||
# method for building the deck
|
||||
def build(self):
|
||||
for s in ['Spades', 'Diamonds', 'Hearts', 'Clubs']:
|
||||
for v in range(2, 11):
|
||||
self.cards.append(CardVP(s,v))
|
||||
for c in ["Jack", "Queen", "King", "Ace"]:
|
||||
self.cards.append(CardVP(s,c))
|
||||
|
||||
# method to show cards in deck
|
||||
def display(self):
|
||||
for c in self.cards:
|
||||
print(drawCardsVp(c))
|
||||
|
||||
# method to shuffle cards in deck
|
||||
def shuffle(self):
|
||||
for i in range(len(self.cards) - 1, 0, -1):
|
||||
r = random.randint(0, i)
|
||||
self.cards[i], self.cards[r] = self.cards[r], self.cards[i]
|
||||
|
||||
# method to draw card from the deck
|
||||
def draw_card(self):
|
||||
return self.cards.pop()
|
||||
|
||||
# Define Player Class
|
||||
class PlayerVP:
|
||||
def __init__(self):
|
||||
self.hand = []
|
||||
self.bankroll = 20
|
||||
|
||||
# Method for initial five-card draw
|
||||
def draw_cards(self, deck):
|
||||
for i in range(5):
|
||||
self.hand.append(deck.draw_card())
|
||||
return self
|
||||
|
||||
# Method for displaying player's hand
|
||||
def show_hand(self):
|
||||
msg = (drawCardsVp(
|
||||
self.hand[0],
|
||||
self.hand[1],
|
||||
self.hand[2],
|
||||
self.hand[3],
|
||||
self.hand[4]))
|
||||
return msg
|
||||
|
||||
# Method for placing a bet
|
||||
def bet(self, ammount=0):
|
||||
bet = int(ammount)
|
||||
self.bet_size = bet
|
||||
self.bankroll -= self.bet_size
|
||||
|
||||
# Method for selecting cards to redraw
|
||||
def redraw(self, deck, message):
|
||||
# if message has single digit, then it is the card to redraw, else it is the list of cards to redraw with a comma
|
||||
if len(message) == 1:
|
||||
try:
|
||||
# if single digit is the letter a redraw all cards
|
||||
if message.lower() == "a":
|
||||
for i in range(5):
|
||||
self.hand[i] = deck.draw_card()
|
||||
else:
|
||||
# error trap for bad input
|
||||
redraw_index = int(message) - 1
|
||||
self.hand[redraw_index] = deck.draw_card()
|
||||
|
||||
return self.show_hand()
|
||||
except Exception as e:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
# error trap for bad input
|
||||
if "," in message:
|
||||
redraw_list = [int(x) - 1 for x in message.split(',')]
|
||||
if "." in message:
|
||||
redraw_list = [int(x) - 1 for x in message.split('.')]
|
||||
if " " in message:
|
||||
redraw_list = [int(x) - 1 for x in message.split(' ')]
|
||||
for i in redraw_list:
|
||||
self.hand[i] = deck.draw_card()
|
||||
return self.show_hand()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return "ex:1,3,4 deals them new, and keeps 2,5 or (N)o to keep current (H)and"
|
||||
|
||||
# Method for scoring hand, calculating winnings, and outputting message
|
||||
def score_hand(self, resetHand = True):
|
||||
points = sorted([self.hand[i].points for i in range(5)])
|
||||
suits = [self.hand[i].suit for i in range(5)]
|
||||
points_repeat = [points.count(i) for i in points]
|
||||
suits_repeat = [suits.count(i) for i in suits]
|
||||
diff = max(points) - min(points)
|
||||
hand_name = ""
|
||||
msg = ""
|
||||
payoff = {
|
||||
"👑Royal Flush🚽": 10,
|
||||
"🧻Straight Flush🚽": 9,
|
||||
"Flush🚽": 8,
|
||||
"Full House🏠": 7,
|
||||
"Four of a Kind👯👯": 6,
|
||||
"Three of a Kind☘️": 5,
|
||||
"Two Pair👯👯": 4,
|
||||
"Straight📏": 3,
|
||||
"Pair👯": 2,
|
||||
"Bad Hand 🙈": -1,
|
||||
}
|
||||
|
||||
if 5 in suits_repeat:
|
||||
if points == [10, 11, 12, 13, 14]: #find royal flush
|
||||
hand_name = "👑Royal Flush🚽"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
elif diff == 4 and max(points_repeat) == 1: # find straight flush w/o ace low
|
||||
hand_name = "🧻Straight Flush🚽"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
elif diff == 12 and points[4] == 14: # find straight flush w/ace low
|
||||
check = 0
|
||||
for i in range(1, 4):
|
||||
check += points[i] - points[i - 1]
|
||||
if check == 3:
|
||||
hand_name = "🧻Straight Flush🚽"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
else:
|
||||
hand_name = "Flush🚽"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
else:
|
||||
hand_name = "Flush🚽"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
elif sorted(points_repeat) == [2,2,3,3,3]: # find full house
|
||||
hand_name = "Full House🏠"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
elif 4 in points_repeat: # find four of a kind
|
||||
hand_name = "Four of a Kind👯👯"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
elif 3 in points_repeat: # find three of a kind
|
||||
hand_name = "Three of a Kind☘️"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
elif points_repeat.count(2) == 4: # find two-pair
|
||||
hand_name = "Two Pair👯👯"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
elif 2 in points_repeat: # find pair
|
||||
hand_name = "Pair👯"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
elif diff == 4 and max(points_repeat) == 1: # find straight w/o ace low
|
||||
hand_name = "Straight📏"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
elif diff == 12 and points[4] == 14: # find straight w/ace low
|
||||
check = 0
|
||||
for i in range(1, 4):
|
||||
check += points[i] - points[i - 1]
|
||||
if check == 3:
|
||||
hand_name = "Straight📏"
|
||||
if resetHand:
|
||||
self.bankroll += self.bet_size * payoff[hand_name]
|
||||
else:
|
||||
hand_name = "Bad Hand 🙈"
|
||||
else: # for everything Hand
|
||||
hand_name = "Bad Hand 🙈"
|
||||
|
||||
if resetHand:
|
||||
self.hand = []
|
||||
msg = f"\nYour hand, {hand_name}. Your bankroll is now {self.bankroll} coins."
|
||||
else:
|
||||
if hand_name != "":
|
||||
msg = f"\nShowing:{hand_name}"
|
||||
return msg
|
||||
|
||||
|
||||
def getLastCmdVp(nodeID):
|
||||
last_cmd = ""
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
last_cmd = vpTracker[i]['cmd']
|
||||
return last_cmd
|
||||
|
||||
def setLastCmdVp(nodeID, cmd):
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['cmd'] = cmd
|
||||
|
||||
def saveHSVp(nodeID, highScore):
|
||||
# Save the game high_score to pickle
|
||||
highScore = {'nodeID': nodeID, 'highScore': highScore}
|
||||
try:
|
||||
with open('data/videopoker_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: BlackJack: Creating new data/videopoker_hs.pkl file")
|
||||
with open('data/videopoker_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
|
||||
def loadHSVp():
|
||||
# Load the game high_score from pickle
|
||||
try:
|
||||
with open('data/videopoker_hs.pkl', 'rb') as file:
|
||||
highScore = pickle.load(file)
|
||||
return highScore
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: VideoPoker: Creating new data/videopoker_hs.pkl file")
|
||||
highScore = {'nodeID': 0, 'highScore': 0}
|
||||
with open('data/videopoker_hs.pkl', 'wb') as file:
|
||||
pickle.dump(highScore, file)
|
||||
return 0
|
||||
|
||||
def playVideoPoker(nodeID, message):
|
||||
msg = ""
|
||||
|
||||
# Initialize the player
|
||||
if getLastCmdVp(nodeID) is None or getLastCmdVp(nodeID=nodeID) == "":
|
||||
# create new player if not in tracker
|
||||
logger.debug(f"System: VideoPoker: New Player {nodeID}")
|
||||
vpTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0})
|
||||
return f"Welcome to 🎰VideoPoker♥️ you have {vpStartingCash} coins, Whats your bet?"
|
||||
|
||||
# Gather the player's bet
|
||||
if getLastCmdVp(nodeID) == "new" or getLastCmdVp(nodeID) == "gameOver":
|
||||
# Initialize shuffled Deck and Player
|
||||
player = PlayerVP()
|
||||
deck = DeckVP()
|
||||
deck.shuffle()
|
||||
drawCount = 1
|
||||
bet = 0
|
||||
msg = ''
|
||||
|
||||
# load the player bankroll from tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
player.bankroll = vpTracker[i]['cash']
|
||||
vpTracker[i]['time'] = time.time()
|
||||
|
||||
# Detect if message is a bet
|
||||
try:
|
||||
bet = int(message)
|
||||
except ValueError:
|
||||
msg += f"Please enter a valid bet, 1 to 5 coins. you have {player.bankroll} coins."
|
||||
|
||||
# Check if bet is valid
|
||||
if bet > player.bankroll:
|
||||
msg += f"You can only bet the money you have. {player.bankroll} coins, No strip poker here..."
|
||||
elif bet < 1:
|
||||
msg += "You must bet at least 1 coin.🪙"
|
||||
elif bet > 5:
|
||||
msg += "The 🎰 coin slot only fits 5 coins max."
|
||||
|
||||
# if msg contains an error, return it
|
||||
if msg is not None and msg != '':
|
||||
return msg
|
||||
else:
|
||||
# Take the bet
|
||||
player.bet(str(message))
|
||||
# Bet placed, start the game
|
||||
setLastCmdVp(nodeID, "playing")
|
||||
|
||||
# save player and deck to tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['player'] = player
|
||||
vpTracker[i]['deck'] = deck
|
||||
vpTracker[i]['cash'] = player.bankroll
|
||||
|
||||
# Play the game
|
||||
if getLastCmdVp(nodeID) == "playing":
|
||||
msg = ''
|
||||
|
||||
player.draw_cards(deck)
|
||||
msg += player.show_hand()
|
||||
# give hint to player
|
||||
msg += player.score_hand(resetHand=False)
|
||||
|
||||
# save player and deck to tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['player'] = player
|
||||
vpTracker[i]['deck'] = deck
|
||||
vpTracker[i]['drawCount'] = drawCount
|
||||
|
||||
msg += f"\nDeal new card? \nex: 1,3,4 or (N)o,(A)ll (H)and"
|
||||
setLastCmdVp(nodeID, "redraw")
|
||||
return msg
|
||||
|
||||
if getLastCmdVp(nodeID) == "redraw":
|
||||
msg = ''
|
||||
# load the player and deck from tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
player = vpTracker[i]['player']
|
||||
deck = vpTracker[i]['deck']
|
||||
drawCount = vpTracker[i]['drawCount']
|
||||
|
||||
# if player wants to redraw cards, and not done already
|
||||
if message.lower().startswith("n"):
|
||||
setLastCmdVp(nodeID, "endGame")
|
||||
if message.lower().startswith("h"):
|
||||
msg = player.show_hand()
|
||||
return msg
|
||||
else:
|
||||
if drawCount <= 1:
|
||||
msg = player.redraw(deck, message)
|
||||
if msg.startswith("Send Card"):
|
||||
# if returned error message, return it
|
||||
return msg
|
||||
drawCount += 1
|
||||
# save player and deck to tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['player'] = player
|
||||
vpTracker[i]['deck'] = deck
|
||||
vpTracker[i]['drawCount'] = drawCount
|
||||
if drawCount == 2:
|
||||
# this is the last draw will carry on to endGame for scoring
|
||||
msg = player.redraw(deck, message) + f"\n"
|
||||
if msg.startswith("Send Card"):
|
||||
# if returned error message, return it
|
||||
return msg
|
||||
# redraw done
|
||||
setLastCmdVp(nodeID, "endGame")
|
||||
else:
|
||||
# show redrawn hand
|
||||
return msg
|
||||
else:
|
||||
# redraw already done
|
||||
setLastCmdVp(nodeID, "endGame")
|
||||
|
||||
if getLastCmdVp(nodeID) == "endGame":
|
||||
# load the player and deck from tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
player = vpTracker[i]['player']
|
||||
deck = vpTracker[i]['deck']
|
||||
|
||||
msg += player.score_hand()
|
||||
|
||||
if player.bankroll < 1:
|
||||
player.bankroll = vpStartingCash
|
||||
msg += "\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
|
||||
elif player.bankroll > vpTracker[i]['highScore']:
|
||||
vpTracker[i]['highScore'] = player.bankroll
|
||||
msg += " 🎉HighScore!"
|
||||
# save high score
|
||||
saveHSVp(nodeID, vpTracker[i]['highScore'])
|
||||
|
||||
msg += f"\nPlace your Bet, or (L)eave Table."
|
||||
|
||||
setLastCmdVp(nodeID, "gameOver")
|
||||
# reset player and deck in tracker
|
||||
for i in range(len(vpTracker)):
|
||||
if vpTracker[i]['nodeID'] == nodeID:
|
||||
vpTracker[i]['player'] = None
|
||||
vpTracker[i]['deck'] = None
|
||||
vpTracker[i]['drawCount'] = 0
|
||||
# save bankroll
|
||||
vpTracker[i]['cash'] = player.bankroll
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
231
modules/llm.py
Normal file
231
modules/llm.py
Normal file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
# LLM Module for meshing-around
|
||||
# This module is used to interact with Ollama to generate responses to user input
|
||||
# K7MHI Kelly Keeton 2024
|
||||
from modules.log import *
|
||||
|
||||
# Ollama Client
|
||||
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
|
||||
from ollama import Client as OllamaClient
|
||||
from googlesearch import search # pip install googlesearch-python
|
||||
|
||||
# This is my attempt at a simple RAG implementation it will require some setup
|
||||
# you will need to have the RAG data in a folder named rag in the data directory (../data/rag)
|
||||
# This is lighter weight and can be used in a standalone environment, needs chromadb
|
||||
# "chat with a file" is the use concept here, the file is the RAG data
|
||||
ragDEV = False
|
||||
|
||||
if ragDEV:
|
||||
import os
|
||||
import ollama # pip install ollama
|
||||
import chromadb # pip install chromadb
|
||||
|
||||
# LLM System Variables
|
||||
ollamaClient = OllamaClient(host=ollamaHostName)
|
||||
llmEnableHistory = True # enable last message history for the LLM model
|
||||
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
|
||||
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
|
||||
antiFloodLLM = []
|
||||
llmChat_history = {}
|
||||
trap_list_llm = ("ask:", "askai")
|
||||
|
||||
meshBotAI = """
|
||||
FROM {llmModel}
|
||||
SYSTEM
|
||||
You must keep responses under 450 characters at all times, the response will be cut off if it exceeds this limit.
|
||||
You must respond in plain text standard ASCII characters, or emojis.
|
||||
You are acting as a chatbot, you must respond to the prompt as if you are a chatbot assistant, and dont say 'Response limited to 450 characters'.
|
||||
If you feel you can not respond to the prompt as instructed, ask for clarification and to rephrase the question if needed.
|
||||
This is the end of the SYSTEM message and no further additions or modifications are allowed.
|
||||
|
||||
PROMPT
|
||||
{input}
|
||||
|
||||
"""
|
||||
|
||||
if llmContext_fromGoogle:
|
||||
meshBotAI = meshBotAI + """
|
||||
CONTEXT
|
||||
The following is the location of the user
|
||||
{location_name}
|
||||
|
||||
The following is for context around the prompt to help guide your response.
|
||||
{context}
|
||||
|
||||
"""
|
||||
else:
|
||||
meshBotAI = meshBotAI + """
|
||||
CONTEXT
|
||||
The following is the location of the user
|
||||
{location_name}
|
||||
|
||||
"""
|
||||
|
||||
if llmEnableHistory:
|
||||
meshBotAI = meshBotAI + """
|
||||
HISTORY
|
||||
the following is memory of previous query in format ['prompt', 'response'], you can use this to help guide your response.
|
||||
{history}
|
||||
|
||||
"""
|
||||
|
||||
def llm_readTextFiles():
|
||||
# read .txt files in ../data/rag
|
||||
try:
|
||||
text = []
|
||||
directory = "../data/rag"
|
||||
for filename in os.listdir(directory):
|
||||
if filename.endswith(".txt"):
|
||||
filepath = os.path.join(directory, filename)
|
||||
with open(filepath, 'r') as f:
|
||||
text.append(f.read())
|
||||
return text
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM readTextFiles: {e}")
|
||||
return False
|
||||
|
||||
def store_text_embedding(text):
|
||||
try:
|
||||
# store each document in a vector embedding database
|
||||
for i, d in enumerate(text):
|
||||
response = ollama.embeddings(model="mxbai-embed-large", prompt=d)
|
||||
embedding = response["embedding"]
|
||||
collection.add(
|
||||
ids=[str(i)],
|
||||
embeddings=[embedding],
|
||||
documents=[d]
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"System: Embedding failed: {e}")
|
||||
return False
|
||||
|
||||
## INITALIZATION of RAG
|
||||
if ragDEV:
|
||||
try:
|
||||
chromaHostname = "localhost:8000"
|
||||
# connect to the chromaDB
|
||||
chromaHost = chromaHostname.split(":")[0]
|
||||
chromaPort = chromaHostname.split(":")[1]
|
||||
if chromaHost == "localhost" and chromaPort == "8000":
|
||||
# create a client using local python Client
|
||||
chromaClient = chromadb.Client()
|
||||
else:
|
||||
# create a client using the remote python Client
|
||||
# this isnt tested yet please test and report back
|
||||
chromaClient = chromadb.Client(host=chromaHost, port=chromaPort)
|
||||
|
||||
clearCollection = False
|
||||
if "meshBotAI" in chromaClient.list_collections() and clearCollection:
|
||||
logger.debug(f"System: LLM: Clearing RAG files from chromaDB")
|
||||
chromaClient.delete_collection("meshBotAI")
|
||||
|
||||
# create a new collection
|
||||
collection = chromaClient.create_collection("meshBotAI")
|
||||
|
||||
logger.debug(f"System: LLM: Cataloging RAG data")
|
||||
store_text_embedding(llm_readTextFiles())
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM: RAG Initalization failed: {e}")
|
||||
|
||||
def query_collection(prompt):
|
||||
# generate an embedding for the prompt and retrieve the most relevant doc
|
||||
response = ollama.embeddings(prompt=prompt, model="mxbai-embed-large")
|
||||
results = collection.query(query_embeddings=[response["embedding"]], n_results=1)
|
||||
data = results['documents'][0][0]
|
||||
return data
|
||||
|
||||
def llm_query(input, nodeID=0, location_name=None):
|
||||
global antiFloodLLM, llmChat_history
|
||||
googleResults = []
|
||||
if not location_name:
|
||||
location_name = "no location provided "
|
||||
|
||||
# add the naughty list here to stop the function before we continue
|
||||
# add a list of allowed nodes only to use the function
|
||||
|
||||
# anti flood protection
|
||||
if nodeID in antiFloodLLM:
|
||||
return "Please wait before sending another message"
|
||||
else:
|
||||
antiFloodLLM.append(nodeID)
|
||||
|
||||
if llmContext_fromGoogle:
|
||||
# grab some context from the internet using google search hits (if available)
|
||||
# localization details at https://pypi.org/project/googlesearch-python/
|
||||
|
||||
# remove common words from the search query
|
||||
# commonWordsList = ["is", "for", "the", "of", "and", "in", "on", "at", "to", "with", "by", "from", "as", "a", "an", "that", "this", "these", "those", "there", "here", "where", "when", "why", "how", "what", "which", "who", "whom", "whose", "whom"]
|
||||
# sanitizedSearch = ' '.join([word for word in input.split() if word.lower() not in commonWordsList])
|
||||
try:
|
||||
googleSearch = search(input, advanced=True, num_results=googleSearchResults)
|
||||
if googleSearch:
|
||||
for result in googleSearch:
|
||||
# SearchResult object has url= title= description= just grab title and description
|
||||
googleResults.append(f"{result.title} {result.description}")
|
||||
else:
|
||||
googleResults = ['no other context provided']
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM Query: context gathering failed, likely due to network issues")
|
||||
googleResults = ['no other context provided']
|
||||
|
||||
history = llmChat_history.get(nodeID, ["", ""])
|
||||
|
||||
if googleResults:
|
||||
logger.debug(f"System: Google-Enhanced LLM Query: {input} From:{nodeID}")
|
||||
else:
|
||||
logger.debug(f"System: LLM Query: {input} From:{nodeID}")
|
||||
|
||||
response = ""
|
||||
result = ""
|
||||
location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')}"
|
||||
|
||||
try:
|
||||
# RAG context inclusion testing
|
||||
ragContext = False
|
||||
if ragDEV:
|
||||
ragContext = query_collection(input)
|
||||
|
||||
if ragContext:
|
||||
ragContextGooogle = ragContext + '\n'.join(googleResults)
|
||||
# Build the query from the template
|
||||
modelPrompt = meshBotAI.format(input=input, context=ragContext, location_name=location_name, llmModel=llmModel, history=history)
|
||||
# Query the model with RAG context
|
||||
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
|
||||
else:
|
||||
# Build the query from the template
|
||||
modelPrompt = meshBotAI.format(input=input, context='\n'.join(googleResults), location_name=location_name, llmModel=llmModel, history=history)
|
||||
# Query the model without RAG context
|
||||
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
|
||||
|
||||
# Condense the result to just needed
|
||||
if isinstance(result, dict):
|
||||
result = result.get("response")
|
||||
|
||||
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
|
||||
except Exception as e:
|
||||
logger.warning(f"System: LLM failure: {e}")
|
||||
return "I am having trouble processing your request, please try again later."
|
||||
|
||||
# cleanup for message output
|
||||
response = result.strip().replace('\n', ' ')
|
||||
# done with the query, remove the user from the anti flood list
|
||||
antiFloodLLM.remove(nodeID)
|
||||
|
||||
if llmEnableHistory:
|
||||
llmChat_history[nodeID] = [input, response]
|
||||
|
||||
return response
|
||||
|
||||
# import subprocess
|
||||
# def get_ollama_cpu():
|
||||
# try:
|
||||
# psOutput = subprocess.run(['ollama', 'ps'], capture_output=True, text=True)
|
||||
# if "GPU" in psOutput.stdout:
|
||||
# logger.debug(f"System: Ollama process with GPU")
|
||||
# else:
|
||||
# logger.debug(f"System: Ollama process with CPU, query time will be slower")
|
||||
# except Exception as e:
|
||||
# logger.debug(f"System: Ollama process not found, {e}")
|
||||
# return False
|
||||
@@ -9,9 +9,9 @@ import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from modules.log import *
|
||||
|
||||
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert")
|
||||
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert", "rlist")
|
||||
|
||||
def where_am_i(lat=0, lon=0):
|
||||
def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
whereIam = ""
|
||||
grid = mh.to_maiden(float(lat), float(lon))
|
||||
|
||||
@@ -22,22 +22,139 @@ def where_am_i(lat=0, lon=0):
|
||||
# initialize Nominatim API
|
||||
geolocator = Nominatim(user_agent="mesh-bot")
|
||||
|
||||
# Nomatim API call to get address
|
||||
if float(lat) == latitudeValue and float(lon) == longitudeValue:
|
||||
# redacted address when no GPS and using default location
|
||||
location = geolocator.reverse(lat + ", " + lon)
|
||||
address = location.raw['address']
|
||||
address_components = ['city', 'state', 'postcode', 'county', 'country']
|
||||
whereIam += ' '.join([address.get(component, '') for component in address_components if component in address])
|
||||
whereIam += " Grid: " + grid
|
||||
try:
|
||||
# Nomatim API call to get address
|
||||
if short:
|
||||
location = geolocator.reverse(lat + ", " + lon)
|
||||
address = location.raw['address']
|
||||
address_components = ['city', 'state', 'county', 'country']
|
||||
whereIam = f"City: {address.get('city', '')}. State: {address.get('state', '')}. County: {address.get('county', '')}. Country: {address.get('country', '')}."
|
||||
return whereIam
|
||||
|
||||
if zip:
|
||||
# return a string with zip code only
|
||||
location = geolocator.reverse(str(lat) + ", " + str(lon))
|
||||
whereIam = location.raw['address'].get('postcode', '')
|
||||
return whereIam
|
||||
|
||||
if float(lat) == latitudeValue and float(lon) == longitudeValue:
|
||||
# redacted address when no GPS and using default location
|
||||
location = geolocator.reverse(str(lat) + ", " + str(lon))
|
||||
address = location.raw['address']
|
||||
address_components = {
|
||||
'city': 'City',
|
||||
'state': 'State',
|
||||
'postcode': 'Zip',
|
||||
'county': 'County',
|
||||
'country': 'Country'
|
||||
}
|
||||
whereIam += ', '.join([f"{label}: {address.get(component, '')}" for component, label in address_components.items() if component in address])
|
||||
else:
|
||||
location = geolocator.reverse(lat + ", " + lon)
|
||||
address = location.raw['address']
|
||||
address_components = {
|
||||
'house_number': 'Number',
|
||||
'road': 'Road',
|
||||
'city': 'City',
|
||||
'state': 'State',
|
||||
'postcode': 'Zip',
|
||||
'county': 'County',
|
||||
'country': 'Country'
|
||||
}
|
||||
whereIam += ', '.join([f"{label}: {address.get(component, '')}" for component, label in address_components.items() if component in address])
|
||||
whereIam += f", Grid: " + grid
|
||||
return whereIam
|
||||
except Exception as e:
|
||||
logger.debug("Location:Error fetching location data with whereami, likely network error")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
def getRepeaterBook(lat=0, lon=0):
|
||||
grid = mh.to_maiden(float(lat), float(lon))
|
||||
data = []
|
||||
repeater_url = f"https://www.repeaterbook.com/repeaters/prox_result.php?city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
|
||||
try:
|
||||
msg = ''
|
||||
response = requests.get(repeater_url)
|
||||
soup = bs.BeautifulSoup(response.text, 'html.parser')
|
||||
table = soup.find('table', attrs={'class': 'w3-table w3-striped w3-responsive w3-mobile w3-auto sortable'})
|
||||
if table is not None:
|
||||
cells = table.find_all('td')
|
||||
data = []
|
||||
for i in range(0, len(cells), 11):
|
||||
if i + 10 < len(cells): #avoid IndexError
|
||||
repeater = {
|
||||
'frequency': cells[i].text.strip() if i < len(cells) else 'N/A',
|
||||
'offset': cells[i + 1].text.strip() if i + 1 < len(cells) else 'N/A',
|
||||
'tone': cells[i + 2].text.strip() if i + 2 < len(cells) else 'N/A',
|
||||
'call_sign': cells[i + 3].text.strip() if i + 3 < len(cells) else 'N/A',
|
||||
'location': cells[i + 4].text.strip() if i + 4 < len(cells) else 'N/A',
|
||||
'state': cells[i + 5].text.strip() if i + 5 < len(cells) else 'N/A',
|
||||
'use': cells[i + 6].text.strip() if i + 6 < len(cells) else 'N/A',
|
||||
'mode': cells[i + 7].text.strip() if i + 7 < len(cells) else 'N/A',
|
||||
'distance': cells[i + 8].text.strip() if i + 8 < len(cells) else 'N/A',
|
||||
'direction': cells[i + 9].text.strip() if i + 9 < len(cells) else 'N/A'
|
||||
}
|
||||
data.append(repeater)
|
||||
else:
|
||||
msg = "bug?Not enough columns"
|
||||
else:
|
||||
msg = "bug?Table not found"
|
||||
except Exception as e:
|
||||
msg = "No repeaters found 😔"
|
||||
# Limit the output to the first 4 repeaters
|
||||
for repeater in data[:4]:
|
||||
tmpTone = repeater['tone'].replace(" /", "")
|
||||
msg += f"{repeater['call_sign']}📶{repeater['frequency']}{repeater['offset']},{tmpTone}.{repeater['mode']}"
|
||||
if repeater != data[:4][-1]: msg += '\n'
|
||||
return msg
|
||||
|
||||
def getArtSciRepeaters(lat=0, lon=0):
|
||||
# UK api_url = "https://api-beta.rsgb.online/all/systems"
|
||||
#grid = mh.to_maiden(float(lat), float(lon))
|
||||
repeaters = []
|
||||
zipCode = where_am_i(lat, lon, zip=True)
|
||||
if zipCode == NO_DATA_NOGPS or zipCode == ERROR_FETCHING_DATA:
|
||||
return zipCode
|
||||
|
||||
if zipCode.isnumeric():
|
||||
try:
|
||||
artsci_url = f"http://www.artscipub.com/mobile/showstate.asp?zip={zipCode}"
|
||||
response = requests.get(artsci_url)
|
||||
soup = bs.BeautifulSoup(response.text, 'html.parser')
|
||||
# results needed xpath is /html/body/table[2]/tbody/tr/td/table/tbody/tr[2]/td/table
|
||||
table = soup.find_all('table')[1]
|
||||
rows = table.find_all('tr')
|
||||
for row in rows:
|
||||
cols = row.find_all('td')
|
||||
cols = [ele.text.strip() for ele in cols]
|
||||
# if no elements have the word 'located' then append
|
||||
if not any('located' in ele for ele in cols):
|
||||
if not any('Location' in ele for ele in cols):
|
||||
repeaters.append([ele for ele in cols if ele])
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching data from {artsci_url}: {e}")
|
||||
|
||||
if repeaters != []:
|
||||
msg = f"Found:{len(repeaters)} in {zipCode}\n"
|
||||
for repeater in repeaters:
|
||||
# format is ['City', 'Frequency', 'Offset', 'PL', 'Call', 'Notes']
|
||||
# there might be missing elements or only one element
|
||||
if len(repeater) == 2:
|
||||
msg += f"Freq:{repeater[1]}"
|
||||
elif len(repeater) == 3:
|
||||
msg += f"Freq:{repeater[1]}, PL:{repeater[2]}"
|
||||
elif len(repeater) == 4:
|
||||
msg += f"Freq:{repeater[1]}, PL:{repeater[2]}, ID: {repeater[3]}"
|
||||
elif len(repeater) == 5:
|
||||
msg += f"Freq:{repeater[1]}, PL:{repeater[2]}, ID:{repeater[3]}"
|
||||
elif len(repeater) == 6:
|
||||
msg += f"Freq:{repeater[1]}, PL:{repeater[2]}, ID:{repeater[3]}. {repeater[5]}"
|
||||
if repeater != repeaters[-1]:
|
||||
msg += "\n"
|
||||
else:
|
||||
location = geolocator.reverse(lat + ", " + lon)
|
||||
address = location.raw['address']
|
||||
address_components = ['house_number', 'road', 'city', 'state', 'postcode', 'county', 'country']
|
||||
whereIam += ' '.join([address.get(component, '') for component in address_components if component in address])
|
||||
whereIam += " Grid: " + grid
|
||||
return whereIam
|
||||
msg = f"no results.. sorry"
|
||||
return msg
|
||||
|
||||
|
||||
def get_tide(lat=0, lon=0):
|
||||
station_id = ""
|
||||
@@ -63,40 +180,44 @@ def get_tide(lat=0, lon=0):
|
||||
logger.error("Location:Error fetching tide station table from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
station_url = "https://tidesandcurrents.noaa.gov/noaatidepredictions.html?id=" + station_id
|
||||
if zuluTime:
|
||||
station_url += "&clock=24hour"
|
||||
station_url = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?date=today&time_zone=lst_ldt&datum=MLLW&product=predictions&interval=hilo&format=json&station=" + station_id
|
||||
|
||||
if use_metric:
|
||||
station_url += "&units=metric"
|
||||
else:
|
||||
station_url += "&units=english"
|
||||
|
||||
try:
|
||||
station_data = requests.get(station_url, timeout=urlTimeoutSeconds)
|
||||
if not station_data.ok:
|
||||
logger.error("Location:Error fetching station data from NOAA")
|
||||
tide_data = requests.get(station_url, timeout=urlTimeoutSeconds)
|
||||
if tide_data.ok:
|
||||
tide_json = tide_data.json()
|
||||
else:
|
||||
logger.error("Location:Error fetching tide data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.error("Location:Error fetching station data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# extract table class="table table-condensed"
|
||||
soup = bs.BeautifulSoup(station_data.text, 'html.parser')
|
||||
table = soup.find('table', class_='table table-condensed')
|
||||
|
||||
# extract rows
|
||||
rows = table.find_all('tr')
|
||||
# extract data from rows
|
||||
tide_data = []
|
||||
for row in rows:
|
||||
row_text = ""
|
||||
cols = row.find_all('td')
|
||||
for col in cols:
|
||||
row_text += col.text + " "
|
||||
tide_data.append(row_text)
|
||||
# format tide data into a string
|
||||
tide_string = ""
|
||||
for data in tide_data:
|
||||
tide_string += data + "\n"
|
||||
# trim off last newline
|
||||
tide_string = tide_string[:-1]
|
||||
return tide_string
|
||||
except (requests.exceptions.RequestException, json.JSONDecodeError):
|
||||
logger.error("Location:Error fetching tide data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
tide_data = tide_json['predictions']
|
||||
|
||||
# format tide data into a table string for mesh
|
||||
# get the date out of the first t value
|
||||
tide_date = tide_data[0]['t'].split(" ")[0]
|
||||
tide_table = "Tide Data for " + tide_date + "\n"
|
||||
for tide in tide_data:
|
||||
tide_time = tide['t'].split(" ")[1]
|
||||
if not zuluTime:
|
||||
# convert to 12 hour clock
|
||||
if int(tide_time.split(":")[0]) > 12:
|
||||
tide_time = str(int(tide_time.split(":")[0]) - 12) + ":" + tide_time.split(":")[1] + " PM"
|
||||
else:
|
||||
tide_time = tide_time + " AM"
|
||||
|
||||
tide_table += tide['type'] + " " + tide_time + ", " + tide['v'] + "\n"
|
||||
# remove last newline
|
||||
tide_table = tide_table[:-1]
|
||||
return tide_table
|
||||
|
||||
def get_weather(lat=0, lon=0, unit=0):
|
||||
# get weather report from NOAA for forecast detailed
|
||||
@@ -193,6 +314,7 @@ def abbreviate_weather(row):
|
||||
"precipitation": "precip",
|
||||
"showers": "shwrs",
|
||||
"thunderstorms": "t-storms",
|
||||
"thunderstorm": "t-storm",
|
||||
"quarters": "qtrs",
|
||||
"quarter": "qtr"
|
||||
}
|
||||
@@ -203,11 +325,15 @@ def abbreviate_weather(row):
|
||||
|
||||
return line
|
||||
|
||||
def getWeatherAlerts(lat=0, lon=0):
|
||||
def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
|
||||
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
|
||||
alerts = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
|
||||
return NO_DATA_NOGPS
|
||||
else:
|
||||
if useDefaultLatLon:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
|
||||
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
|
||||
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
|
||||
@@ -247,6 +373,23 @@ def getWeatherAlerts(lat=0, lon=0):
|
||||
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
|
||||
return data
|
||||
|
||||
wxAlertCache = ""
|
||||
def alertBrodcast():
|
||||
# get the latest weather alerts and broadcast them if there are any
|
||||
global wxAlertCache
|
||||
currentAlert = getWeatherAlerts(latitudeValue, longitudeValue)
|
||||
|
||||
if currentAlert == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS or currentAlert == NO_ALERTS:
|
||||
wxAlertCache = ""
|
||||
return False
|
||||
# broadcast the alerts send to wxBrodcastCh
|
||||
elif currentAlert[0] != wxAlertCache:
|
||||
logger.debug("Location:Broadcasting weather alerts")
|
||||
wxAlertCache = currentAlert[0]
|
||||
return currentAlert
|
||||
|
||||
return False
|
||||
|
||||
def getActiveWeatherAlertsDetail(lat=0, lon=0):
|
||||
# get the latest details of weather alerts from NOAA
|
||||
alerts = ""
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# Custom logger for MeshBot and PongBot
|
||||
# you can change the sdtout_handler level to logging.INFO to only show INFO level logs
|
||||
# stdout_handler.setLevel(logging.INFO)vs stdout_handler.setLevel(logging.DEBUG)
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
import re
|
||||
from datetime import datetime
|
||||
from modules.settings import *
|
||||
|
||||
@@ -33,6 +31,13 @@ class CustomFormatter(logging.Formatter):
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
||||
class plainFormatter(logging.Formatter):
|
||||
ansi_escape = re.compile(r'\x1b\[([0-9]+)(;[0-9]+)*m')
|
||||
|
||||
def format(self, record):
|
||||
message = super().format(record)
|
||||
return self.ansi_escape.sub('', message)
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger("MeshBot System Logger")
|
||||
@@ -54,19 +59,19 @@ stdout_handler = logging.StreamHandler()
|
||||
stdout_handler.setLevel(logging.DEBUG)
|
||||
# Set format for stdout handler
|
||||
stdout_handler.setFormatter(CustomFormatter(logFormat))
|
||||
|
||||
# Add handlers to the logger
|
||||
logger.addHandler(stdout_handler)
|
||||
|
||||
if syslog_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler = logging.FileHandler('system{}.log'.format(today.strftime('%Y_%m_%d')))
|
||||
file_handler.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
|
||||
file_handler.setFormatter(logging.Formatter(logFormat))
|
||||
logger.addHandler(file_handler)
|
||||
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count)
|
||||
file_handler_sys.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
|
||||
file_handler_sys.setFormatter(plainFormatter(logFormat))
|
||||
logger.addHandler(file_handler_sys)
|
||||
|
||||
if log_messages_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler = logging.FileHandler('messages{}.log'.format(today.strftime('%Y_%m_%d')))
|
||||
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count)
|
||||
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
|
||||
file_handler.setFormatter(logging.Formatter(msgLogFormat))
|
||||
msgLogger.addHandler(file_handler)
|
||||
msgLogger.addHandler(file_handler)
|
||||
@@ -10,7 +10,6 @@ MOTD = 'Thanks for using MeshBOT! Have a good day!'
|
||||
NO_ALERTS = "No weather alerts found."
|
||||
|
||||
# setup the global variables
|
||||
MESSAGE_CHUNK_SIZE = 160 # message chunk size for sending at high success rate
|
||||
SITREP_NODE_COUNT = 3 # number of nodes to report in the sitrep
|
||||
msg_history = [] # message history for the store and forward feature
|
||||
bbs_ban_list = [] # list of banned users, imported from config
|
||||
@@ -20,11 +19,15 @@ antiSpam = True # anti-spam feature to prevent flooding public channel
|
||||
ping_enabled = True # ping feature to respond to pings, ack's etc.
|
||||
sitrep_enabled = True # sitrep feature to respond to sitreps
|
||||
lastHamLibAlert = 0 # last alert from hamlib
|
||||
lastFileAlert = 0 # last alert from file monitor
|
||||
max_retry_count1 = 4 # max retry count for interface 1
|
||||
max_retry_count2 = 4 # max retry count for interface 2
|
||||
retry_int1 = False
|
||||
retry_int2 = False
|
||||
scheduler_enabled = False # enable the scheduler currently config via code only
|
||||
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
|
||||
playingGame = False
|
||||
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
|
||||
|
||||
# Read the config file, if it does not exist, create basic config file
|
||||
config = configparser.ConfigParser()
|
||||
@@ -44,15 +47,15 @@ if config.sections() == []:
|
||||
print (f"System: Config file created, check {config_file} or review the config.template")
|
||||
|
||||
if 'sentry' not in config:
|
||||
config['Sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
|
||||
config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'location' not in config:
|
||||
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True'}
|
||||
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'bbs' not in config:
|
||||
config['bbs'] = {'enabled': 'False', 'bbsdb': 'bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
|
||||
config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'repeater' not in config:
|
||||
@@ -63,6 +66,18 @@ if 'radioMon' not in config:
|
||||
config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'games' not in config:
|
||||
config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'messagingSettings' not in config:
|
||||
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'fileMon' not in config:
|
||||
config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
# interface1 settings
|
||||
interface1_type = config['interface'].get('type', 'serial')
|
||||
port1 = config['interface'].get('port', '')
|
||||
@@ -81,26 +96,40 @@ else:
|
||||
|
||||
# variables
|
||||
try:
|
||||
# general
|
||||
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
|
||||
publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel
|
||||
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
|
||||
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
|
||||
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True
|
||||
syslog_to_file = config['general'].getboolean('SyslogToFile', False) # default True
|
||||
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
|
||||
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
|
||||
syslog_to_file = config['general'].getboolean('SyslogToFile', True) # default on
|
||||
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
|
||||
store_forward_enabled = config['general'].getboolean('StoreForward', True) # default False
|
||||
store_forward_enabled = config['general'].getboolean('StoreForward', True)
|
||||
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
|
||||
welcome_message = config['general'].get(f'welcome_message', WELCOME_MSG)
|
||||
welcome_message = config['general'].get('welcome_message', WELCOME_MSG)
|
||||
welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message
|
||||
motd_enabled = config['general'].getboolean('motdEnabled', True)
|
||||
dad_jokes_enabled = config['general'].getboolean('DadJokes', True)
|
||||
solar_conditions_enabled = config['general'].getboolean('spaceWeather', True)
|
||||
MOTD = config['general'].get('motd', MOTD)
|
||||
enableCmdHistory = config['general'].getboolean('enableCmdHistory', True)
|
||||
lheardCmdIgnoreNode = config['general'].get('lheardCmdIgnoreNode', '').split(',')
|
||||
whoami_enabled = config['general'].getboolean('whoami', True)
|
||||
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
|
||||
dad_jokes_emojiJokes = config['general'].getboolean('DadJokesEmoji', False)
|
||||
solar_conditions_enabled = config['general'].getboolean('spaceWeather', True)
|
||||
wikipedia_enabled = config['general'].getboolean('wikipedia', False)
|
||||
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
|
||||
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
|
||||
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
|
||||
|
||||
# sentry
|
||||
sentry_enabled = config['sentry'].getboolean('SentryEnabled', False) # default False
|
||||
secure_channel = config['sentry'].getint('SentryChannel', 2) # default 2
|
||||
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
|
||||
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
|
||||
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
|
||||
|
||||
# location
|
||||
location_enabled = config['location'].getboolean('enabled', True)
|
||||
latitudeValue = config['location'].getfloat('lat', 48.50)
|
||||
longitudeValue = config['location'].getfloat('lon', -123.0)
|
||||
@@ -109,23 +138,56 @@ try:
|
||||
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
|
||||
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet
|
||||
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
|
||||
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
|
||||
# brodcast channel for weather alerts
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
|
||||
if wxAlertBroadcastChannel:
|
||||
if ',' in wxAlertBroadcastChannel:
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
|
||||
else:
|
||||
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
|
||||
|
||||
# bbs
|
||||
bbs_enabled = config['bbs'].getboolean('enabled', False)
|
||||
bbsdb = config['bbs'].get('bbsdb', 'bbsdb.pkl')
|
||||
bbsdb = config['bbs'].get('bbsdb', 'data/bbsdb.pkl')
|
||||
bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',')
|
||||
bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',')
|
||||
|
||||
# repeater
|
||||
repeater_enabled = config['repeater'].getboolean('enabled', False)
|
||||
repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
|
||||
|
||||
radio_dectection_enabled = config['radioMon'].getboolean('enabled', False)
|
||||
# radio monitoring
|
||||
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
|
||||
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
|
||||
sigWatchBrodcastCh = config['radioMon'].get('sigWatchBrodcastCh', '2').split(',') # default Channel 2
|
||||
sigWatchBroadcastCh = config['radioMon'].get('sigWatchBroadcastCh', '2').split(',') # default Channel 2
|
||||
signalDetectionThreshold = config['radioMon'].getint('signalDetectionThreshold', -10) # default -10 dBm
|
||||
signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds
|
||||
signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second
|
||||
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
|
||||
|
||||
# file monitor
|
||||
file_monitor_enabled = config['fileMon'].getboolean('enabled', False)
|
||||
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
|
||||
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
|
||||
|
||||
# games
|
||||
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
|
||||
dopewars_enabled = config['games'].getboolean('dopeWars', True)
|
||||
lemonade_enabled = config['games'].getboolean('lemonade', True)
|
||||
blackjack_enabled = config['games'].getboolean('blackjack', True)
|
||||
videoPoker_enabled = config['games'].getboolean('videoPoker', True)
|
||||
mastermind_enabled = config['games'].getboolean('mastermind', True)
|
||||
golfSim_enabled = config['games'].getboolean('golfSim', True)
|
||||
|
||||
# messaging settings
|
||||
responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7
|
||||
splitDelay = config['messagingSettings'].getfloat('splitDelay', 0) # default 0
|
||||
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160
|
||||
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
|
||||
maxBuffer = config['messagingSettings'].getint('maxBuffer', 220) # default 220
|
||||
|
||||
except KeyError as e:
|
||||
print(f"System: Error reading config file: {e}")
|
||||
print(f"System: Check the config.ini against config.template file for missing sections or values.")
|
||||
|
||||
1172
modules/system.py
1172
modules/system.py
File diff suppressed because it is too large
Load Diff
@@ -94,10 +94,20 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
code_string = ""
|
||||
if daily_weather_code[i] == 0:
|
||||
code_string = "Clear sky"
|
||||
elif daily_weather_code[i] == 1 or 2 or 3:
|
||||
code_string = "Partly cloudy"
|
||||
elif daily_weather_code[i] == 45 or 48:
|
||||
elif daily_weather_code[i] == 1:
|
||||
code_string = "Mostly Cloudy"
|
||||
elif daily_weather_code[i] == 2:
|
||||
code_string = "Partly Cloudy"
|
||||
elif daily_weather_code[i] == 3:
|
||||
code_string = "Overcast"
|
||||
elif daily_weather_code[i] == 5:
|
||||
code_string = "Haze"
|
||||
elif daily_weather_code[i] == 10:
|
||||
code_string = "Mist"
|
||||
elif daily_weather_code[i] == 45:
|
||||
code_string = "Fog"
|
||||
elif daily_weather_code[i] == 48:
|
||||
code_string = "Freezing Fog"
|
||||
elif daily_weather_code[i] == 51:
|
||||
code_string = "Drizzle: Light"
|
||||
elif daily_weather_code[i] == 53:
|
||||
@@ -126,6 +136,10 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
code_string = "Snow: Heavy"
|
||||
elif daily_weather_code[i] == 77:
|
||||
code_string = "Snow Grains"
|
||||
elif daily_weather_code[i] == 78:
|
||||
code_string = "Ice Crystals"
|
||||
elif daily_weather_code[i] == 79:
|
||||
code_string = "Ice Pellets"
|
||||
elif daily_weather_code[i] == 80:
|
||||
code_string = "Rain showers: Slight"
|
||||
elif daily_weather_code[i] == 81:
|
||||
@@ -133,15 +147,17 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
elif daily_weather_code[i] == 82:
|
||||
code_string = "Rain showers: Heavy"
|
||||
elif daily_weather_code[i] == 85:
|
||||
code_string = "Snow showers: Light"
|
||||
code_string = "Snow showers"
|
||||
elif daily_weather_code[i] == 86:
|
||||
code_string = "Snow showers: Moderate"
|
||||
code_string = "Snow showers: Heavy"
|
||||
elif daily_weather_code[i] == 95:
|
||||
code_string = "Thunderstorm: Slight"
|
||||
code_string = "Thunderstorm"
|
||||
elif daily_weather_code[i] == 96:
|
||||
code_string = "Thunderstorm: Moderate"
|
||||
code_string = "Hailstorm"
|
||||
elif daily_weather_code[i] == 97:
|
||||
code_string = "Thunderstorm Heavy"
|
||||
elif daily_weather_code[i] == 99:
|
||||
code_string = "Thunderstorm: Heavy"
|
||||
code_string = "Hailstorm Heavy"
|
||||
|
||||
weather_report += "Cond: " + code_string + ". "
|
||||
|
||||
|
||||
57
pong_bot.py
57
pong_bot.py
@@ -8,6 +8,8 @@ from pubsub import pub # pip install pubsub
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
|
||||
def auto_response(message, snr, rssi, hop, message_from_id, channel_number, deviceID):
|
||||
# Auto response to messages
|
||||
message_lower = message.lower()
|
||||
@@ -37,8 +39,8 @@ def auto_response(message, snr, rssi, hop, message_from_id, channel_number, devi
|
||||
# run the first command after sorting
|
||||
bot_response = command_handler[cmds[0]['cmd']]()
|
||||
|
||||
# wait a 700ms to avoid message collision from lora-ack
|
||||
time.sleep(0.7)
|
||||
# wait a responseDelay to avoid message collision from lora-ack
|
||||
time.sleep(responseDelay)
|
||||
|
||||
return bot_response
|
||||
|
||||
@@ -75,15 +77,15 @@ def handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2):
|
||||
|
||||
def handle_ack(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "🏓ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
|
||||
return "✋ACK-ACK! " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🏓ACK-ACK! " + hop
|
||||
return "✋ACK-ACK! " + hop
|
||||
|
||||
def handle_testing(hop, snr, rssi):
|
||||
if hop == "Direct":
|
||||
return "🏓Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
|
||||
return "🎙Testing 1,2,3 " + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
return "🏓Testing 1,2,3 " + hop
|
||||
return "🎙Testing 1,2,3 " + hop
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2
|
||||
@@ -104,12 +106,29 @@ def onDisconnect(interface):
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
logger.critical(f"System: Lost Connection to Device BLE")
|
||||
if interface1_type == 'ble':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
retry_int2 = True
|
||||
|
||||
def onReceive(packet, interface):
|
||||
# extract interface defailts from interface object
|
||||
rxType = type(interface).__name__
|
||||
rxNode = 0
|
||||
# Debug print the interface object
|
||||
#for item in interface.__dict__.items(): print (item)
|
||||
message_from_id = 0
|
||||
snr = 0
|
||||
rssi = 0
|
||||
hop = 0
|
||||
hop_away = 0
|
||||
|
||||
if DEBUGpacket:
|
||||
# Debug print the interface object
|
||||
for item in interface.__dict__.items(): intDebug = f"{item}\n"
|
||||
logger.debug(f"System: Packet Received on {rxType} Interface\n {intDebug} \n END of interface \n")
|
||||
# Debug print the packet for debugging
|
||||
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
|
||||
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
@@ -125,9 +144,11 @@ def onReceive(packet, interface):
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
rxNode = 2
|
||||
|
||||
# Debug print the packet for debugging
|
||||
#print(f"Packet Received\n {packet} \n END of packet \n")
|
||||
message_from_id = 0
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble':
|
||||
rxNode = 1
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
|
||||
# check for a message packet and process it
|
||||
try:
|
||||
@@ -237,8 +258,8 @@ def onReceive(packet, interface):
|
||||
|
||||
# repeat the message on the other device
|
||||
if repeater_enabled and interface2_enabled:
|
||||
# wait a 700ms to avoid message collision from lora-ack.
|
||||
time.sleep(0.7)
|
||||
# wait a responseDelay to avoid message collision from lora-ack.
|
||||
time.sleep(responseDelay)
|
||||
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
# if channel found in the repeater list repeat the message
|
||||
if str(channel_number) in repeater_channels:
|
||||
@@ -264,17 +285,17 @@ async def start_rx():
|
||||
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
|
||||
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
|
||||
if log_messages_to_file:
|
||||
logger.debug(f"System: Logging Messages to disk")
|
||||
logger.debug("System: Logging Messages to disk")
|
||||
if sentry_enabled:
|
||||
logger.debug(f"System: Sentry Enabled")
|
||||
logger.debug("System: Sentry Enabled")
|
||||
if store_forward_enabled:
|
||||
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
|
||||
if useDMForResponse:
|
||||
logger.debug(f"System: Respond by DM only")
|
||||
logger.debug("System: Respond by DM only")
|
||||
if repeater_enabled and interface2_enabled:
|
||||
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
|
||||
if radio_dectection_enabled:
|
||||
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBrodcastCh} for {get_freq_common_name(get_hamlib('f'))}")
|
||||
if radio_detection_enabled:
|
||||
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
|
||||
|
||||
# here we go loopty loo
|
||||
while True:
|
||||
|
||||
@@ -12,3 +12,6 @@ retry_requests
|
||||
numpy
|
||||
geopy
|
||||
schedule
|
||||
wikipedia
|
||||
ollama
|
||||
googlesearch-python
|
||||
|
||||
Reference in New Issue
Block a user