From 9c84accda87a83381e64bf8182777be9ef128b1e Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sun, 5 Sep 2021 00:19:46 -0400 Subject: Move snakes commands into fun folder --- .../fun/snakes/snake_cards/backs/card_back1.jpg | Bin 0 -> 165788 bytes .../fun/snakes/snake_cards/backs/card_back2.jpg | Bin 0 -> 140868 bytes .../fun/snakes/snake_cards/card_bottom.png | Bin 0 -> 18165 bytes .../fun/snakes/snake_cards/card_frame.png | Bin 0 -> 1460 bytes bot/resources/fun/snakes/snake_cards/card_top.png | Bin 0 -> 12581 bytes .../fun/snakes/snake_cards/expressway.ttf | Bin 0 -> 156244 bytes bot/resources/fun/snakes/snake_facts.json | 233 +++ bot/resources/fun/snakes/snake_idioms.json | 275 +++ bot/resources/fun/snakes/snake_names.json | 2170 ++++++++++++++++++++ bot/resources/fun/snakes/snake_quiz.json | 200 ++ .../fun/snakes/snakes_and_ladders/banner.jpg | Bin 0 -> 17928 bytes .../fun/snakes/snakes_and_ladders/board.jpg | Bin 0 -> 80264 bytes bot/resources/fun/snakes/special_snakes.json | 16 + 13 files changed, 2894 insertions(+) create mode 100644 bot/resources/fun/snakes/snake_cards/backs/card_back1.jpg create mode 100644 bot/resources/fun/snakes/snake_cards/backs/card_back2.jpg create mode 100644 bot/resources/fun/snakes/snake_cards/card_bottom.png create mode 100644 bot/resources/fun/snakes/snake_cards/card_frame.png create mode 100644 bot/resources/fun/snakes/snake_cards/card_top.png create mode 100644 bot/resources/fun/snakes/snake_cards/expressway.ttf create mode 100644 bot/resources/fun/snakes/snake_facts.json create mode 100644 bot/resources/fun/snakes/snake_idioms.json create mode 100644 bot/resources/fun/snakes/snake_names.json create mode 100644 bot/resources/fun/snakes/snake_quiz.json create mode 100644 bot/resources/fun/snakes/snakes_and_ladders/banner.jpg create mode 100644 bot/resources/fun/snakes/snakes_and_ladders/board.jpg create mode 100644 bot/resources/fun/snakes/special_snakes.json (limited to 'bot/resources/fun') diff --git a/bot/resources/fun/snakes/snake_cards/backs/card_back1.jpg b/bot/resources/fun/snakes/snake_cards/backs/card_back1.jpg new file mode 100644 index 00000000..22959fa7 Binary files /dev/null and b/bot/resources/fun/snakes/snake_cards/backs/card_back1.jpg differ diff --git a/bot/resources/fun/snakes/snake_cards/backs/card_back2.jpg b/bot/resources/fun/snakes/snake_cards/backs/card_back2.jpg new file mode 100644 index 00000000..d56edc32 Binary files /dev/null and b/bot/resources/fun/snakes/snake_cards/backs/card_back2.jpg differ diff --git a/bot/resources/fun/snakes/snake_cards/card_bottom.png b/bot/resources/fun/snakes/snake_cards/card_bottom.png new file mode 100644 index 00000000..8b2b91c5 Binary files /dev/null and b/bot/resources/fun/snakes/snake_cards/card_bottom.png differ diff --git a/bot/resources/fun/snakes/snake_cards/card_frame.png b/bot/resources/fun/snakes/snake_cards/card_frame.png new file mode 100644 index 00000000..149a0a5f Binary files /dev/null and b/bot/resources/fun/snakes/snake_cards/card_frame.png differ diff --git a/bot/resources/fun/snakes/snake_cards/card_top.png b/bot/resources/fun/snakes/snake_cards/card_top.png new file mode 100644 index 00000000..e329c873 Binary files /dev/null and b/bot/resources/fun/snakes/snake_cards/card_top.png differ diff --git a/bot/resources/fun/snakes/snake_cards/expressway.ttf b/bot/resources/fun/snakes/snake_cards/expressway.ttf new file mode 100644 index 00000000..39e15794 Binary files /dev/null and b/bot/resources/fun/snakes/snake_cards/expressway.ttf differ diff --git a/bot/resources/fun/snakes/snake_facts.json b/bot/resources/fun/snakes/snake_facts.json new file mode 100644 index 00000000..ca9ba769 --- /dev/null +++ b/bot/resources/fun/snakes/snake_facts.json @@ -0,0 +1,233 @@ +[ + { + "fact": "The decapitated head of a dead snake can still bite, even hours after death. These types of bites usually contain huge amounts of venom." + }, + { + "fact": "What is considered the most 'dangerous' snake depends on both a specific country’s health care and the availability of antivenom following a bite. Based on these criteria, the most dangerous snake in the world is the saw-scaled viper, which bites and kills more people each year than any other snake." + }, + { + "fact": "Snakes live everywhere on Earth except Ireland, Iceland, New Zealand, and the North and South Poles." + }, + { + "fact": "Of the approximately 725 species of venomous snakes worldwide, 250 can kill a human with one bite." + }, + { + "fact": "Snakes evolved from a four-legged reptilian ancestor—most likely a small, burrowing, land-bound lizard—about 100 million years ago. Some snakes, such as pythons and boas, still have traces of back legs." + }, + { + "fact": "The fear of snakes (ophiophobia or herpetophobia) is one of the most common phobias worldwide. Approximately 1/3 of all adult humans areophidiophobic , which suggests that humans have an innate, evolutionary fear of snakes." + }, + { + "fact": "The top 5 most venomous snakes in the world are the inland taipan, the eastern brown snake, the coastal taipan, the tiger snake, and the black tiger snake." + }, + { + "fact": "The warmer a snake’s body, the more quickly it can digest its prey. Typically, it takes 3–5 days for a snake to digest its meal. For very large snakes, such as the anaconda, digestion can take weeks." + }, + { + "fact": "Some animals, such as the Mongoose, are immune to snake venom." + }, + { + "fact": "To avoid predators, some snakes can poop whenever they want. They make themselves so dirty and smelly that predators will run away." + }, + { + "fact": "The heaviest snake in the world is the anaconda. It weighs over 595 pounds (270 kg) and can grow to over 30 feet (9m) long. It has been known to eat caimans, capybaras, and jaguars." + }, + { + "fact": "The Brahminy Blind Snake, or flowerpot snake, is the only snake species made up of solely females and, as such, does not need a mate to reproduce. It is also the most widespread terrestrial snake in the world." + }, + { + "fact": "If a person suddenly turned into a snake, they would be about 4 times longer than they are now and only a few inches thick. While humans have 24 ribs, some snakes can have more than 400." + }, + { + "fact": "The most advanced snake species in the world is believed to be the black mamba. It has the most highly evolved venom delivery system of any snake on Earth. It can strike up to 12 times in a row, though just one bite is enough to kill a grown man.o" + }, + { + "fact": "The inland taipan is the world’s most toxic snake, meaning it has both the most toxic venom and it injects the most venom when it bites. Its venom sacs hold enough poison to kill up to 80 people." + }, + { + "fact": "The death adder has the fastest strike of any snake in the world. It can attack, inject venom, and go back to striking position in under 0.15 seconds." + }, + { + "fact": "While snakes do not have external ears or eardrums, their skin, muscles, and bones carry sound vibrations to their inner ears." + }, + { + "fact": "Some snakes have been known to explode after eating a large meal. For example, a 13-foot python blew up after it tried to eat a 6-foot alligator. The python was found with the alligator’s tail protruding from its midsection. Its head was missing." + }, + { + "fact": "The word 'snake' is from the Proto-Indo-European root *sneg -, meaning 'to crawl, creeping thing.' The word 'serpent' is from the Proto-Indo-European root *serp -, meaning 'to crawl, creep.'" + }, + { + "fact": "Rattlesnake rattles are made of rings of keratin, which is the same material as human hair and fingernails. A rattler will add a new ring each time it sheds its skin." + }, + { + "fact": "Some snakes have over 200 teeth. The teeth aren’t used for chewing but they point backward to prevent prey from escaping the snake’s throat." + }, + { + "fact": "There are about 500 genera and 3,000 different species of snakes. All of them are predators." + }, + { + "fact": "Naturalist Paul Rosolie attempted to be the first person to survive being swallowed by an anaconda in 2014. Though he was wearing a specially designed carbon fiber suit equipped with a breathing system, cameras, and a communication system, he ultimately called off his stunt when he felt like the anaconda was breaking his arm as it tightened its grip around his body." + }, + { + "fact": "There are five recognized species of flying snakes. Growing up to 4 feet, some types can glide up to 330 feet through the air." + }, + { + "fact": "Scales cover every inch of a snake’s body, even its eyes. Scales are thick, tough pieces of skin made from keratin, which is the same material human nails and hair are made from." + }, + { + "fact": "The most common snake in North America is the garter (gardener) snake. This snake is also Massachusetts’s state reptile. While previously thought to be nonvenomous, garter snakes do, in fact, produce a mild neurotoxic venom that is harmless to humans." + }, + { + "fact": "Snakes do not lap up water like mammals do. Instead, they dunk their snouts underwater and use their throats to pump water into their stomachs." + }, + { + "fact": "A snake’s fangs usually last about 6–10 weeks. When a fang wears out, a new one grows in its place." + }, + { + "fact": "Because the end of a snake’s tongue is forked, the two tips taste different amounts of chemicals. Essentially, a snake 'smells in stereo' and can even tell which direction a smell is coming from. It identifies scents on its tongue using pits in the roof of its mouth called the Jacobson’s organ." + }, + { + "fact": "The amount of food a snake eats determines how many offspring it will have. The Arafura file snake eats the least and lays just one egg every decade." + }, + { + "fact": "While smaller snakes, such a tree- or- ground-dwelling snakes, use their tongues to follow the scent trails of prey (such as spiders, birds, and other snakes). Larger snakes, such as boas, have heat-sensing organs called labial (lip) pits in their snouts." + }, + { + "fact": "Snakes typically need to eat only 6–30 meals each year to be healthy." + }, + { + "fact": "Snakes like to lie on roads and rocky areas because stones and rocks absorb heat from the sun, which warms them. Basking on these surfaces warms a snake quickly so it can move. If the temperature reaches below 50° Fahrenheit, a snake’s body does not work properly." + }, + { + "fact": "The Mozambique spitting cobra can spit venom over 8 feet away. It can spit from any position, including lying on the ground or raised up. It prefers to aim for its victim’s eyes." + }, + { + "fact": "Snakes cannot chew, so they must swallow their food whole. They are able to stretch their mouths very wide because they have a very flexible lower jaw. Snakes can eat other animals that are 75%–100% bigger than their own bodies." + }, + { + "fact": "To keep from choking on large prey, a snake will push the end of its trachea, or windpipe, out of its mouth, similar to the way a snorkel works." + }, + { + "fact": "The Gaboon viper has the longest fangs of any snake, reaching about 2 inches (5 cm) long." + }, + { + "fact": "Anacondas can hold their breath for up to 10 minutes under water. Additionally, similar to crocodiles, anacondas have eyes and nostrils that can poke above the water’s surface to increase their stealth and hunting prowess." + }, + { + "fact": "The longest snake ever recorded is the reticulated python. It can reach over 33 feet long, which is big enough to swallow a pig, a deer, or even a person." + }, + { + "fact": "Sea snakes with their paddle-shaped tails can dive over 300 feet into the ocean." + }, + { + "fact": "If a snake is threatened soon after a meal, it will often regurgitate its food so it can quickly escape the perceived threat. A snake’s digestive system can dissolve everything but a prey’s hair, feathers, and claws." + }, + { + "fact": "Snakes do not have eyelids; rather, a single transparent scale called a brille protects their eyes. Most snakes see very well, especially if the object is moving." + }, + { + "fact": "The world’s longest venomous snake is the king cobra from Asia. It can grow up to 18 feet, rear almost as high as a person, growl loudly, and inject enough venom to kill an elephant." + }, + { + "fact": "The king cobra is thought to be one of the most intelligent of all snakes. Additionally, unlike most snakes, who do not care for their young, king cobras are careful parents who defend and protect their eggs from enemies." + }, + { + "fact": "Not all snakes have fangs—only those that kill their prey with venom have them. When their fangs are not in use, they fold them back into the roof of the mouth (except for the coral snake, whose fangs do not fold back)." + }, + { + "fact": "Some venomous snakes have died after biting and poisoning themselves by mistake." + }, + { + "fact": "Elephant trunk snakes are almost completely aquatic. They cannot slither because they lack the broad scales in the belly that help other snakes move on land. Rather, elephant trunk snakes have large knobby scales to hold onto slippery fish and constrict them underwater." + }, + { + "fact": "The shortest known snake is the thread snake. It is about 4 inches long and lives on the island of Barbados in the Caribbean. It is said to be as 'thin as spaghetti' and it feeds primarily on termites and larvae." + }, + { + "fact": "In 2009, a farm worker in East Africa survived an epic 3-hour battle with a 12-foot python after accidentally stepping on the large snake. It coiled around the man and carried him into a tree. The man wrapped his shirt over the snake’s mouth to prevent it from swallowing him, and he was finally rescued by police after calling for help on his cell phone." + }, + { + "fact": "The venom from a Brazilian pit viper is used in a drug to treat high blood pressure." + }, + { + "fact": "The word 'cobra' means 'hooded.' Some cobras have large spots on the back of their hood that look like eyes to make them appear intimating even from behind." + }, + { + "fact": "Some desert snakes, such as the African rock python, sleep during the hottest parts of the desert summer. This summer sleep is similar to hibernation and is called “aestivation.”" + }, + { + "fact": "The black mamba is the world’s fastest snake and the world’s second-longest venomous snake in the world, after the king cobra. Found in East Africa, it can reach speeds of up to 12 mph (19kph). It’s named not from the color of its scales, which is olive green, but from the inside of its mouth, which is inky black. Its venom is highly toxic, and without anti-venom, death in humans usually occurs within 7–15 hours." + }, + { + "fact": "Although a snake’s growth rate slows as it gets older, a snake never stops growing." + }, + { + "fact": "While a snake cannot hear the music of a snake charmer, the snake responds to the vibrations of the charmer’s tapping foot or to the movement of the flute." + }, + { + "fact": "Most snakes are not harmful to humans and they help balance the ecosystem by keeping the population of rats, mice, and birds under control." + }, + { + "fact": "The largest snake fossil ever found is the Titanoboa. It lived over 60 million years ago and reached over 50 feet (15 meters) long. It weighed more than 20 people and ate crocodiles and giant tortoises." + }, + { + "fact": "Two-headed snakes are similar to conjoined twins: an embryo begins to split to create identical twins, but the process does not finish. Such snakes rarely survive in the wild because the two heads have duplicate senses, they fight over food, and one head may try to eat the other head." + }, + { + "fact": "Snakes can be grouped into two sections: primitive snakes and true (typical) snakes. Primitive snakes—such as blind snakes, worm snakes, and thread snakes—represent the earliest forms of snakes. True snakes, such as rat snakes and king snakes, are more evolved and more active." + }, + { + "fact": "The oldest written record that describes snakes is in the Brooklyn Papyrus, which is a medical papyrus dating from ancient Egypt (450 B.C.)." + }, + { + "fact": "Approximately 70% of snakes lay eggs. Those that lay eggs are called oviparous. The other 30% of snakes live in colder climates and give birth to live young because it is too cold for eggs outside the body to develop and hatch." + }, + { + "fact": "Most snakes have an elongated right lung, many have a smaller left lung, and a few even have a third lung. They do not have a sense of taste, and most of their organs are organized linearly." + }, + { + "fact": "The most rare and endangered snake is the St. Lucia racer. There are only 18 to 100 of these snakes left." + }, + { + "fact": "Snakes kill over 40,000 people a year—though, with unreported incidents, the total may be over 100,000. About half of these deaths are in India." + }, + { + "fact": "In some cultures, eating snakes is considered a delicacy. For example, snake soup has been a popular Cantonese delicacy for over 2,000 years." + }, + { + "fact": "In some Asian countries, it is believed that drinking the blood of snakes, particularly the cobra, will increase sexual virility. The blood is usually drained from a live snake and then mixed with liquor." + }, + { + "fact": "In the United States, fewer than 1 in 37,500 people are bitten by venomous snakes each year (7,000–8,000 bites per year), and only 1 in 50 million people will die from snake bite (5–6 fatalities per year). In the U.S., a person is 9 times more likely to die from being struck by lightening than to die from a venomous snakebite." + }, + { + "fact": "Some members of the U.S. Army Special Forces are taught to kill and eat snakes during their survival training, which has earned them the nickname 'Snake Eaters.'" + }, + { + "fact": "One of the great feats of the legendary Greek hero Perseus was to kill Medusa, a female monster whose hair consisted of writhing, venomous snakes." + }, + { + "fact": "The symbol of the snake is one of the most widespread and oldest cultural symbols in history. Snakes often represent the duality of good and evil and of life and death." + }, + { + "fact": "Because snakes shed their skin, they are often symbols of rebirth, transformation, and healing. For example, Asclepius, the god of medicine, carries a staff encircled by a snake." + }, + { + "fact": "The snake has held various meanings throughout history. For example, The Egyptians viewed the snake as representing royalty and deity. In the Jewish rabbinical tradition and in Hinduism, it represents sexual passion and desire. And the Romans interpreted the snake as a symbol of eternal love." + }, + { + "fact": "Anacondas mate in a huge 'breeding ball.' The ball consists of 1 female and nearly 12 males. They stay in a 'mating ball' for up to a month." + }, + { + "fact": "Depending on the species, snakes can live from 4 to over 25 years." + }, + { + "fact": "Snakes that are poisonous have pupils that are shaped like a diamond. Nonpoisonous snakes have round pupils." + }, + { + "fact": "Endangered snakes include the San Francisco garter snake, eastern indigo snake, the king cobra, and Dumeril’s boa." + }, + { + "fact": "A mysterious, new 'mad snake disease' causes captive pythons and boas to tie themselves in knots. Other symptoms include 'stargazing,' which is when snakes stare upwards for long periods of time. Snake experts believe a rodent virus causes the fatal disease." + } +] diff --git a/bot/resources/fun/snakes/snake_idioms.json b/bot/resources/fun/snakes/snake_idioms.json new file mode 100644 index 00000000..ecbeb6ff --- /dev/null +++ b/bot/resources/fun/snakes/snake_idioms.json @@ -0,0 +1,275 @@ +[ + { + "idiom": "snek it up" + }, + { + "idiom": "get ur snek on" + }, + { + "idiom": "snek ur heart out" + }, + { + "idiom": "snek 4 ever" + }, + { + "idiom": "i luve snek" + }, + { + "idiom": "snek bff" + }, + { + "idiom": "boyfriend snek" + }, + { + "idiom": "dont snek ur homies" + }, + { + "idiom": "garden snek" + }, + { + "idiom": "snektie" + }, + { + "idiom": "snek keks" + }, + { + "idiom": "birthday snek!" + }, + { + "idiom": "snek tonight?" + }, + { + "idiom": "snek hott lips" + }, + { + "idiom": "snek u latr" + }, + { + "idiom": "netflx and snek" + }, + { + "idiom": "holy snek prey4u" + }, + { + "idiom": "ghowst snek hauntt u" + }, + { + "idiom": "ipekek snek syrop" + }, + { + "idiom": "2 snek 2 furius" + }, + { + "idiom": "the shawsnek redumpton" + }, + { + "idiom": "snekler's list" + }, + { + "idiom": "snekablanca" + }, + { + "idiom": "romeo n snekulet" + }, + { + "idiom": "citizn snek" + }, + { + "idiom": "gon wit the snek" + }, + { + "idiom": "dont step on snek" + }, + { + "idiom": "the wizrd uf snek" + }, + { + "idiom": "forrest snek" + }, + { + "idiom": "snek of musik" + }, + { + "idiom": "west snek story" + }, + { + "idiom": "snek wars eposide XI" + }, + { + "idiom": "2001: a snek odyssuuy" + }, + { + "idiom": "E.T. the snekstra terrastriul" + }, + { + "idiom": "snekkin' inth rain" + }, + { + "idiom": "dr sneklove" + }, + { + "idiom": "snekley kubrik" + }, + { + "idiom": "willium snekspeare" + }, + { + "idiom": "snek on tutanic" + }, + { + "idiom": "a snekwork orunge" + }, + { + "idiom": "the snek the bad n the ogly" + }, + { + "idiom": "the sneksorcist" + }, + { + "idiom": "gudd snek huntin" + }, + { + "idiom": "leonurdo disnekrio" + }, + { + "idiom": "denzal snekington" + }, + { + "idiom": "snekuel l jocksons" + }, + { + "idiom": "kevn snek" + }, + { + "idiom": "snekthony hopkuns" + }, + { + "idiom": "hugh snekman" + }, + { + "idiom": "snek but it glow in durk" + }, + { + "idiom": "snek but u cn ride it" + }, + { + "idiom": "snek but slep in ur bed" + }, + { + "idiom": "snek but mad frum plastk" + }, + { + "idiom": "snek but bulong 2 ur frnd" + }, + { + "idiom": "sneks on plene" + }, + { + "idiom": "baby snek" + }, + { + "idiom": "trouser snek" + }, + { + "idiom": "momo snek" + }, + { + "idiom": "fast snek" + }, + { + "idiom": "super slow snek" + }, + { + "idiom": "old snek" + }, + { + "idiom": "slimy snek" + }, + { + "idiom": "snek attekk" + }, + { + "idiom": "snek get wrekk" + }, + { + "idiom": "snek you long time" + }, + { + "idiom": "carpenter snek" + }, + { + "idiom": "drain snek" + }, + { + "idiom": "eat ur face snek" + }, + { + "idiom": "kawaii snek" + }, + { + "idiom": "dis snek is soft" + }, + { + "idiom": "snek is 4 yers uld" + }, + { + "idiom": "pls feed snek, is hingry" + }, + { + "idiom": "snek? snek? sneeeeek!!" + }, + { + "idiom": "solid snek" + }, + { + "idiom": "big bos snek" + }, + { + "idiom": "snek republic" + }, + { + "idiom": "snekoslovakia" + }, + { + "idiom": "snek please!" + }, + { + "idiom": "i brok my snek :(" + }, + { + "idiom": "star snek the nxt generatin" + }, + { + "idiom": "azsnek tempul" + }, + { + "idiom": "discosnek" + }, + { + "idiom": "bottlsnek" + }, + { + "idiom": "turtlsnek" + }, + { + "idiom": "cashiers snek" + }, + { + "idiom": "mega snek!!" + }, + { + "idiom": "one tim i saw snek neked" + }, + { + "idiom": "snek cnt clim trees" + }, + { + "idiom": "snek in muth is jus tongue" + }, + { + "idiom": "juan snek" + }, + { + "idiom": "photosnek" + } +] diff --git a/bot/resources/fun/snakes/snake_names.json b/bot/resources/fun/snakes/snake_names.json new file mode 100644 index 00000000..25832550 --- /dev/null +++ b/bot/resources/fun/snakes/snake_names.json @@ -0,0 +1,2170 @@ +[ + { + "name": "Acanthophis", + "scientific": "Acanthophis" + }, + { + "name": "Aesculapian snake", + "scientific": "Aesculapian snake" + }, + { + "name": "African beaked snake", + "scientific": "Rufous beaked snake" + }, + { + "name": "African puff adder", + "scientific": "Bitis arietans" + }, + { + "name": "African rock python", + "scientific": "African rock python" + }, + { + "name": "African twig snake", + "scientific": "Twig snake" + }, + { + "name": "Agkistrodon piscivorus", + "scientific": "Agkistrodon piscivorus" + }, + { + "name": "Ahaetulla", + "scientific": "Ahaetulla" + }, + { + "name": "Amazonian palm viper", + "scientific": "Bothriopsis bilineata" + }, + { + "name": "American copperhead", + "scientific": "Agkistrodon contortrix" + }, + { + "name": "Amethystine python", + "scientific": "Amethystine python" + }, + { + "name": "Anaconda", + "scientific": "Anaconda" + }, + { + "name": "Andaman cat snake", + "scientific": "Boiga andamanensis" + }, + { + "name": "Andrea's keelback", + "scientific": "Amphiesma andreae" + }, + { + "name": "Annulated sea snake", + "scientific": "Hydrophis cyanocinctus" + }, + { + "name": "Arafura file snake", + "scientific": "Acrochordus arafurae" + }, + { + "name": "Arizona black rattlesnake", + "scientific": "Crotalus oreganus cerberus" + }, + { + "name": "Arizona coral snake", + "scientific": "Coral snake" + }, + { + "name": "Aruba rattlesnake", + "scientific": "Crotalus durissus unicolor" + }, + { + "name": "Asian cobra", + "scientific": "Indian cobra" + }, + { + "name": "Asian keelback", + "scientific": "Amphiesma vibakari" + }, + { + "name": "Asp (reptile)", + "scientific": "Asp (reptile)" + }, + { + "name": "Assam keelback", + "scientific": "Amphiesma pealii" + }, + { + "name": "Australian copperhead", + "scientific": "Austrelaps" + }, + { + "name": "Australian scrub python", + "scientific": "Amethystine python" + }, + { + "name": "Baird's rat snake", + "scientific": "Pantherophis bairdi" + }, + { + "name": "Banded Flying Snake", + "scientific": "Banded flying snake" + }, + { + "name": "Banded cat-eyed snake", + "scientific": "Banded cat-eyed snake" + }, + { + "name": "Banded krait", + "scientific": "Banded krait" + }, + { + "name": "Barred wolf snake", + "scientific": "Lycodon striatus" + }, + { + "name": "Beaked sea snake", + "scientific": "Enhydrina schistosa" + }, + { + "name": "Beauty rat snake", + "scientific": "Beauty rat snake" + }, + { + "name": "Beddome's cat snake", + "scientific": "Boiga beddomei" + }, + { + "name": "Beddome's coral snake", + "scientific": "Beddome's coral snake" + }, + { + "name": "Bird snake", + "scientific": "Twig snake" + }, + { + "name": "Black-banded trinket snake", + "scientific": "Oreocryptophis porphyraceus" + }, + { + "name": "Black-headed snake", + "scientific": "Western black-headed snake" + }, + { + "name": "Black-necked cobra", + "scientific": "Black-necked spitting cobra" + }, + { + "name": "Black-necked spitting cobra", + "scientific": "Black-necked spitting cobra" + }, + { + "name": "Black-striped keelback", + "scientific": "Buff striped keelback" + }, + { + "name": "Black-tailed horned pit viper", + "scientific": "Mixcoatlus melanurus" + }, + { + "name": "Black headed python", + "scientific": "Black-headed python" + }, + { + "name": "Black krait", + "scientific": "Greater black krait" + }, + { + "name": "Black mamba", + "scientific": "Black mamba" + }, + { + "name": "Black rat snake", + "scientific": "Rat snake" + }, + { + "name": "Black tree cobra", + "scientific": "Cobra" + }, + { + "name": "Blind snake", + "scientific": "Scolecophidia" + }, + { + "name": "Blonde hognose snake", + "scientific": "Hognose" + }, + { + "name": "Blood python", + "scientific": "Python brongersmai" + }, + { + "name": "Blue krait", + "scientific": "Bungarus candidus" + }, + { + "name": "Blunt-headed tree snake", + "scientific": "Imantodes cenchoa" + }, + { + "name": "Boa constrictor", + "scientific": "Boa constrictor" + }, + { + "name": "Bocourt's water snake", + "scientific": "Subsessor" + }, + { + "name": "Boelen python", + "scientific": "Morelia boeleni" + }, + { + "name": "Boidae", + "scientific": "Boidae" + }, + { + "name": "Boiga", + "scientific": "Boiga" + }, + { + "name": "Boomslang", + "scientific": "Boomslang" + }, + { + "name": "Brahminy blind snake", + "scientific": "Indotyphlops braminus" + }, + { + "name": "Brazilian coral snake", + "scientific": "Coral snake" + }, + { + "name": "Brazilian smooth snake", + "scientific": "Hydrodynastes gigas" + }, + { + "name": "Brown snake (disambiguation)", + "scientific": "Brown snake" + }, + { + "name": "Brown tree snake", + "scientific": "Brown tree snake" + }, + { + "name": "Brown white-lipped python", + "scientific": "Leiopython" + }, + { + "name": "Buff striped keelback", + "scientific": "Buff striped keelback" + }, + { + "name": "Bull snake", + "scientific": "Bull snake" + }, + { + "name": "Burmese keelback", + "scientific": "Burmese keelback water snake" + }, + { + "name": "Burmese krait", + "scientific": "Burmese krait" + }, + { + "name": "Burmese python", + "scientific": "Burmese python" + }, + { + "name": "Burrowing viper", + "scientific": "Atractaspidinae" + }, + { + "name": "Buttermilk racer", + "scientific": "Coluber constrictor anthicus" + }, + { + "name": "California kingsnake", + "scientific": "California kingsnake" + }, + { + "name": "Cantor's pitviper", + "scientific": "Trimeresurus cantori" + }, + { + "name": "Cape cobra", + "scientific": "Cape cobra" + }, + { + "name": "Cape coral snake", + "scientific": "Aspidelaps lubricus" + }, + { + "name": "Cape gopher snake", + "scientific": "Cape gopher snake" + }, + { + "name": "Carpet viper", + "scientific": "Echis" + }, + { + "name": "Cat-eyed night snake", + "scientific": "Banded cat-eyed snake" + }, + { + "name": "Cat-eyed snake", + "scientific": "Banded cat-eyed snake" + }, + { + "name": "Cat snake", + "scientific": "Boiga" + }, + { + "name": "Central American lyre snake", + "scientific": "Trimorphodon biscutatus" + }, + { + "name": "Central ranges taipan", + "scientific": "Taipan" + }, + { + "name": "Chappell Island tiger snake", + "scientific": "Tiger snake" + }, + { + "name": "Checkered garter snake", + "scientific": "Checkered garter snake" + }, + { + "name": "Checkered keelback", + "scientific": "Checkered keelback" + }, + { + "name": "Children's python", + "scientific": "Children's python" + }, + { + "name": "Chinese cobra", + "scientific": "Chinese cobra" + }, + { + "name": "Coachwhip snake", + "scientific": "Masticophis flagellum" + }, + { + "name": "Coastal taipan", + "scientific": "Coastal taipan" + }, + { + "name": "Cobra", + "scientific": "Cobra" + }, + { + "name": "Collett's snake", + "scientific": "Collett's snake" + }, + { + "name": "Common adder", + "scientific": "Vipera berus" + }, + { + "name": "Common cobra", + "scientific": "Chinese cobra" + }, + { + "name": "Common garter snake", + "scientific": "Common garter snake" + }, + { + "name": "Common ground snake", + "scientific": "Western ground snake" + }, + { + "name": "Common keelback (disambiguation)", + "scientific": "Common keelback" + }, + { + "name": "Common tiger snake", + "scientific": "Tiger snake" + }, + { + "name": "Common worm snake", + "scientific": "Indotyphlops braminus" + }, + { + "name": "Congo snake", + "scientific": "Amphiuma" + }, + { + "name": "Congo water cobra", + "scientific": "Naja christyi" + }, + { + "name": "Coral snake", + "scientific": "Coral snake" + }, + { + "name": "Corn snake", + "scientific": "Corn snake" + }, + { + "name": "Coronado Island rattlesnake", + "scientific": "Crotalus oreganus caliginis" + }, + { + "name": "Crossed viper", + "scientific": "Vipera berus" + }, + { + "name": "Crotalus cerastes", + "scientific": "Crotalus cerastes" + }, + { + "name": "Crotalus durissus", + "scientific": "Crotalus durissus" + }, + { + "name": "Crotalus horridus", + "scientific": "Timber rattlesnake" + }, + { + "name": "Crowned snake", + "scientific": "Tantilla" + }, + { + "name": "Cuban boa", + "scientific": "Chilabothrus angulifer" + }, + { + "name": "Cuban wood snake", + "scientific": "Tropidophis melanurus" + }, + { + "name": "Dasypeltis", + "scientific": "Dasypeltis" + }, + { + "name": "Desert death adder", + "scientific": "Desert death adder" + }, + { + "name": "Desert kingsnake", + "scientific": "Desert kingsnake" + }, + { + "name": "Desert woma python", + "scientific": "Woma python" + }, + { + "name": "Diamond python", + "scientific": "Morelia spilota spilota" + }, + { + "name": "Dog-toothed cat snake", + "scientific": "Boiga cynodon" + }, + { + "name": "Down's tiger snake", + "scientific": "Tiger snake" + }, + { + "name": "Dubois's sea snake", + "scientific": "Aipysurus duboisii" + }, + { + "name": "Durango rock rattlesnake", + "scientific": "Crotalus lepidus klauberi" + }, + { + "name": "Dusty hognose snake", + "scientific": "Hognose" + }, + { + "name": "Dwarf beaked snake", + "scientific": "Dwarf beaked snake" + }, + { + "name": "Dwarf boa", + "scientific": "Boa constrictor" + }, + { + "name": "Dwarf pipe snake", + "scientific": "Anomochilus" + }, + { + "name": "Eastern brown snake", + "scientific": "Eastern brown snake" + }, + { + "name": "Eastern coral snake", + "scientific": "Micrurus fulvius" + }, + { + "name": "Eastern diamondback rattlesnake", + "scientific": "Eastern diamondback rattlesnake" + }, + { + "name": "Eastern green mamba", + "scientific": "Eastern green mamba" + }, + { + "name": "Eastern hognose snake", + "scientific": "Eastern hognose snake" + }, + { + "name": "Eastern mud snake", + "scientific": "Mud snake" + }, + { + "name": "Eastern racer", + "scientific": "Coluber constrictor" + }, + { + "name": "Eastern tiger snake", + "scientific": "Tiger snake" + }, + { + "name": "Eastern water cobra", + "scientific": "Cobra" + }, + { + "name": "Elaps harlequin snake", + "scientific": "Micrurus fulvius" + }, + { + "name": "Eunectes", + "scientific": "Eunectes" + }, + { + "name": "European Smooth Snake", + "scientific": "Smooth snake" + }, + { + "name": "False cobra", + "scientific": "False cobra" + }, + { + "name": "False coral snake", + "scientific": "Coral snake" + }, + { + "name": "False water cobra", + "scientific": "Hydrodynastes gigas" + }, + { + "name": "Fierce snake", + "scientific": "Inland taipan" + }, + { + "name": "Flying snake", + "scientific": "Chrysopelea" + }, + { + "name": "Forest cobra", + "scientific": "Forest cobra" + }, + { + "name": "Forsten's cat snake", + "scientific": "Boiga forsteni" + }, + { + "name": "Fox snake", + "scientific": "Fox snake" + }, + { + "name": "Gaboon viper", + "scientific": "Gaboon viper" + }, + { + "name": "Garter snake", + "scientific": "Garter snake" + }, + { + "name": "Giant Malagasy hognose snake", + "scientific": "Hognose" + }, + { + "name": "Glossy snake", + "scientific": "Glossy snake" + }, + { + "name": "Gold-ringed cat snake", + "scientific": "Boiga dendrophila" + }, + { + "name": "Gold tree cobra", + "scientific": "Pseudohaje goldii" + }, + { + "name": "Golden tree snake", + "scientific": "Chrysopelea ornata" + }, + { + "name": "Gopher snake", + "scientific": "Pituophis catenifer" + }, + { + "name": "Grand Canyon rattlesnake", + "scientific": "Crotalus oreganus abyssus" + }, + { + "name": "Grass snake", + "scientific": "Grass snake" + }, + { + "name": "Gray cat snake", + "scientific": "Boiga ocellata" + }, + { + "name": "Great Plains rat snake", + "scientific": "Pantherophis emoryi" + }, + { + "name": "Green anaconda", + "scientific": "Green anaconda" + }, + { + "name": "Green rat snake", + "scientific": "Rat snake" + }, + { + "name": "Green tree python", + "scientific": "Green tree python" + }, + { + "name": "Grey-banded kingsnake", + "scientific": "Gray-banded kingsnake" + }, + { + "name": "Grey Lora", + "scientific": "Leptophis stimsoni" + }, + { + "name": "Halmahera python", + "scientific": "Morelia tracyae" + }, + { + "name": "Harlequin coral snake", + "scientific": "Micrurus fulvius" + }, + { + "name": "Herald snake", + "scientific": "Caduceus" + }, + { + "name": "High Woods coral snake", + "scientific": "Coral snake" + }, + { + "name": "Hill keelback", + "scientific": "Amphiesma monticola" + }, + { + "name": "Himalayan keelback", + "scientific": "Amphiesma platyceps" + }, + { + "name": "Hognose snake", + "scientific": "Hognose" + }, + { + "name": "Hognosed viper", + "scientific": "Porthidium" + }, + { + "name": "Hook Nosed Sea Snake", + "scientific": "Enhydrina schistosa" + }, + { + "name": "Hoop snake", + "scientific": "Hoop snake" + }, + { + "name": "Hopi rattlesnake", + "scientific": "Crotalus viridis nuntius" + }, + { + "name": "Indian cobra", + "scientific": "Indian cobra" + }, + { + "name": "Indian egg-eater", + "scientific": "Indian egg-eating snake" + }, + { + "name": "Indian flying snake", + "scientific": "Chrysopelea ornata" + }, + { + "name": "Indian krait", + "scientific": "Bungarus" + }, + { + "name": "Indigo snake", + "scientific": "Drymarchon" + }, + { + "name": "Inland carpet python", + "scientific": "Morelia spilota metcalfei" + }, + { + "name": "Inland taipan", + "scientific": "Inland taipan" + }, + { + "name": "Jamaican boa", + "scientific": "Jamaican boa" + }, + { + "name": "Jan's hognose snake", + "scientific": "Hognose" + }, + { + "name": "Japanese forest rat snake", + "scientific": "Euprepiophis conspicillatus" + }, + { + "name": "Japanese rat snake", + "scientific": "Japanese rat snake" + }, + { + "name": "Japanese striped snake", + "scientific": "Japanese striped snake" + }, + { + "name": "Kayaudi dwarf reticulated python", + "scientific": "Reticulated python" + }, + { + "name": "Keelback", + "scientific": "Natricinae" + }, + { + "name": "Khasi Hills keelback", + "scientific": "Amphiesma khasiense" + }, + { + "name": "King Island tiger snake", + "scientific": "Tiger snake" + }, + { + "name": "King brown", + "scientific": "Mulga snake" + }, + { + "name": "King cobra", + "scientific": "King cobra" + }, + { + "name": "King rat snake", + "scientific": "Rat snake" + }, + { + "name": "King snake", + "scientific": "Kingsnake" + }, + { + "name": "Krait", + "scientific": "Bungarus" + }, + { + "name": "Krefft's tiger snake", + "scientific": "Tiger snake" + }, + { + "name": "Lance-headed rattlesnake", + "scientific": "Crotalus polystictus" + }, + { + "name": "Lancehead", + "scientific": "Bothrops" + }, + { + "name": "Large shield snake", + "scientific": "Pseudotyphlops" + }, + { + "name": "Leptophis ahaetulla", + "scientific": "Leptophis ahaetulla" + }, + { + "name": "Lesser black krait", + "scientific": "Lesser black krait" + }, + { + "name": "Long-nosed adder", + "scientific": "Eastern hognose snake" + }, + { + "name": "Long-nosed tree snake", + "scientific": "Western hognose snake" + }, + { + "name": "Long-nosed whip snake", + "scientific": "Ahaetulla nasuta" + }, + { + "name": "Long-tailed rattlesnake", + "scientific": "Rattlesnake" + }, + { + "name": "Longnosed worm snake", + "scientific": "Leptotyphlops macrorhynchus" + }, + { + "name": "Lyre snake", + "scientific": "Trimorphodon" + }, + { + "name": "Madagascar ground boa", + "scientific": "Acrantophis madagascariensis" + }, + { + "name": "Malayan krait", + "scientific": "Bungarus candidus" + }, + { + "name": "Malayan long-glanded coral snake", + "scientific": "Calliophis bivirgata" + }, + { + "name": "Malayan pit viper", + "scientific": "Pit viper" + }, + { + "name": "Mamba", + "scientific": "Mamba" + }, + { + "name": "Mamushi", + "scientific": "Mamushi" + }, + { + "name": "Manchurian Black Water Snake", + "scientific": "Elaphe schrenckii" + }, + { + "name": "Mandarin rat snake", + "scientific": "Mandarin rat snake" + }, + { + "name": "Mangrove snake (disambiguation)", + "scientific": "Mangrove snake" + }, + { + "name": "Many-banded krait", + "scientific": "Many-banded krait" + }, + { + "name": "Many-banded tree snake", + "scientific": "Many-banded tree snake" + }, + { + "name": "Many-spotted cat snake", + "scientific": "Boiga multomaculata" + }, + { + "name": "Massasauga rattlesnake", + "scientific": "Massasauga" + }, + { + "name": "Mexican black kingsnake", + "scientific": "Mexican black kingsnake" + }, + { + "name": "Mexican green rattlesnake", + "scientific": "Crotalus basiliscus" + }, + { + "name": "Mexican hognose snake", + "scientific": "Hognose" + }, + { + "name": "Mexican parrot snake", + "scientific": "Leptophis mexicanus" + }, + { + "name": "Mexican racer", + "scientific": "Coluber constrictor oaxaca" + }, + { + "name": "Mexican vine snake", + "scientific": "Oxybelis aeneus" + }, + { + "name": "Mexican west coast rattlesnake", + "scientific": "Crotalus basiliscus" + }, + { + "name": "Micropechis ikaheka", + "scientific": "Micropechis ikaheka" + }, + { + "name": "Midget faded rattlesnake", + "scientific": "Crotalus oreganus concolor" + }, + { + "name": "Milk snake", + "scientific": "Milk snake" + }, + { + "name": "Moccasin snake", + "scientific": "Agkistrodon piscivorus" + }, + { + "name": "Modest keelback", + "scientific": "Amphiesma modestum" + }, + { + "name": "Mojave desert sidewinder", + "scientific": "Crotalus cerastes" + }, + { + "name": "Mojave rattlesnake", + "scientific": "Crotalus scutulatus" + }, + { + "name": "Mole viper", + "scientific": "Atractaspidinae" + }, + { + "name": "Moluccan flying snake", + "scientific": "Chrysopelea" + }, + { + "name": "Montpellier snake", + "scientific": "Malpolon monspessulanus" + }, + { + "name": "Mud adder", + "scientific": "Mud adder" + }, + { + "name": "Mud snake", + "scientific": "Mud snake" + }, + { + "name": "Mussurana", + "scientific": "Mussurana" + }, + { + "name": "Narrowhead Garter Snake", + "scientific": "Garter snake" + }, + { + "name": "Nicobar Island keelback", + "scientific": "Amphiesma nicobariense" + }, + { + "name": "Nicobar cat snake", + "scientific": "Boiga wallachi" + }, + { + "name": "Night snake", + "scientific": "Night snake" + }, + { + "name": "Nilgiri keelback", + "scientific": "Nilgiri keelback" + }, + { + "name": "North eastern king snake", + "scientific": "Eastern hognose snake" + }, + { + "name": "Northeastern hill krait", + "scientific": "Northeastern hill krait" + }, + { + "name": "Northern black-tailed rattlesnake", + "scientific": "Crotalus molossus" + }, + { + "name": "Northern tree snake", + "scientific": "Dendrelaphis calligastra" + }, + { + "name": "Northern water snake", + "scientific": "Northern water snake" + }, + { + "name": "Northern white-lipped python", + "scientific": "Leiopython" + }, + { + "name": "Oaxacan small-headed rattlesnake", + "scientific": "Crotalus intermedius gloydi" + }, + { + "name": "Okinawan habu", + "scientific": "Okinawan habu" + }, + { + "name": "Olive sea snake", + "scientific": "Aipysurus laevis" + }, + { + "name": "Opheodrys", + "scientific": "Opheodrys" + }, + { + "name": "Orange-collared keelback", + "scientific": "Rhabdophis himalayanus" + }, + { + "name": "Ornate flying snake", + "scientific": "Chrysopelea ornata" + }, + { + "name": "Oxybelis", + "scientific": "Oxybelis" + }, + { + "name": "Palestine viper", + "scientific": "Vipera palaestinae" + }, + { + "name": "Paradise flying snake", + "scientific": "Chrysopelea paradisi" + }, + { + "name": "Parrot snake", + "scientific": "Leptophis ahaetulla" + }, + { + "name": "Patchnose snake", + "scientific": "Salvadora (snake)" + }, + { + "name": "Pelagic sea snake", + "scientific": "Yellow-bellied sea snake" + }, + { + "name": "Peninsula tiger snake", + "scientific": "Tiger snake" + }, + { + "name": "Perrotet's shieldtail snake", + "scientific": "Plectrurus perrotetii" + }, + { + "name": "Persian rat snake", + "scientific": "Rat snake" + }, + { + "name": "Pine snake", + "scientific": "Pine snake" + }, + { + "name": "Pit viper", + "scientific": "Pit viper" + }, + { + "name": "Plains hognose snake", + "scientific": "Western hognose snake" + }, + { + "name": "Prairie kingsnake", + "scientific": "Lampropeltis calligaster" + }, + { + "name": "Pygmy python", + "scientific": "Pygmy python" + }, + { + "name": "Pythonidae", + "scientific": "Pythonidae" + }, + { + "name": "Queen snake", + "scientific": "Queen snake" + }, + { + "name": "Rat snake", + "scientific": "Rat snake" + }, + { + "name": "Rattler", + "scientific": "Rattlesnake" + }, + { + "name": "Rattlesnake", + "scientific": "Rattlesnake" + }, + { + "name": "Red-bellied black snake", + "scientific": "Red-bellied black snake" + }, + { + "name": "Red-headed krait", + "scientific": "Red-headed krait" + }, + { + "name": "Red-necked keelback", + "scientific": "Rhabdophis subminiatus" + }, + { + "name": "Red-tailed bamboo pitviper", + "scientific": "Trimeresurus erythrurus" + }, + { + "name": "Red-tailed boa", + "scientific": "Boa constrictor" + }, + { + "name": "Red-tailed pipe snake", + "scientific": "Cylindrophis ruffus" + }, + { + "name": "Red blood python", + "scientific": "Python brongersmai" + }, + { + "name": "Red diamond rattlesnake", + "scientific": "Crotalus ruber" + }, + { + "name": "Reticulated python", + "scientific": "Reticulated python" + }, + { + "name": "Ribbon snake", + "scientific": "Ribbon snake" + }, + { + "name": "Ringed hognose snake", + "scientific": "Hognose" + }, + { + "name": "Rosy boa", + "scientific": "Rosy boa" + }, + { + "name": "Rough green snake", + "scientific": "Opheodrys aestivus" + }, + { + "name": "Rubber boa", + "scientific": "Rubber boa" + }, + { + "name": "Rufous beaked snake", + "scientific": "Rufous beaked snake" + }, + { + "name": "Russell's viper", + "scientific": "Russell's viper" + }, + { + "name": "San Francisco garter snake", + "scientific": "San Francisco garter snake" + }, + { + "name": "Sand boa", + "scientific": "Erycinae" + }, + { + "name": "Sand viper", + "scientific": "Sand viper" + }, + { + "name": "Saw-scaled viper", + "scientific": "Echis" + }, + { + "name": "Scarlet kingsnake", + "scientific": "Scarlet kingsnake" + }, + { + "name": "Sea snake", + "scientific": "Hydrophiinae" + }, + { + "name": "Selayer reticulated python", + "scientific": "Reticulated python" + }, + { + "name": "Shield-nosed cobra", + "scientific": "Shield-nosed cobra" + }, + { + "name": "Shield-tailed snake", + "scientific": "Uropeltidae" + }, + { + "name": "Sikkim keelback", + "scientific": "Sikkim keelback" + }, + { + "name": "Sind krait", + "scientific": "Sind krait" + }, + { + "name": "Smooth green snake", + "scientific": "Smooth green snake" + }, + { + "name": "South American hognose snake", + "scientific": "Hognose" + }, + { + "name": "South Andaman krait", + "scientific": "South Andaman krait" + }, + { + "name": "South eastern corn snake", + "scientific": "Corn snake" + }, + { + "name": "Southern Pacific rattlesnake", + "scientific": "Crotalus oreganus helleri" + }, + { + "name": "Southern black racer", + "scientific": "Southern black racer" + }, + { + "name": "Southern hognose snake", + "scientific": "Southern hognose snake" + }, + { + "name": "Southern white-lipped python", + "scientific": "Leiopython" + }, + { + "name": "Southwestern blackhead snake", + "scientific": "Tantilla hobartsmithi" + }, + { + "name": "Southwestern carpet python", + "scientific": "Morelia spilota imbricata" + }, + { + "name": "Southwestern speckled rattlesnake", + "scientific": "Crotalus mitchellii pyrrhus" + }, + { + "name": "Speckled hognose snake", + "scientific": "Hognose" + }, + { + "name": "Speckled kingsnake", + "scientific": "Lampropeltis getula holbrooki" + }, + { + "name": "Spectacled cobra", + "scientific": "Indian cobra" + }, + { + "name": "Sri Lanka cat snake", + "scientific": "Boiga ceylonensis" + }, + { + "name": "Stiletto snake", + "scientific": "Atractaspidinae" + }, + { + "name": "Stimson's python", + "scientific": "Stimson's python" + }, + { + "name": "Striped snake", + "scientific": "Japanese striped snake" + }, + { + "name": "Sumatran short-tailed python", + "scientific": "Python curtus" + }, + { + "name": "Sunbeam snake", + "scientific": "Xenopeltis" + }, + { + "name": "Taipan", + "scientific": "Taipan" + }, + { + "name": "Tan racer", + "scientific": "Coluber constrictor etheridgei" + }, + { + "name": "Tancitaran dusky rattlesnake", + "scientific": "Crotalus pusillus" + }, + { + "name": "Tanimbar python", + "scientific": "Reticulated python" + }, + { + "name": "Tasmanian tiger snake", + "scientific": "Tiger snake" + }, + { + "name": "Tawny cat snake", + "scientific": "Boiga ochracea" + }, + { + "name": "Temple pit viper", + "scientific": "Pit viper" + }, + { + "name": "Tentacled snake", + "scientific": "Erpeton tentaculatum" + }, + { + "name": "Texas Coral Snake", + "scientific": "Coral snake" + }, + { + "name": "Texas blind snake", + "scientific": "Leptotyphlops dulcis" + }, + { + "name": "Texas garter snake", + "scientific": "Texas garter snake" + }, + { + "name": "Texas lyre snake", + "scientific": "Trimorphodon biscutatus vilkinsonii" + }, + { + "name": "Texas night snake", + "scientific": "Hypsiglena jani" + }, + { + "name": "Thai cobra", + "scientific": "King cobra" + }, + { + "name": "Three-lined ground snake", + "scientific": "Atractus trilineatus" + }, + { + "name": "Tic polonga", + "scientific": "Russell's viper" + }, + { + "name": "Tiger rattlesnake", + "scientific": "Crotalus tigris" + }, + { + "name": "Tiger snake", + "scientific": "Tiger snake" + }, + { + "name": "Tigre snake", + "scientific": "Spilotes pullatus" + }, + { + "name": "Timber rattlesnake", + "scientific": "Timber rattlesnake" + }, + { + "name": "Tree snake", + "scientific": "Brown tree snake" + }, + { + "name": "Tri-color hognose snake", + "scientific": "Hognose" + }, + { + "name": "Trinket snake", + "scientific": "Trinket snake" + }, + { + "name": "Tropical rattlesnake", + "scientific": "Crotalus durissus" + }, + { + "name": "Twig snake", + "scientific": "Twig snake" + }, + { + "name": "Twin-Barred tree snake", + "scientific": "Banded flying snake" + }, + { + "name": "Twin-spotted rat snake", + "scientific": "Rat snake" + }, + { + "name": "Twin-spotted rattlesnake", + "scientific": "Crotalus pricei" + }, + { + "name": "Uracoan rattlesnake", + "scientific": "Crotalus durissus vegrandis" + }, + { + "name": "Viperidae", + "scientific": "Viperidae" + }, + { + "name": "Wall's keelback", + "scientific": "Amphiesma xenura" + }, + { + "name": "Wart snake", + "scientific": "Acrochordidae" + }, + { + "name": "Water adder", + "scientific": "Agkistrodon piscivorus" + }, + { + "name": "Water moccasin", + "scientific": "Agkistrodon piscivorus" + }, + { + "name": "West Indian racer", + "scientific": "Antiguan racer" + }, + { + "name": "Western blind snake", + "scientific": "Leptotyphlops humilis" + }, + { + "name": "Western carpet python", + "scientific": "Morelia spilota" + }, + { + "name": "Western coral snake", + "scientific": "Coral snake" + }, + { + "name": "Western diamondback rattlesnake", + "scientific": "Western diamondback rattlesnake" + }, + { + "name": "Western green mamba", + "scientific": "Western green mamba" + }, + { + "name": "Western ground snake", + "scientific": "Western ground snake" + }, + { + "name": "Western hognose snake", + "scientific": "Western hognose snake" + }, + { + "name": "Western mud snake", + "scientific": "Mud snake" + }, + { + "name": "Western tiger snake", + "scientific": "Tiger snake" + }, + { + "name": "Western woma python", + "scientific": "Woma python" + }, + { + "name": "White-lipped keelback", + "scientific": "Amphiesma leucomystax" + }, + { + "name": "Wolf snake", + "scientific": "Lycodon capucinus" + }, + { + "name": "Woma python", + "scientific": "Woma python" + }, + { + "name": "Wutu", + "scientific": "Bothrops alternatus" + }, + { + "name": "Wynaad keelback", + "scientific": "Amphiesma monticola" + }, + { + "name": "Yellow-banded sea snake", + "scientific": "Yellow-bellied sea snake" + }, + { + "name": "Yellow-bellied sea snake", + "scientific": "Yellow-bellied sea snake" + }, + { + "name": "Yellow-lipped sea snake", + "scientific": "Yellow-lipped sea krait" + }, + { + "name": "Yellow-striped rat snake", + "scientific": "Rat snake" + }, + { + "name": "Yellow anaconda", + "scientific": "Yellow anaconda" + }, + { + "name": "Yellow cobra", + "scientific": "Cape cobra" + }, + { + "name": "Yunnan keelback", + "scientific": "Amphiesma parallelum" + }, + { + "name": "Abaco Island boa", + "scientific": "Epicrates exsul" + }, + { + "name": "Agkistrodon bilineatus", + "scientific": "Agkistrodon bilineatus" + }, + { + "name": "Amazon tree boa", + "scientific": "Corallus hortulanus" + }, + { + "name": "Andaman cobra", + "scientific": "Andaman cobra" + }, + { + "name": "Angolan python", + "scientific": "Python anchietae" + }, + { + "name": "Arabian cobra", + "scientific": "Arabian cobra" + }, + { + "name": "Asp viper", + "scientific": "Vipera aspis" + }, + { + "name": "Ball Python", + "scientific": "Ball python" + }, + { + "name": "Ball python", + "scientific": "Ball python" + }, + { + "name": "Bamboo pitviper", + "scientific": "Trimeresurus gramineus" + }, + { + "name": "Banded pitviper", + "scientific": "Trimeresurus fasciatus" + }, + { + "name": "Banded water cobra", + "scientific": "Naja annulata" + }, + { + "name": "Barbour's pit viper", + "scientific": "Mixcoatlus barbouri" + }, + { + "name": "Bismarck ringed python", + "scientific": "Bothrochilus" + }, + { + "name": "Black-speckled palm-pitviper", + "scientific": "Bothriechis nigroviridis" + }, + { + "name": "Bluntnose viper", + "scientific": "Macrovipera lebetina" + }, + { + "name": "Bornean pitviper", + "scientific": "Trimeresurus borneensis" + }, + { + "name": "Borneo short-tailed python", + "scientific": "Borneo python" + }, + { + "name": "Bothrops jararacussu", + "scientific": "Bothrops jararacussu" + }, + { + "name": "Bredl's python", + "scientific": "Morelia bredli" + }, + { + "name": "Brongersma's pitviper", + "scientific": "Trimeresurus brongersmai" + }, + { + "name": "Brown spotted pitviper", + "scientific": "Trimeresurus mucrosquamatus" + }, + { + "name": "Brown water python", + "scientific": "Liasis fuscus" + }, + { + "name": "Burrowing cobra", + "scientific": "Egyptian cobra" + }, + { + "name": "Bush viper", + "scientific": "Atheris" + }, + { + "name": "Calabar python", + "scientific": "Calabar python" + }, + { + "name": "Caspian cobra", + "scientific": "Caspian cobra" + }, + { + "name": "Centralian carpet python", + "scientific": "Morelia bredli" + }, + { + "name": "Chinese tree viper", + "scientific": "Trimeresurus stejnegeri" + }, + { + "name": "Coastal carpet python", + "scientific": "Morelia spilota mcdowelli" + }, + { + "name": "Colorado desert sidewinder", + "scientific": "Crotalus cerastes laterorepens" + }, + { + "name": "Common lancehead", + "scientific": "Bothrops atrox" + }, + { + "name": "Cyclades blunt-nosed viper", + "scientific": "Macrovipera schweizeri" + }, + { + "name": "Dauan Island water python", + "scientific": "Liasis fuscus" + }, + { + "name": "De Schauensee's anaconda", + "scientific": "Eunectes deschauenseei" + }, + { + "name": "Dumeril's boa", + "scientific": "Acrantophis dumerili" + }, + { + "name": "Dusky pigmy rattlesnake", + "scientific": "Sistrurus miliarius barbouri" + }, + { + "name": "Dwarf sand adder", + "scientific": "Bitis peringueyi" + }, + { + "name": "Egyptian cobra", + "scientific": "Egyptian cobra" + }, + { + "name": "Elegant pitviper", + "scientific": "Trimeresurus elegans" + }, + { + "name": "Emerald tree boa", + "scientific": "Emerald tree boa" + }, + { + "name": "Equatorial spitting cobra", + "scientific": "Equatorial spitting cobra" + }, + { + "name": "European asp", + "scientific": "Vipera aspis" + }, + { + "name": "Eyelash palm-pitviper", + "scientific": "Bothriechis schlegelii" + }, + { + "name": "Eyelash pit viper", + "scientific": "Bothriechis schlegelii" + }, + { + "name": "Eyelash viper", + "scientific": "Bothriechis schlegelii" + }, + { + "name": "False horned viper", + "scientific": "Pseudocerastes" + }, + { + "name": "Fan-Si-Pan horned pitviper", + "scientific": "Trimeresurus cornutus" + }, + { + "name": "Fea's viper", + "scientific": "Azemiops" + }, + { + "name": "Fifty pacer", + "scientific": "Deinagkistrodon" + }, + { + "name": "Flat-nosed pitviper", + "scientific": "Trimeresurus puniceus" + }, + { + "name": "Godman's pit viper", + "scientific": "Cerrophidion godmani" + }, + { + "name": "Great Lakes bush viper", + "scientific": "Atheris nitschei" + }, + { + "name": "Green palm viper", + "scientific": "Bothriechis lateralis" + }, + { + "name": "Green tree pit viper", + "scientific": "Trimeresurus gramineus" + }, + { + "name": "Guatemalan palm viper", + "scientific": "Bothriechis aurifer" + }, + { + "name": "Guatemalan tree viper", + "scientific": "Bothriechis bicolor" + }, + { + "name": "Hagen's pitviper", + "scientific": "Trimeresurus hageni" + }, + { + "name": "Hairy bush viper", + "scientific": "Atheris hispida" + }, + { + "name": "Himehabu", + "scientific": "Ovophis okinavensis" + }, + { + "name": "Hogg Island boa", + "scientific": "Boa constrictor imperator" + }, + { + "name": "Honduran palm viper", + "scientific": "Bothriechis marchi" + }, + { + "name": "Horned desert viper", + "scientific": "Cerastes cerastes" + }, + { + "name": "Horseshoe pitviper", + "scientific": "Trimeresurus strigatus" + }, + { + "name": "Hundred pacer", + "scientific": "Deinagkistrodon" + }, + { + "name": "Hutton's tree viper", + "scientific": "Tropidolaemus huttoni" + }, + { + "name": "Indian python", + "scientific": "Python molurus" + }, + { + "name": "Indian tree viper", + "scientific": "Trimeresurus gramineus" + }, + { + "name": "Indochinese spitting cobra", + "scientific": "Indochinese spitting cobra" + }, + { + "name": "Indonesian water python", + "scientific": "Liasis mackloti" + }, + { + "name": "Javan spitting cobra", + "scientific": "Javan spitting cobra" + }, + { + "name": "Jerdon's pitviper", + "scientific": "Trimeresurus jerdonii" + }, + { + "name": "Jumping viper", + "scientific": "Atropoides" + }, + { + "name": "Jungle carpet python", + "scientific": "Morelia spilota cheynei" + }, + { + "name": "Kanburian pit viper", + "scientific": "Trimeresurus kanburiensis" + }, + { + "name": "Kaulback's lance-headed pitviper", + "scientific": "Trimeresurus kaulbacki" + }, + { + "name": "Kaznakov's viper", + "scientific": "Vipera kaznakovi" + }, + { + "name": "Kham Plateau pitviper", + "scientific": "Protobothrops xiangchengensis" + }, + { + "name": "Lachesis (genus)", + "scientific": "Lachesis (genus)" + }, + { + "name": "Large-eyed pitviper", + "scientific": "Trimeresurus macrops" + }, + { + "name": "Large-scaled tree viper", + "scientific": "Trimeresurus macrolepis" + }, + { + "name": "Leaf-nosed viper", + "scientific": "Eristicophis" + }, + { + "name": "Leaf viper", + "scientific": "Atheris squamigera" + }, + { + "name": "Levant viper", + "scientific": "Macrovipera lebetina" + }, + { + "name": "Long-nosed viper", + "scientific": "Vipera ammodytes" + }, + { + "name": "Macklot's python", + "scientific": "Liasis mackloti" + }, + { + "name": "Madagascar tree boa", + "scientific": "Sanzinia" + }, + { + "name": "Malabar rock pitviper", + "scientific": "Trimeresurus malabaricus" + }, + { + "name": "Malcolm's tree viper", + "scientific": "Trimeresurus sumatranus malcolmi" + }, + { + "name": "Mandalay cobra", + "scientific": "Mandalay spitting cobra" + }, + { + "name": "Mangrove pit viper", + "scientific": "Trimeresurus purpureomaculatus" + }, + { + "name": "Mangshan pitviper", + "scientific": "Trimeresurus mangshanensis" + }, + { + "name": "McMahon's viper", + "scientific": "Eristicophis" + }, + { + "name": "Mexican palm-pitviper", + "scientific": "Bothriechis rowleyi" + }, + { + "name": "Monocled cobra", + "scientific": "Monocled cobra" + }, + { + "name": "Motuo bamboo pitviper", + "scientific": "Trimeresurus medoensis" + }, + { + "name": "Mozambique spitting cobra", + "scientific": "Mozambique spitting cobra" + }, + { + "name": "Namaqua dwarf adder", + "scientific": "Bitis schneideri" + }, + { + "name": "Namib dwarf sand adder", + "scientific": "Bitis peringueyi" + }, + { + "name": "New Guinea carpet python", + "scientific": "Morelia spilota variegata" + }, + { + "name": "Nicobar bamboo pitviper", + "scientific": "Trimeresurus labialis" + }, + { + "name": "Nitsche's bush viper", + "scientific": "Atheris nitschei" + }, + { + "name": "Nitsche's tree viper", + "scientific": "Atheris nitschei" + }, + { + "name": "Northwestern carpet python", + "scientific": "Morelia spilota variegata" + }, + { + "name": "Nubian spitting cobra", + "scientific": "Nubian spitting cobra" + }, + { + "name": "Oenpelli python", + "scientific": "Oenpelli python" + }, + { + "name": "Olive python", + "scientific": "Liasis olivaceus" + }, + { + "name": "Pallas' viper", + "scientific": "Gloydius halys" + }, + { + "name": "Palm viper", + "scientific": "Bothriechis lateralis" + }, + { + "name": "Papuan python", + "scientific": "Apodora" + }, + { + "name": "Peringuey's adder", + "scientific": "Bitis peringueyi" + }, + { + "name": "Philippine cobra", + "scientific": "Philippine cobra" + }, + { + "name": "Philippine pitviper", + "scientific": "Trimeresurus flavomaculatus" + }, + { + "name": "Pope's tree viper", + "scientific": "Trimeresurus popeorum" + }, + { + "name": "Portuguese viper", + "scientific": "Vipera seoanei" + }, + { + "name": "Puerto Rican boa", + "scientific": "Puerto Rican boa" + }, + { + "name": "Rainbow boa", + "scientific": "Rainbow boa" + }, + { + "name": "Red spitting cobra", + "scientific": "Red spitting cobra" + }, + { + "name": "Rhinoceros viper", + "scientific": "Bitis nasicornis" + }, + { + "name": "Rhombic night adder", + "scientific": "Causus maculatus" + }, + { + "name": "Rinkhals", + "scientific": "Rinkhals" + }, + { + "name": "Rinkhals cobra", + "scientific": "Rinkhals" + }, + { + "name": "River jack", + "scientific": "Bitis nasicornis" + }, + { + "name": "Rough-scaled bush viper", + "scientific": "Atheris hispida" + }, + { + "name": "Rough-scaled python", + "scientific": "Rough-scaled python" + }, + { + "name": "Rough-scaled tree viper", + "scientific": "Atheris hispida" + }, + { + "name": "Royal python", + "scientific": "Ball python" + }, + { + "name": "Rungwe tree viper", + "scientific": "Atheris nitschei rungweensis" + }, + { + "name": "Sakishima habu", + "scientific": "Trimeresurus elegans" + }, + { + "name": "Savu python", + "scientific": "Liasis mackloti savuensis" + }, + { + "name": "Schlegel's viper", + "scientific": "Bothriechis schlegelii" + }, + { + "name": "Schultze's pitviper", + "scientific": "Trimeresurus schultzei" + }, + { + "name": "Sedge viper", + "scientific": "Atheris nitschei" + }, + { + "name": "Sharp-nosed viper", + "scientific": "Deinagkistrodon" + }, + { + "name": "Siamese palm viper", + "scientific": "Trimeresurus puniceus" + }, + { + "name": "Side-striped palm-pitviper", + "scientific": "Bothriechis lateralis" + }, + { + "name": "Snorkel viper", + "scientific": "Deinagkistrodon" + }, + { + "name": "Snouted cobra", + "scientific": "Snouted cobra" + }, + { + "name": "Sonoran sidewinder", + "scientific": "Crotalus cerastes cercobombus" + }, + { + "name": "Southern Indonesian spitting cobra", + "scientific": "Javan spitting cobra" + }, + { + "name": "Southern Philippine cobra", + "scientific": "Samar cobra" + }, + { + "name": "Spiny bush viper", + "scientific": "Atheris hispida" + }, + { + "name": "Spitting cobra", + "scientific": "Spitting cobra" + }, + { + "name": "Spotted python", + "scientific": "Spotted python" + }, + { + "name": "Sri Lankan pit viper", + "scientific": "Trimeresurus trigonocephalus" + }, + { + "name": "Stejneger's bamboo pitviper", + "scientific": "Trimeresurus stejnegeri" + }, + { + "name": "Storm water cobra", + "scientific": "Naja annulata" + }, + { + "name": "Sumatran tree viper", + "scientific": "Trimeresurus sumatranus" + }, + { + "name": "Temple viper", + "scientific": "Tropidolaemus wagleri" + }, + { + "name": "Tibetan bamboo pitviper", + "scientific": "Trimeresurus tibetanus" + }, + { + "name": "Tiger pit viper", + "scientific": "Trimeresurus kanburiensis" + }, + { + "name": "Timor python", + "scientific": "Python timoriensis" + }, + { + "name": "Tokara habu", + "scientific": "Trimeresurus tokarensis" + }, + { + "name": "Tree boa", + "scientific": "Emerald tree boa" + }, + { + "name": "Undulated pit viper", + "scientific": "Ophryacus undulatus" + }, + { + "name": "Ursini's viper", + "scientific": "Vipera ursinii" + }, + { + "name": "Wagler's pit viper", + "scientific": "Tropidolaemus wagleri" + }, + { + "name": "West African brown spitting cobra", + "scientific": "Mozambique spitting cobra" + }, + { + "name": "White-lipped tree viper", + "scientific": "Trimeresurus albolabris" + }, + { + "name": "Wirot's pit viper", + "scientific": "Trimeresurus puniceus" + }, + { + "name": "Yellow-lined palm viper", + "scientific": "Bothriechis lateralis" + }, + { + "name": "Zebra spitting cobra", + "scientific": "Naja nigricincta" + }, + { + "name": "Yarara", + "scientific": "Bothrops jararaca" + }, + { + "name": "Wetar Island python", + "scientific": "Liasis macklot" + }, + { + "name": "Urutus", + "scientific": "Bothrops alternatus" + }, + { + "name": "Titanboa", + "scientific": "Titanoboa" + } +] diff --git a/bot/resources/fun/snakes/snake_quiz.json b/bot/resources/fun/snakes/snake_quiz.json new file mode 100644 index 00000000..8c426b22 --- /dev/null +++ b/bot/resources/fun/snakes/snake_quiz.json @@ -0,0 +1,200 @@ +[ + { + "id": 0, + "question": "How long have snakes been roaming the Earth for?", + "options": { + "a": "3 million years", + "b": "30 million years", + "c": "130 million years", + "d": "200 million years" + }, + "answerkey": "c" + }, + { + "id": 1, + "question": "What characteristics do all snakes share?", + "options": { + "a": "They are carnivoes", + "b": "They are all programming languages", + "c": "They're all cold-blooded", + "d": "They are both carnivores and cold-blooded" + }, + "answerkey": "c" + }, + { + "id": 2, + "question": "How do snakes hear?", + "options": { + "a": "With small ears", + "b": "Through their skin", + "c": "Through their tail", + "d": "They don't use their ears at all" + }, + "answerkey": "b" + }, + { + "id": 3, + "question": "What can't snakes see?", + "options": { + "a": "Colour", + "b": "Light", + "c": "Both of the above", + "d": "Other snakes" + }, + "answerkey": "a" + }, + { + "id": 4, + "question": "What unique vision ability do boas and pythons possess?", + "options": { + "a": "Night vision", + "b": "Infrared vision", + "c": "See through walls", + "d": "They don't have vision" + }, + "answerkey": "b" + }, + { + "id": 5, + "question": "How does a snake smell?", + "options": { + "a": "Quite pleasant", + "b": "Through its nose", + "c": "Through its tongues", + "d": "Both through its nose and its tongues" + }, + "answerkey": "d" + }, + { + "id": 6, + "question": "Where are Jacobson's organs located in snakes?", + "options": { + "a": "Mouth", + "b": "Tail", + "c": "Stomach", + "d": "Liver" + }, + "answerkey": "a" + }, + { + "id": 7, + "question": "Snakes have very similar internal organs compared to humans. Snakes, however; lack the following:", + "options": { + "a": "A diaphragm", + "b": "Intestines", + "c": "Lungs", + "d": "Kidney" + }, + "answerkey": "a" + }, + { + "id": 8, + "question": "Snakes have different shaped lungs than humans. What do snakes have?", + "options": { + "a": "An elongated right lung", + "b": "A small left lung", + "c": "Both of the above", + "d": "None of the above" + }, + "answerkey": "c" + }, + { + "id": 9, + "question": "What's true about two-headed snakes?", + "options": { + "a": "They're a myth!", + "b": "They rarely survive in the wild", + "c": "They're very dangerous", + "d": "They can kiss each other" + }, + "answerkey": "b" + }, + { + "id": 10, + "question": "What substance covers a snake's skin?", + "options": { + "a": "Calcium", + "b": "Keratin", + "c": "Copper", + "d": "Iron" + }, + "answerkey": "b" + }, + { + "id": 11, + "question": "What snake doesn't have to have a mate to lay eggs?", + "options": { + "a": "Copperhead", + "b": "Cornsnake", + "c": "Kingsnake", + "d": "Flower pot snake" + }, + "answerkey": "d" + }, + { + "id": 12, + "question": "What snake is the longest?", + "options": { + "a": "Green anaconda", + "b": "Reticulated python", + "c": "King cobra", + "d": "Kingsnake" + }, + "answerkey": "b" + }, + { + "id": 13, + "question": "Though invasive species can now be found in the Everglades, in which three continents are pythons (members of the family Pythonidae) found in the wild?", + "options": { + "a": "Africa, Asia and Australia", + "b": "Africa, Australia and Europe", + "c": "Africa, Australia and South America", + "d": "Africa, Asia and South America" + }, + "answerkey": "a" + }, + { + "id": 14, + "question": "Pythons are held as some of the most dangerous snakes on earth, but are often confused with anacondas. Which of these is *not* a difference between pythons and anacondas?", + "options": { + "a": "Pythons suffocate their prey, anacondas crush them", + "b": "Pythons lay eggs, anacondas give birth to live young", + "c": "Pythons grow longer, anacondas grow heavier", + "d": "Pythons generally spend less time in water than anacondas do" + }, + "answerkey": "a" + }, + { + "id": 15, + "question": "Pythons are unable to chew their food, and so swallow prey whole. Which of these methods is most commonly demonstrated to help a python to swallow large prey?", + "options": { + "a": "The python's stomach pressure is reduced, so prey is sucked in", + "b": "An extra set of upper teeth 'walk' along the prey", + "c": "The python holds its head up, so prey falls into its stomach", + "d": "Prey is pushed against a barrier and is forced down the python's throat" + }, + "answerkey": "b" + }, + { + "id": 16, + "question": "Pythons, like many large constrictors, possess vestigial hind limbs. Whilst these 'spurs' serve no purpose in locomotion, how are they put to use by some male pythons? ", + "options": { + "a": "To store sperm", + "b": "To release pheromones", + "c": "To grip females during mating", + "d": "To fight off rival males" + }, + "answerkey": "c" + }, + { + "id": 17, + "question": "Pythons tend to travel by the rectilinear method (in straight lines) when on land, as opposed to the concertina method (s-shaped movement). Why do large pythons tend not to use the concertina method? ", + "options": { + "a": "Their spine is too inflexible", + "b": "They move too slowly", + "c": "The scales on their backs are too rigid", + "d": "They are too heavy" + }, + "answerkey": "d" + } +] diff --git a/bot/resources/fun/snakes/snakes_and_ladders/banner.jpg b/bot/resources/fun/snakes/snakes_and_ladders/banner.jpg new file mode 100644 index 00000000..69eaaf12 Binary files /dev/null and b/bot/resources/fun/snakes/snakes_and_ladders/banner.jpg differ diff --git a/bot/resources/fun/snakes/snakes_and_ladders/board.jpg b/bot/resources/fun/snakes/snakes_and_ladders/board.jpg new file mode 100644 index 00000000..20032e39 Binary files /dev/null and b/bot/resources/fun/snakes/snakes_and_ladders/board.jpg differ diff --git a/bot/resources/fun/snakes/special_snakes.json b/bot/resources/fun/snakes/special_snakes.json new file mode 100644 index 00000000..46214f66 --- /dev/null +++ b/bot/resources/fun/snakes/special_snakes.json @@ -0,0 +1,16 @@ +[ + { + "name": "Bob Ross", + "info": "Robert Norman Ross (October 29, 1942 – July 4, 1995) was an American painter, art instructor, and television host. He was the creator and host of The Joy of Painting, an instructional television program that aired from 1983 to 1994 on PBS in the United States, and also aired in Canada, Latin America, and Europe.", + "image_list": [ + "https://d3atagt0rnqk7k.cloudfront.net/wp-content/uploads/2016/09/23115633/bob-ross-1-1280x800.jpg" + ] + }, + { + "name": "Mystery Snake", + "info": "The Mystery Snake is rumored to be a thin, serpentine creature that hides in spaghetti dinners. It has yellow, pasta-like scales with a completely smooth texture, and is quite glossy. ", + "image_list": [ + "https://img.thrfun.com/img/080/349/spaghetti_dinner_l1.jpg" + ] + } +] -- cgit v1.2.3 From 02512e43f3d68ffd89654c5f2e9e3e9a27c0c018 Mon Sep 17 00:00:00 2001 From: Janine vN Date: Sun, 5 Sep 2021 00:31:20 -0400 Subject: Move game and fun commands to Fun folder, fix ddg This moves all the fun commands and games into the fun folder. This commit also makes changes to the duck_game. It was setting a footer during an embed init, which is no longer possible with the version of d.py we use. Additionally, an issue with editing an embed that had a local image loaded. The workaround for the time being is to update the message, not the embed. --- bot/exts/evergreen/__init__.py | 0 bot/exts/evergreen/battleship.py | 448 ---------- bot/exts/evergreen/catify.py | 86 -- bot/exts/evergreen/coinflip.py | 53 -- bot/exts/evergreen/connect_four.py | 452 ---------- bot/exts/evergreen/duck_game.py | 356 -------- bot/exts/evergreen/fun.py | 250 ------ bot/exts/evergreen/game.py | 485 ----------- bot/exts/evergreen/magic_8ball.py | 30 - bot/exts/evergreen/minesweeper.py | 270 ------ bot/exts/evergreen/movie.py | 205 ----- bot/exts/evergreen/recommend_game.py | 51 -- bot/exts/evergreen/rps.py | 57 -- bot/exts/evergreen/space.py | 236 ----- bot/exts/evergreen/speedrun.py | 26 - bot/exts/evergreen/status_codes.py | 87 -- bot/exts/evergreen/tic_tac_toe.py | 335 -------- bot/exts/evergreen/trivia_quiz.py | 593 ------------- bot/exts/evergreen/wonder_twins.py | 49 -- bot/exts/evergreen/xkcd.py | 91 -- bot/exts/fun/__init__.py | 0 bot/exts/fun/battleship.py | 448 ++++++++++ bot/exts/fun/catify.py | 86 ++ bot/exts/fun/coinflip.py | 53 ++ bot/exts/fun/connect_four.py | 452 ++++++++++ bot/exts/fun/duck_game.py | 336 ++++++++ bot/exts/fun/fun.py | 250 ++++++ bot/exts/fun/game.py | 485 +++++++++++ bot/exts/fun/magic_8ball.py | 30 + bot/exts/fun/minesweeper.py | 270 ++++++ bot/exts/fun/movie.py | 205 +++++ bot/exts/fun/recommend_game.py | 51 ++ bot/exts/fun/rps.py | 57 ++ bot/exts/fun/space.py | 236 +++++ bot/exts/fun/speedrun.py | 26 + bot/exts/fun/status_codes.py | 87 ++ bot/exts/fun/tic_tac_toe.py | 335 ++++++++ bot/exts/fun/trivia_quiz.py | 593 +++++++++++++ bot/exts/fun/wonder_twins.py | 49 ++ bot/exts/fun/xkcd.py | 91 ++ bot/resources/evergreen/LuckiestGuy-Regular.ttf | Bin 58292 -> 0 bytes bot/resources/evergreen/all_cards.png | Bin 155466 -> 0 bytes bot/resources/evergreen/caesar_info.json | 4 - bot/resources/evergreen/ducks_help_ex.png | Bin 343921 -> 0 bytes .../evergreen/game_recs/chrono_trigger.json | 7 - .../evergreen/game_recs/digimon_world.json | 7 - bot/resources/evergreen/game_recs/doom_2.json | 7 - bot/resources/evergreen/game_recs/skyrim.json | 7 - bot/resources/evergreen/html_colours.json | 150 ---- bot/resources/evergreen/magic8ball.json | 22 - bot/resources/evergreen/speedrun_links.json | 18 - bot/resources/evergreen/trivia_quiz.json | 912 -------------------- bot/resources/evergreen/wonder_twins.yaml | 99 --- bot/resources/evergreen/xkcd_colours.json | 951 --------------------- bot/resources/fun/LuckiestGuy-Regular.ttf | Bin 0 -> 58292 bytes bot/resources/fun/all_cards.png | Bin 0 -> 155466 bytes bot/resources/fun/caesar_info.json | 4 + bot/resources/fun/ducks_help_ex.png | Bin 0 -> 343921 bytes bot/resources/fun/game_recs/chrono_trigger.json | 7 + bot/resources/fun/game_recs/digimon_world.json | 7 + bot/resources/fun/game_recs/doom_2.json | 7 + bot/resources/fun/game_recs/skyrim.json | 7 + bot/resources/fun/html_colours.json | 150 ++++ bot/resources/fun/magic8ball.json | 22 + bot/resources/fun/speedrun_links.json | 18 + bot/resources/fun/trivia_quiz.json | 912 ++++++++++++++++++++ bot/resources/fun/wonder_twins.yaml | 99 +++ bot/resources/fun/xkcd_colours.json | 951 +++++++++++++++++++++ 68 files changed, 6324 insertions(+), 6344 deletions(-) delete mode 100644 bot/exts/evergreen/__init__.py delete mode 100644 bot/exts/evergreen/battleship.py delete mode 100644 bot/exts/evergreen/catify.py delete mode 100644 bot/exts/evergreen/coinflip.py delete mode 100644 bot/exts/evergreen/connect_four.py delete mode 100644 bot/exts/evergreen/duck_game.py delete mode 100644 bot/exts/evergreen/fun.py delete mode 100644 bot/exts/evergreen/game.py delete mode 100644 bot/exts/evergreen/magic_8ball.py delete mode 100644 bot/exts/evergreen/minesweeper.py delete mode 100644 bot/exts/evergreen/movie.py delete mode 100644 bot/exts/evergreen/recommend_game.py delete mode 100644 bot/exts/evergreen/rps.py delete mode 100644 bot/exts/evergreen/space.py delete mode 100644 bot/exts/evergreen/speedrun.py delete mode 100644 bot/exts/evergreen/status_codes.py delete mode 100644 bot/exts/evergreen/tic_tac_toe.py delete mode 100644 bot/exts/evergreen/trivia_quiz.py delete mode 100644 bot/exts/evergreen/wonder_twins.py delete mode 100644 bot/exts/evergreen/xkcd.py create mode 100644 bot/exts/fun/__init__.py create mode 100644 bot/exts/fun/battleship.py create mode 100644 bot/exts/fun/catify.py create mode 100644 bot/exts/fun/coinflip.py create mode 100644 bot/exts/fun/connect_four.py create mode 100644 bot/exts/fun/duck_game.py create mode 100644 bot/exts/fun/fun.py create mode 100644 bot/exts/fun/game.py create mode 100644 bot/exts/fun/magic_8ball.py create mode 100644 bot/exts/fun/minesweeper.py create mode 100644 bot/exts/fun/movie.py create mode 100644 bot/exts/fun/recommend_game.py create mode 100644 bot/exts/fun/rps.py create mode 100644 bot/exts/fun/space.py create mode 100644 bot/exts/fun/speedrun.py create mode 100644 bot/exts/fun/status_codes.py create mode 100644 bot/exts/fun/tic_tac_toe.py create mode 100644 bot/exts/fun/trivia_quiz.py create mode 100644 bot/exts/fun/wonder_twins.py create mode 100644 bot/exts/fun/xkcd.py delete mode 100644 bot/resources/evergreen/LuckiestGuy-Regular.ttf delete mode 100644 bot/resources/evergreen/all_cards.png delete mode 100644 bot/resources/evergreen/caesar_info.json delete mode 100644 bot/resources/evergreen/ducks_help_ex.png delete mode 100644 bot/resources/evergreen/game_recs/chrono_trigger.json delete mode 100644 bot/resources/evergreen/game_recs/digimon_world.json delete mode 100644 bot/resources/evergreen/game_recs/doom_2.json delete mode 100644 bot/resources/evergreen/game_recs/skyrim.json delete mode 100644 bot/resources/evergreen/html_colours.json delete mode 100644 bot/resources/evergreen/magic8ball.json delete mode 100644 bot/resources/evergreen/speedrun_links.json delete mode 100644 bot/resources/evergreen/trivia_quiz.json delete mode 100644 bot/resources/evergreen/wonder_twins.yaml delete mode 100644 bot/resources/evergreen/xkcd_colours.json create mode 100644 bot/resources/fun/LuckiestGuy-Regular.ttf create mode 100644 bot/resources/fun/all_cards.png create mode 100644 bot/resources/fun/caesar_info.json create mode 100644 bot/resources/fun/ducks_help_ex.png create mode 100644 bot/resources/fun/game_recs/chrono_trigger.json create mode 100644 bot/resources/fun/game_recs/digimon_world.json create mode 100644 bot/resources/fun/game_recs/doom_2.json create mode 100644 bot/resources/fun/game_recs/skyrim.json create mode 100644 bot/resources/fun/html_colours.json create mode 100644 bot/resources/fun/magic8ball.json create mode 100644 bot/resources/fun/speedrun_links.json create mode 100644 bot/resources/fun/trivia_quiz.json create mode 100644 bot/resources/fun/wonder_twins.yaml create mode 100644 bot/resources/fun/xkcd_colours.json (limited to 'bot/resources/fun') diff --git a/bot/exts/evergreen/__init__.py b/bot/exts/evergreen/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bot/exts/evergreen/battleship.py b/bot/exts/evergreen/battleship.py deleted file mode 100644 index f4351954..00000000 --- a/bot/exts/evergreen/battleship.py +++ /dev/null @@ -1,448 +0,0 @@ -import asyncio -import logging -import random -import re -from dataclasses import dataclass -from functools import partial -from typing import Optional - -import discord -from discord.ext import commands - -from bot.bot import Bot -from bot.constants import Colours - -log = logging.getLogger(__name__) - - -@dataclass -class Square: - """Each square on the battleship grid - if they contain a boat and if they've been aimed at.""" - - boat: Optional[str] - aimed: bool - - -Grid = list[list[Square]] -EmojiSet = dict[tuple[bool, bool], str] - - -@dataclass -class Player: - """Each player in the game - their messages for the boards and their current grid.""" - - user: Optional[discord.Member] - board: Optional[discord.Message] - opponent_board: discord.Message - grid: Grid - - -# The name of the ship and its size -SHIPS = { - "Carrier": 5, - "Battleship": 4, - "Cruiser": 3, - "Submarine": 3, - "Destroyer": 2, -} - - -# For these two variables, the first boolean is whether the square is a ship (True) or not (False). -# The second boolean is whether the player has aimed for that square (True) or not (False) - -# This is for the player's own board which shows the location of their own ships. -SHIP_EMOJIS = { - (True, True): ":fire:", - (True, False): ":ship:", - (False, True): ":anger:", - (False, False): ":ocean:", -} - -# This is for the opposing player's board which only shows aimed locations. -HIDDEN_EMOJIS = { - (True, True): ":red_circle:", - (True, False): ":black_circle:", - (False, True): ":white_circle:", - (False, False): ":black_circle:", -} - -# For the top row of the board -LETTERS = ( - ":stop_button::regional_indicator_a::regional_indicator_b::regional_indicator_c::regional_indicator_d:" - ":regional_indicator_e::regional_indicator_f::regional_indicator_g::regional_indicator_h:" - ":regional_indicator_i::regional_indicator_j:" -) - -# For the first column of the board -NUMBERS = [ - ":one:", - ":two:", - ":three:", - ":four:", - ":five:", - ":six:", - ":seven:", - ":eight:", - ":nine:", - ":keycap_ten:", -] - -CROSS_EMOJI = "\u274e" -HAND_RAISED_EMOJI = "\U0001f64b" - - -class Game: - """A Battleship Game.""" - - def __init__( - self, - bot: Bot, - channel: discord.TextChannel, - player1: discord.Member, - player2: discord.Member - ): - - self.bot = bot - self.public_channel = channel - - self.p1 = Player(player1, None, None, self.generate_grid()) - self.p2 = Player(player2, None, None, self.generate_grid()) - - self.gameover: bool = False - - self.turn: Optional[discord.Member] = None - self.next: Optional[discord.Member] = None - - self.match: Optional[re.Match] = None - self.surrender: bool = False - - self.setup_grids() - - @staticmethod - def generate_grid() -> Grid: - """Generates a grid by instantiating the Squares.""" - return [[Square(None, False) for _ in range(10)] for _ in range(10)] - - @staticmethod - def format_grid(player: Player, emojiset: EmojiSet) -> str: - """ - Gets and formats the grid as a list into a string to be output to the DM. - - Also adds the Letter and Number indexes. - """ - grid = [ - [emojiset[bool(square.boat), square.aimed] for square in row] - for row in player.grid - ] - - rows = ["".join([number] + row) for number, row in zip(NUMBERS, grid)] - return "\n".join([LETTERS] + rows) - - @staticmethod - def get_square(grid: Grid, square: str) -> Square: - """Grabs a square from a grid with an inputted key.""" - index = ord(square[0].upper()) - ord("A") - number = int(square[1:]) - - return grid[number-1][index] # -1 since lists are indexed from 0 - - async def game_over( - self, - *, - winner: discord.Member, - loser: discord.Member - ) -> None: - """Removes games from list of current games and announces to public chat.""" - await self.public_channel.send(f"Game Over! {winner.mention} won against {loser.mention}") - - for player in (self.p1, self.p2): - grid = self.format_grid(player, SHIP_EMOJIS) - await self.public_channel.send(f"{player.user}'s Board:\n{grid}") - - @staticmethod - def check_sink(grid: Grid, boat: str) -> bool: - """Checks if all squares containing a given boat have sunk.""" - return all(square.aimed for row in grid for square in row if square.boat == boat) - - @staticmethod - def check_gameover(grid: Grid) -> bool: - """Checks if all boats have been sunk.""" - return all(square.aimed for row in grid for square in row if square.boat) - - def setup_grids(self) -> None: - """Places the boats on the grids to initialise the game.""" - for player in (self.p1, self.p2): - for name, size in SHIPS.items(): - while True: # Repeats if about to overwrite another boat - ship_collision = False - coords = [] - - coord1 = random.randint(0, 9) - coord2 = random.randint(0, 10 - size) - - if random.choice((True, False)): # Vertical or Horizontal - x, y = coord1, coord2 - xincr, yincr = 0, 1 - else: - x, y = coord2, coord1 - xincr, yincr = 1, 0 - - for i in range(size): - new_x = x + (xincr * i) - new_y = y + (yincr * i) - if player.grid[new_x][new_y].boat: # Check if there's already a boat - ship_collision = True - break - coords.append((new_x, new_y)) - if not ship_collision: # If not overwriting any other boat spaces, break loop - break - - for x, y in coords: - player.grid[x][y].boat = name - - async def print_grids(self) -> None: - """Prints grids to the DM channels.""" - # Convert squares into Emoji - - boards = [ - self.format_grid(player, emojiset) - for emojiset in (HIDDEN_EMOJIS, SHIP_EMOJIS) - for player in (self.p1, self.p2) - ] - - locations = ( - (self.p2, "opponent_board"), (self.p1, "opponent_board"), - (self.p1, "board"), (self.p2, "board") - ) - - for board, location in zip(boards, locations): - player, attr = location - if getattr(player, attr): - await getattr(player, attr).edit(content=board) - else: - setattr(player, attr, await player.user.send(board)) - - def predicate(self, message: discord.Message) -> bool: - """Predicate checking the message typed for each turn.""" - if message.author == self.turn.user and message.channel == self.turn.user.dm_channel: - if message.content.lower() == "surrender": - self.surrender = True - return True - self.match = re.fullmatch("([A-J]|[a-j]) ?((10)|[1-9])", message.content.strip()) - if not self.match: - self.bot.loop.create_task(message.add_reaction(CROSS_EMOJI)) - return bool(self.match) - - async def take_turn(self) -> Optional[Square]: - """Lets the player who's turn it is choose a square.""" - square = None - turn_message = await self.turn.user.send( - "It's your turn! Type the square you want to fire at. Format it like this: A1\n" - "Type `surrender` to give up." - ) - await self.next.user.send("Their turn", delete_after=3.0) - while True: - try: - await self.bot.wait_for("message", check=self.predicate, timeout=60.0) - except asyncio.TimeoutError: - await self.turn.user.send("You took too long. Game over!") - await self.next.user.send(f"{self.turn.user} took too long. Game over!") - await self.public_channel.send( - f"Game over! {self.turn.user.mention} timed out so {self.next.user.mention} wins!" - ) - self.gameover = True - break - else: - if self.surrender: - await self.next.user.send(f"{self.turn.user} surrendered. Game over!") - await self.public_channel.send( - f"Game over! {self.turn.user.mention} surrendered to {self.next.user.mention}!" - ) - self.gameover = True - break - square = self.get_square(self.next.grid, self.match.string) - if square.aimed: - await self.turn.user.send("You've already aimed at this square!", delete_after=3.0) - else: - break - await turn_message.delete() - return square - - async def hit(self, square: Square, alert_messages: list[discord.Message]) -> None: - """Occurs when a player successfully aims for a ship.""" - await self.turn.user.send("Hit!", delete_after=3.0) - alert_messages.append(await self.next.user.send("Hit!")) - if self.check_sink(self.next.grid, square.boat): - await self.turn.user.send(f"You've sunk their {square.boat} ship!", delete_after=3.0) - alert_messages.append(await self.next.user.send(f"Oh no! Your {square.boat} ship sunk!")) - if self.check_gameover(self.next.grid): - await self.turn.user.send("You win!") - await self.next.user.send("You lose!") - self.gameover = True - await self.game_over(winner=self.turn.user, loser=self.next.user) - - async def start_game(self) -> None: - """Begins the game.""" - await self.p1.user.send(f"You're playing battleship with {self.p2.user}.") - await self.p2.user.send(f"You're playing battleship with {self.p1.user}.") - - alert_messages = [] - - self.turn = self.p1 - self.next = self.p2 - - while True: - await self.print_grids() - - if self.gameover: - return - - square = await self.take_turn() - if not square: - return - square.aimed = True - - for message in alert_messages: - await message.delete() - - alert_messages = [] - alert_messages.append(await self.next.user.send(f"{self.turn.user} aimed at {self.match.string}!")) - - if square.boat: - await self.hit(square, alert_messages) - if self.gameover: - return - else: - await self.turn.user.send("Miss!", delete_after=3.0) - alert_messages.append(await self.next.user.send("Miss!")) - - self.turn, self.next = self.next, self.turn - - -class Battleship(commands.Cog): - """Play the classic game Battleship!""" - - def __init__(self, bot: Bot): - self.bot = bot - self.games: list[Game] = [] - self.waiting: list[discord.Member] = [] - - def predicate( - self, - ctx: commands.Context, - announcement: discord.Message, - reaction: discord.Reaction, - user: discord.Member - ) -> bool: - """Predicate checking the criteria for the announcement message.""" - if self.already_playing(ctx.author): # If they've joined a game since requesting a player 2 - return True # Is dealt with later on - if ( - user.id not in (ctx.me.id, ctx.author.id) - and str(reaction.emoji) == HAND_RAISED_EMOJI - and reaction.message.id == announcement.id - ): - if self.already_playing(user): - self.bot.loop.create_task(ctx.send(f"{user.mention} You're already playing a game!")) - self.bot.loop.create_task(announcement.remove_reaction(reaction, user)) - return False - - if user in self.waiting: - self.bot.loop.create_task(ctx.send( - f"{user.mention} Please cancel your game first before joining another one." - )) - self.bot.loop.create_task(announcement.remove_reaction(reaction, user)) - return False - - return True - - if ( - user.id == ctx.author.id - and str(reaction.emoji) == CROSS_EMOJI - and reaction.message.id == announcement.id - ): - return True - return False - - def already_playing(self, player: discord.Member) -> bool: - """Check if someone is already in a game.""" - return any(player in (game.p1.user, game.p2.user) for game in self.games) - - @commands.group(invoke_without_command=True) - @commands.guild_only() - async def battleship(self, ctx: commands.Context) -> None: - """ - Play a game of Battleship with someone else! - - This will set up a message waiting for someone else to react and play along. - The game takes place entirely in DMs. - Make sure you have your DMs open so that the bot can message you. - """ - if self.already_playing(ctx.author): - await ctx.send("You're already playing a game!") - return - - if ctx.author in self.waiting: - await ctx.send("You've already sent out a request for a player 2.") - return - - announcement = await ctx.send( - "**Battleship**: A new game is about to start!\n" - f"Press {HAND_RAISED_EMOJI} to play against {ctx.author.mention}!\n" - f"(Cancel the game with {CROSS_EMOJI}.)" - ) - self.waiting.append(ctx.author) - await announcement.add_reaction(HAND_RAISED_EMOJI) - await announcement.add_reaction(CROSS_EMOJI) - - try: - reaction, user = await self.bot.wait_for( - "reaction_add", - check=partial(self.predicate, ctx, announcement), - timeout=60.0 - ) - except asyncio.TimeoutError: - self.waiting.remove(ctx.author) - await announcement.delete() - await ctx.send(f"{ctx.author.mention} Seems like there's no one here to play...") - return - - if str(reaction.emoji) == CROSS_EMOJI: - self.waiting.remove(ctx.author) - await announcement.delete() - await ctx.send(f"{ctx.author.mention} Game cancelled.") - return - - await announcement.delete() - self.waiting.remove(ctx.author) - if self.already_playing(ctx.author): - return - game = Game(self.bot, ctx.channel, ctx.author, user) - self.games.append(game) - try: - await game.start_game() - self.games.remove(game) - except discord.Forbidden: - await ctx.send( - f"{ctx.author.mention} {user.mention} " - "Game failed. This is likely due to you not having your DMs open. Check and try again." - ) - self.games.remove(game) - except Exception: - # End the game in the event of an unforseen error so the players aren't stuck in a game - await ctx.send(f"{ctx.author.mention} {user.mention} An error occurred. Game failed.") - self.games.remove(game) - raise - - @battleship.command(name="ships", aliases=("boats",)) - async def battleship_ships(self, ctx: commands.Context) -> None: - """Lists the ships that are found on the battleship grid.""" - embed = discord.Embed(colour=Colours.blue) - embed.add_field(name="Name", value="\n".join(SHIPS)) - embed.add_field(name="Size", value="\n".join(str(size) for size in SHIPS.values())) - await ctx.send(embed=embed) - - -def setup(bot: Bot) -> None: - """Load the Battleship Cog.""" - bot.add_cog(Battleship(bot)) diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py deleted file mode 100644 index 32dfae09..00000000 --- a/bot/exts/evergreen/catify.py +++ /dev/null @@ -1,86 +0,0 @@ -import random -from contextlib import suppress -from typing import Optional - -from discord import AllowedMentions, Embed, Forbidden -from discord.ext import commands - -from bot.bot import Bot -from bot.constants import Cats, Colours, NEGATIVE_REPLIES -from bot.utils import helpers - - -class Catify(commands.Cog): - """Cog for the catify command.""" - - @commands.command(aliases=("ᓚᘏᗢify", "ᓚᘏᗢ")) - @commands.cooldown(1, 5, commands.BucketType.user) - async def catify(self, ctx: commands.Context, *, text: Optional[str]) -> None: - """ - Convert the provided text into a cat themed sentence by interspercing cats throughout text. - - If no text is given then the users nickname is edited. - """ - if not text: - display_name = ctx.author.display_name - - if len(display_name) > 26: - embed = Embed( - title=random.choice(NEGATIVE_REPLIES), - description=( - "Your display name is too long to be catified! " - "Please change it to be under 26 characters." - ), - color=Colours.soft_red - ) - await ctx.send(embed=embed) - return - - else: - display_name += f" | {random.choice(Cats.cats)}" - - await ctx.send(f"Your catified nickname is: `{display_name}`", allowed_mentions=AllowedMentions.none()) - - with suppress(Forbidden): - await ctx.author.edit(nick=display_name) - else: - if len(text) >= 1500: - embed = Embed( - title=random.choice(NEGATIVE_REPLIES), - description="Submitted text was too large! Please submit something under 1500 characters.", - color=Colours.soft_red - ) - await ctx.send(embed=embed) - return - - string_list = text.split() - for index, name in enumerate(string_list): - name = name.lower() - if "cat" in name: - if random.randint(0, 5) == 5: - string_list[index] = name.replace("cat", f"**{random.choice(Cats.cats)}**") - else: - string_list[index] = name.replace("cat", random.choice(Cats.cats)) - for element in Cats.cats: - if element in name: - string_list[index] = name.replace(element, "cat") - - string_len = len(string_list) // 3 or len(string_list) - - for _ in range(random.randint(1, string_len)): - # insert cat at random index - if random.randint(0, 5) == 5: - string_list.insert(random.randint(0, len(string_list)), f"**{random.choice(Cats.cats)}**") - else: - string_list.insert(random.randint(0, len(string_list)), random.choice(Cats.cats)) - - text = helpers.suppress_links(" ".join(string_list)) - await ctx.send( - f">>> {text}", - allowed_mentions=AllowedMentions.none() - ) - - -def setup(bot: Bot) -> None: - """Loads the catify cog.""" - bot.add_cog(Catify()) diff --git a/bot/exts/evergreen/coinflip.py b/bot/exts/evergreen/coinflip.py deleted file mode 100644 index 804306bd..00000000 --- a/bot/exts/evergreen/coinflip.py +++ /dev/null @@ -1,53 +0,0 @@ -import random - -from discord.ext import commands - -from bot.bot import Bot -from bot.constants import Emojis - - -class CoinSide(commands.Converter): - """Class used to convert the `side` parameter of coinflip command.""" - - HEADS = ("h", "head", "heads") - TAILS = ("t", "tail", "tails") - - async def convert(self, ctx: commands.Context, side: str) -> str: - """Converts the provided `side` into the corresponding string.""" - side = side.lower() - if side in self.HEADS: - return "heads" - - if side in self.TAILS: - return "tails" - - raise commands.BadArgument(f"{side!r} is not a valid coin side.") - - -class CoinFlip(commands.Cog): - """Cog for the CoinFlip command.""" - - @commands.command(name="coinflip", aliases=("flip", "coin", "cf")) - async def coinflip_command(self, ctx: commands.Context, side: CoinSide = None) -> None: - """ - Flips a coin. - - If `side` is provided will state whether you guessed the side correctly. - """ - flipped_side = random.choice(["heads", "tails"]) - - message = f"{ctx.author.mention} flipped **{flipped_side}**. " - if not side: - await ctx.send(message) - return - - if side == flipped_side: - message += f"You guessed correctly! {Emojis.lemon_hyperpleased}" - else: - message += f"You guessed incorrectly. {Emojis.lemon_pensive}" - await ctx.send(message) - - -def setup(bot: Bot) -> None: - """Loads the coinflip cog.""" - bot.add_cog(CoinFlip()) diff --git a/bot/exts/evergreen/connect_four.py b/bot/exts/evergreen/connect_four.py deleted file mode 100644 index 647bb2b7..00000000 --- a/bot/exts/evergreen/connect_four.py +++ /dev/null @@ -1,452 +0,0 @@ -import asyncio -import random -from functools import partial -from typing import Optional, Union - -import discord -import emojis -from discord.ext import commands -from discord.ext.commands import guild_only - -from bot.bot import Bot -from bot.constants import Emojis - -NUMBERS = list(Emojis.number_emojis.values()) -CROSS_EMOJI = Emojis.incident_unactioned - -Coordinate = Optional[tuple[int, int]] -EMOJI_CHECK = Union[discord.Emoji, str] - - -class Game: - """A Connect 4 Game.""" - - def __init__( - self, - bot: Bot, - channel: discord.TextChannel, - player1: discord.Member, - player2: Optional[discord.Member], - tokens: list[str], - size: int = 7 - ): - self.bot = bot - self.channel = channel - self.player1 = player1 - self.player2 = player2 or AI(self.bot, game=self) - self.tokens = tokens - - self.grid = self.generate_board(size) - self.grid_size = size - - self.unicode_numbers = NUMBERS[:self.grid_size] - - self.message = None - - self.player_active = None - self.player_inactive = None - - @staticmethod - def generate_board(size: int) -> list[list[int]]: - """Generate the connect 4 board.""" - return [[0 for _ in range(size)] for _ in range(size)] - - async def print_grid(self) -> None: - """Formats and outputs the Connect Four grid to the channel.""" - title = ( - f"Connect 4: {self.player1.display_name}" - f" VS {self.bot.user.display_name if isinstance(self.player2, AI) else self.player2.display_name}" - ) - - rows = [" ".join(self.tokens[s] for s in row) for row in self.grid] - first_row = " ".join(x for x in NUMBERS[:self.grid_size]) - formatted_grid = "\n".join([first_row] + rows) - embed = discord.Embed(title=title, description=formatted_grid) - - if self.message: - await self.message.edit(embed=embed) - else: - self.message = await self.channel.send(content="Loading...") - for emoji in self.unicode_numbers: - await self.message.add_reaction(emoji) - await self.message.add_reaction(CROSS_EMOJI) - await self.message.edit(content=None, embed=embed) - - async def game_over(self, action: str, player1: discord.user, player2: discord.user) -> None: - """Announces to public chat.""" - if action == "win": - await self.channel.send(f"Game Over! {player1.mention} won against {player2.mention}") - elif action == "draw": - await self.channel.send(f"Game Over! {player1.mention} {player2.mention} It's A Draw :tada:") - elif action == "quit": - await self.channel.send(f"{self.player1.mention} surrendered. Game over!") - await self.print_grid() - - async def start_game(self) -> None: - """Begins the game.""" - self.player_active, self.player_inactive = self.player1, self.player2 - - while True: - await self.print_grid() - - if isinstance(self.player_active, AI): - coords = self.player_active.play() - if not coords: - await self.game_over( - "draw", - self.bot.user if isinstance(self.player_active, AI) else self.player_active, - self.bot.user if isinstance(self.player_inactive, AI) else self.player_inactive, - ) - else: - coords = await self.player_turn() - - if not coords: - return - - if self.check_win(coords, 1 if self.player_active == self.player1 else 2): - await self.game_over( - "win", - self.bot.user if isinstance(self.player_active, AI) else self.player_active, - self.bot.user if isinstance(self.player_inactive, AI) else self.player_inactive, - ) - return - - self.player_active, self.player_inactive = self.player_inactive, self.player_active - - def predicate(self, reaction: discord.Reaction, user: discord.Member) -> bool: - """The predicate to check for the player's reaction.""" - return ( - reaction.message.id == self.message.id - and user.id == self.player_active.id - and str(reaction.emoji) in (*self.unicode_numbers, CROSS_EMOJI) - ) - - async def player_turn(self) -> Coordinate: - """Initiate the player's turn.""" - message = await self.channel.send( - f"{self.player_active.mention}, it's your turn! React with the column you want to place your token in." - ) - player_num = 1 if self.player_active == self.player1 else 2 - while True: - try: - reaction, user = await self.bot.wait_for("reaction_add", check=self.predicate, timeout=30.0) - except asyncio.TimeoutError: - await self.channel.send(f"{self.player_active.mention}, you took too long. Game over!") - return - else: - await message.delete() - if str(reaction.emoji) == CROSS_EMOJI: - await self.game_over("quit", self.player_active, self.player_inactive) - return - - await self.message.remove_reaction(reaction, user) - - column_num = self.unicode_numbers.index(str(reaction.emoji)) - column = [row[column_num] for row in self.grid] - - for row_num, square in reversed(list(enumerate(column))): - if not square: - self.grid[row_num][column_num] = player_num - return row_num, column_num - message = await self.channel.send(f"Column {column_num + 1} is full. Try again") - - def check_win(self, coords: Coordinate, player_num: int) -> bool: - """Check that placing a counter here would cause the player to win.""" - vertical = [(-1, 0), (1, 0)] - horizontal = [(0, 1), (0, -1)] - forward_diag = [(-1, 1), (1, -1)] - backward_diag = [(-1, -1), (1, 1)] - axes = [vertical, horizontal, forward_diag, backward_diag] - - for axis in axes: - counters_in_a_row = 1 # The initial counter that is compared to - for (row_incr, column_incr) in axis: - row, column = coords - row += row_incr - column += column_incr - - while 0 <= row < self.grid_size and 0 <= column < self.grid_size: - if self.grid[row][column] == player_num: - counters_in_a_row += 1 - row += row_incr - column += column_incr - else: - break - if counters_in_a_row >= 4: - return True - return False - - -class AI: - """The Computer Player for Single-Player games.""" - - def __init__(self, bot: Bot, game: Game): - self.game = game - self.mention = bot.user.mention - - def get_possible_places(self) -> list[Coordinate]: - """Gets all the coordinates where the AI could possibly place a counter.""" - possible_coords = [] - for column_num in range(self.game.grid_size): - column = [row[column_num] for row in self.game.grid] - for row_num, square in reversed(list(enumerate(column))): - if not square: - possible_coords.append((row_num, column_num)) - break - return possible_coords - - def check_ai_win(self, coord_list: list[Coordinate]) -> Optional[Coordinate]: - """ - Check AI win. - - Check if placing a counter in any possible coordinate would cause the AI to win - with 10% chance of not winning and returning None - """ - if random.randint(1, 10) == 1: - return - for coords in coord_list: - if self.game.check_win(coords, 2): - return coords - - def check_player_win(self, coord_list: list[Coordinate]) -> Optional[Coordinate]: - """ - Check Player win. - - Check if placing a counter in possible coordinates would stop the player - from winning with 25% of not blocking them and returning None. - """ - if random.randint(1, 4) == 1: - return - for coords in coord_list: - if self.game.check_win(coords, 1): - return coords - - @staticmethod - def random_coords(coord_list: list[Coordinate]) -> Coordinate: - """Picks a random coordinate from the possible ones.""" - return random.choice(coord_list) - - def play(self) -> Union[Coordinate, bool]: - """ - Plays for the AI. - - Gets all possible coords, and determins the move: - 1. coords where it can win. - 2. coords where the player can win. - 3. Random coord - The first possible value is choosen. - """ - possible_coords = self.get_possible_places() - - if not possible_coords: - return False - - coords = ( - self.check_ai_win(possible_coords) - or self.check_player_win(possible_coords) - or self.random_coords(possible_coords) - ) - - row, column = coords - self.game.grid[row][column] = 2 - return coords - - -class ConnectFour(commands.Cog): - """Connect Four. The Classic Vertical Four-in-a-row Game!""" - - def __init__(self, bot: Bot): - self.bot = bot - self.games: list[Game] = [] - self.waiting: list[discord.Member] = [] - - self.tokens = [":white_circle:", ":blue_circle:", ":red_circle:"] - - self.max_board_size = 9 - self.min_board_size = 5 - - async def check_author(self, ctx: commands.Context, board_size: int) -> bool: - """Check if the requester is free and the board size is correct.""" - if self.already_playing(ctx.author): - await ctx.send("You're already playing a game!") - return False - - if ctx.author in self.waiting: - await ctx.send("You've already sent out a request for a player 2") - return False - - if not self.min_board_size <= board_size <= self.max_board_size: - await ctx.send( - f"{board_size} is not a valid board size. A valid board size is " - f"between `{self.min_board_size}` and `{self.max_board_size}`." - ) - return False - - return True - - def get_player( - self, - ctx: commands.Context, - announcement: discord.Message, - reaction: discord.Reaction, - user: discord.Member - ) -> bool: - """Predicate checking the criteria for the announcement message.""" - if self.already_playing(ctx.author): # If they've joined a game since requesting a player 2 - return True # Is dealt with later on - - if ( - user.id not in (ctx.me.id, ctx.author.id) - and str(reaction.emoji) == Emojis.hand_raised - and reaction.message.id == announcement.id - ): - if self.already_playing(user): - self.bot.loop.create_task(ctx.send(f"{user.mention} You're already playing a game!")) - self.bot.loop.create_task(announcement.remove_reaction(reaction, user)) - return False - - if user in self.waiting: - self.bot.loop.create_task(ctx.send( - f"{user.mention} Please cancel your game first before joining another one." - )) - self.bot.loop.create_task(announcement.remove_reaction(reaction, user)) - return False - - return True - - if ( - user.id == ctx.author.id - and str(reaction.emoji) == CROSS_EMOJI - and reaction.message.id == announcement.id - ): - return True - return False - - def already_playing(self, player: discord.Member) -> bool: - """Check if someone is already in a game.""" - return any(player in (game.player1, game.player2) for game in self.games) - - @staticmethod - def check_emojis( - e1: EMOJI_CHECK, e2: EMOJI_CHECK - ) -> tuple[bool, Optional[str]]: - """Validate the emojis, the user put.""" - if isinstance(e1, str) and emojis.count(e1) != 1: - return False, e1 - if isinstance(e2, str) and emojis.count(e2) != 1: - return False, e2 - return True, None - - async def _play_game( - self, - ctx: commands.Context, - user: Optional[discord.Member], - board_size: int, - emoji1: str, - emoji2: str - ) -> None: - """Helper for playing a game of connect four.""" - self.tokens = [":white_circle:", str(emoji1), str(emoji2)] - game = None # if game fails to intialize in try...except - - try: - game = Game(self.bot, ctx.channel, ctx.author, user, self.tokens, size=board_size) - self.games.append(game) - await game.start_game() - self.games.remove(game) - except Exception: - # End the game in the event of an unforeseen error so the players aren't stuck in a game - await ctx.send(f"{ctx.author.mention} {user.mention if user else ''} An error occurred. Game failed.") - if game in self.games: - self.games.remove(game) - raise - - @guild_only() - @commands.group( - invoke_without_command=True, - aliases=("4inarow", "connect4", "connectfour", "c4"), - case_insensitive=True - ) - async def connect_four( - self, - ctx: commands.Context, - board_size: int = 7, - emoji1: EMOJI_CHECK = "\U0001f535", - emoji2: EMOJI_CHECK = "\U0001f534" - ) -> None: - """ - Play the classic game of Connect Four with someone! - - Sets up a message waiting for someone else to react and play along. - The game will start once someone has reacted. - All inputs will be through reactions. - """ - check, emoji = self.check_emojis(emoji1, emoji2) - if not check: - raise commands.EmojiNotFound(emoji) - - check_author_result = await self.check_author(ctx, board_size) - if not check_author_result: - return - - announcement = await ctx.send( - "**Connect Four**: A new game is about to start!\n" - f"Press {Emojis.hand_raised} to play against {ctx.author.mention}!\n" - f"(Cancel the game with {CROSS_EMOJI}.)" - ) - self.waiting.append(ctx.author) - await announcement.add_reaction(Emojis.hand_raised) - await announcement.add_reaction(CROSS_EMOJI) - - try: - reaction, user = await self.bot.wait_for( - "reaction_add", - check=partial(self.get_player, ctx, announcement), - timeout=60.0 - ) - except asyncio.TimeoutError: - self.waiting.remove(ctx.author) - await announcement.delete() - await ctx.send( - f"{ctx.author.mention} Seems like there's no one here to play. " - f"Use `{ctx.prefix}{ctx.invoked_with} ai` to play against a computer." - ) - return - - if str(reaction.emoji) == CROSS_EMOJI: - self.waiting.remove(ctx.author) - await announcement.delete() - await ctx.send(f"{ctx.author.mention} Game cancelled.") - return - - await announcement.delete() - self.waiting.remove(ctx.author) - if self.already_playing(ctx.author): - return - - await self._play_game(ctx, user, board_size, str(emoji1), str(emoji2)) - - @guild_only() - @connect_four.command(aliases=("bot", "computer", "cpu")) - async def ai( - self, - ctx: commands.Context, - board_size: int = 7, - emoji1: EMOJI_CHECK = "\U0001f535", - emoji2: EMOJI_CHECK = "\U0001f534" - ) -> None: - """Play Connect Four against a computer player.""" - check, emoji = self.check_emojis(emoji1, emoji2) - if not check: - raise commands.EmojiNotFound(emoji) - - check_author_result = await self.check_author(ctx, board_size) - if not check_author_result: - return - - await self._play_game(ctx, None, board_size, str(emoji1), str(emoji2)) - - -def setup(bot: Bot) -> None: - """Load ConnectFour Cog.""" - bot.add_cog(ConnectFour(bot)) diff --git a/bot/exts/evergreen/duck_game.py b/bot/exts/evergreen/duck_game.py deleted file mode 100644 index d592f3df..00000000 --- a/bot/exts/evergreen/duck_game.py +++ /dev/null @@ -1,356 +0,0 @@ -import asyncio -import random -import re -from collections import defaultdict -from io import BytesIO -from itertools import product -from pathlib import Path -from urllib.parse import urlparse - -import discord -from PIL import Image, ImageDraw, ImageFont -from discord.ext import commands - -from bot.bot import Bot -from bot.constants import Colours, MODERATION_ROLES -from bot.utils.decorators import with_role - - -DECK = list(product(*[(0, 1, 2)]*4)) - -GAME_DURATION = 180 - -# Scoring -CORRECT_SOLN = 1 -INCORRECT_SOLN = -1 -CORRECT_GOOSE = 2 -INCORRECT_GOOSE = -1 - -# Distribution of minimum acceptable solutions at board generation. -# This is for gameplay reasons, to shift the number of solutions per board up, -# while still making the end of the game unpredictable. -# Note: this is *not* the same as the distribution of number of solutions. - -SOLN_DISTR = 0, 0.05, 0.05, 0.1, 0.15, 0.25, 0.2, 0.15, .05 - -IMAGE_PATH = Path("bot", "resources", "evergreen", "all_cards.png") -FONT_PATH = Path("bot", "resources", "evergreen", "LuckiestGuy-Regular.ttf") -HELP_IMAGE_PATH = Path("bot", "resources", "evergreen", "ducks_help_ex.png") - -ALL_CARDS = Image.open(IMAGE_PATH) -LABEL_FONT = ImageFont.truetype(str(FONT_PATH), size=16) -CARD_WIDTH = 155 -CARD_HEIGHT = 97 - -EMOJI_WRONG = "\u274C" - -ANSWER_REGEX = re.compile(r'^\D*(\d+)\D+(\d+)\D+(\d+)\D*$') - -HELP_TEXT = """ -**Each card has 4 features** -Color, Number, Hat, and Accessory - -**A valid flight** -3 cards where each feature is either all the same or all different - -**Call "GOOSE"** -if you think there are no more flights - -**+1** for each valid flight -**+2** for a correct "GOOSE" call -**-1** for any wrong answer - -The first flight below is invalid: the first card has swords while the other two have no accessory.\ - It would be valid if the first card was empty-handed, or one of the other two had paintbrushes. - -The second flight is valid because there are no 2:1 splits; each feature is either all the same or all different. -""" - - -def assemble_board_image(board: list[tuple[int]], rows: int, columns: int) -> Image: - """Cut and paste images representing the given cards into an image representing the board.""" - new_im = Image.new("RGBA", (CARD_WIDTH*columns, CARD_HEIGHT*rows)) - draw = ImageDraw.Draw(new_im) - for idx, card in enumerate(board): - card_image = get_card_image(card) - row, col = divmod(idx, columns) - top, left = row * CARD_HEIGHT, col * CARD_WIDTH - new_im.paste(card_image, (left, top)) - draw.text( - xy=(left+5, top+5), # magic numbers are buffers for the card labels - text=str(idx), - fill=(0, 0, 0), - font=LABEL_FONT, - ) - return new_im - - -def get_card_image(card: tuple[int]) -> Image: - """Slice the image containing all the cards to get just this card.""" - # The master card image file should have 9x9 cards, - # arranged such that their features can be interpreted as ordered trinary. - row, col = divmod(as_trinary(card), 9) - x1 = col * CARD_WIDTH - x2 = x1 + CARD_WIDTH - y1 = row * CARD_HEIGHT - y2 = y1 + CARD_HEIGHT - return ALL_CARDS.crop((x1, y1, x2, y2)) - - -def as_trinary(card: tuple[int]) -> int: - """Find the card's unique index by interpreting its features as trinary.""" - return int(''.join(str(x) for x in card), base=3) - - -class DuckGame: - """A class for a single game.""" - - def __init__( - self, - rows: int = 4, - columns: int = 3, - minimum_solutions: int = 1, - ): - """ - Take samples from the deck to generate a board. - - Args: - rows (int, optional): Rows in the game board. Defaults to 4. - columns (int, optional): Columns in the game board. Defaults to 3. - minimum_solutions (int, optional): Minimum acceptable number of solutions in the board. Defaults to 1. - """ - self.rows = rows - self.columns = columns - size = rows * columns - - self._solutions = None - self.claimed_answers = {} - self.scores = defaultdict(int) - self.editing_embed = asyncio.Lock() - - self.board = random.sample(DECK, size) - while len(self.solutions) < minimum_solutions: - self.board = random.sample(DECK, size) - - @property - def board(self) -> list[tuple[int]]: - """Accesses board property.""" - return self._board - - @board.setter - def board(self, val: list[tuple[int]]) -> None: - """Erases calculated solutions if the board changes.""" - self._solutions = None - self._board = val - - @property - def solutions(self) -> None: - """Calculate valid solutions and cache to avoid redoing work.""" - if self._solutions is None: - self._solutions = set() - for idx_a, card_a in enumerate(self.board): - for idx_b, card_b in enumerate(self.board[idx_a+1:], start=idx_a+1): - # Two points determine a line, and there are exactly 3 points per line in {0,1,2}^4. - # The completion of a line will only be a duplicate point if the other two points are the same, - # which is prevented by the triangle iteration. - completion = tuple( - feat_a if feat_a == feat_b else 3-feat_a-feat_b - for feat_a, feat_b in zip(card_a, card_b) - ) - try: - idx_c = self.board.index(completion) - except ValueError: - continue - - # Indices within the solution are sorted to detect duplicate solutions modulo order. - solution = tuple(sorted((idx_a, idx_b, idx_c))) - self._solutions.add(solution) - - return self._solutions - - -class DuckGamesDirector(commands.Cog): - """A cog for running Duck Duck Duck Goose games.""" - - def __init__(self, bot: Bot): - self.bot = bot - self.current_games = {} - - @commands.group( - name='duckduckduckgoose', - aliases=['dddg', 'ddg', 'duckduckgoose', 'duckgoose'], - invoke_without_command=True - ) - @commands.cooldown(rate=1, per=2, type=commands.BucketType.channel) - async def start_game(self, ctx: commands.Context) -> None: - """Generate a board, send the game embed, and end the game after a time limit.""" - if ctx.channel.id in self.current_games: - await ctx.send("There's already a game running!") - return - - minimum_solutions, = random.choices(range(len(SOLN_DISTR)), weights=SOLN_DISTR) - game = DuckGame(minimum_solutions=minimum_solutions) - game.running = True - self.current_games[ctx.channel.id] = game - - game.embed_msg = await self.send_board_embed(ctx, game) - await asyncio.sleep(GAME_DURATION) - - # Checking for the channel ID in the currently running games is not sufficient. - # The game could have been ended by a player, and a new game already started in the same channel. - if game.running: - try: - del self.current_games[ctx.channel.id] - await self.end_game(ctx.channel, game, end_message="Time's up!") - except KeyError: - pass - - @commands.Cog.listener() - async def on_message(self, msg: discord.Message) -> None: - """Listen for messages and process them as answers if appropriate.""" - if msg.author.bot: - return - - channel = msg.channel - if channel.id not in self.current_games: - return - - game = self.current_games[channel.id] - if msg.content.strip().lower() == 'goose': - # If all of the solutions have been claimed, i.e. the "goose" call is correct. - if len(game.solutions) == len(game.claimed_answers): - try: - del self.current_games[channel.id] - game.scores[msg.author] += CORRECT_GOOSE - await self.end_game(channel, game, end_message=f"{msg.author.display_name} GOOSED!") - except KeyError: - pass - else: - await msg.add_reaction(EMOJI_WRONG) - game.scores[msg.author] += INCORRECT_GOOSE - return - - # Valid answers contain 3 numbers. - if not (match := re.match(ANSWER_REGEX, msg.content)): - return - answer = tuple(sorted(int(m) for m in match.groups())) - - # Be forgiving for answers that use indices not on the board. - if not all(0 <= n < len(game.board) for n in answer): - return - - # Also be forgiving for answers that have already been claimed (and avoid penalizing for racing conditions). - if answer in game.claimed_answers: - return - - if answer in game.solutions: - game.claimed_answers[answer] = msg.author - game.scores[msg.author] += CORRECT_SOLN - await self.display_claimed_answer(game, msg.author, answer) - else: - await msg.add_reaction(EMOJI_WRONG) - game.scores[msg.author] += INCORRECT_SOLN - - async def send_board_embed(self, ctx: commands.Context, game: DuckGame) -> discord.Message: - """Create and send the initial game embed. This will be edited as the game goes on.""" - image = assemble_board_image(game.board, game.rows, game.columns) - with BytesIO() as image_stream: - image.save(image_stream, format="png") - image_stream.seek(0) - file = discord.File(fp=image_stream, filename="board.png") - embed = discord.Embed( - title="Duck Duck Duck Goose!", - color=Colours.bright_green, - footer="" - ) - embed.set_image(url="attachment://board.png") - return await ctx.send(embed=embed, file=file) - - async def display_claimed_answer(self, game: DuckGame, author: discord.Member, answer: tuple[int]) -> None: - """Add a claimed answer to the game embed.""" - async with game.editing_embed: - game_embed, = game.embed_msg.embeds - old_footer = game_embed.footer.text - if old_footer == discord.Embed.Empty: - old_footer = "" - game_embed.set_footer(text=f"{old_footer}\n{str(answer):12s} - {author.display_name}") - await self.edit_embed_with_image(game.embed_msg, game_embed) - - async def end_game(self, channel: discord.TextChannel, game: DuckGame, end_message: str) -> None: - """Edit the game embed to reflect the end of the game and mark the game as not running.""" - game.running = False - - scoreboard_embed = discord.Embed( - title=end_message, - color=discord.Color.dark_purple(), - ) - scores = sorted( - game.scores.items(), - key=lambda item: item[1], - reverse=True, - ) - scoreboard = "Final scores:\n\n" - scoreboard += "\n".join(f"{member.display_name}: {score}" for member, score in scores) - scoreboard_embed.description = scoreboard - await channel.send(embed=scoreboard_embed) - - missed = [ans for ans in game.solutions if ans not in game.claimed_answers] - if missed: - missed_text = "Flights everyone missed:\n" + "\n".join(f"{ans}" for ans in missed) - else: - missed_text = "All the flights were found!" - - game_embed, = game.embed_msg.embeds - old_footer = game_embed.footer.text - if old_footer == discord.Embed.Empty: - old_footer = "" - embed_as_dict = game_embed.to_dict() # Cannot set embed color after initialization - embed_as_dict["color"] = discord.Color.red().value - game_embed = discord.Embed.from_dict(embed_as_dict) - game_embed.set_footer( - text=f"{old_footer.rstrip()}\n\n{missed_text}" - ) - await self.edit_embed_with_image(game.embed_msg, game_embed) - - @start_game.command(name="help") - async def show_rules(self, ctx: commands.Context) -> None: - """Explain the rules of the game.""" - await self.send_help_embed(ctx) - - @start_game.command(name="stop") - @with_role(*MODERATION_ROLES) - async def stop_game(self, ctx: commands.Context) -> None: - """Stop a currently running game. Only available to mods.""" - try: - game = self.current_games.pop(ctx.channel.id) - except KeyError: - await ctx.send("No game currently running in this channel") - return - await self.end_game(ctx.channel, game, end_message="Game canceled.") - - @staticmethod - async def send_help_embed(ctx: commands.Context) -> discord.Message: - """Send rules embed.""" - embed = discord.Embed( - title="Compete against other players to find valid flights!", - color=discord.Color.dark_purple(), - ) - embed.description = HELP_TEXT - file = discord.File(HELP_IMAGE_PATH, filename="help.png") - embed.set_image(url="attachment://help.png") - embed.set_footer( - text="Tip: using Discord's compact message display mode can help keep the board on the screen" - ) - return await ctx.send(file=file, embed=embed) - - @staticmethod - async def edit_embed_with_image(msg: discord.Message, embed: discord.Embed) -> None: - """Edit an embed without the attached image going wonky.""" - attach_name = urlparse(embed.image.url).path.split("/")[-1] - embed.set_image(url=f"attachment://{attach_name}") - await msg.edit(embed=embed) - - -def setup(bot: Bot) -> None: - """Load the DuckGamesDirector cog.""" - bot.add_cog(DuckGamesDirector(bot)) diff --git a/bot/exts/evergreen/fun.py b/bot/exts/evergreen/fun.py deleted file mode 100644 index 4bbfe859..00000000 --- a/bot/exts/evergreen/fun.py +++ /dev/null @@ -1,250 +0,0 @@ -import functools -import json -import logging -import random -from collections.abc import Iterable -from pathlib import Path -from typing import Callable, Optional, Union - -from discord import Embed, Message -from discord.ext import commands -from discord.ext.commands import BadArgument, Cog, Context, MessageConverter, clean_content - -from bot import utils -from bot.bot import Bot -from bot.constants import Client, Colours, Emojis -from bot.utils import helpers - -log = logging.getLogger(__name__) - -UWU_WORDS = { - "fi": "fwi", - "l": "w", - "r": "w", - "some": "sum", - "th": "d", - "thing": "fing", - "tho": "fo", - "you're": "yuw'we", - "your": "yur", - "you": "yuw", -} - - -def caesar_cipher(text: str, offset: int) -> Iterable[str]: - """ - Implements a lazy Caesar Cipher algorithm. - - Encrypts a `text` given a specific integer `offset`. The sign - of the `offset` dictates the direction in which it shifts to, - with a negative value shifting to the left, and a positive - value shifting to the right. - """ - for char in text: - if not char.isascii() or not char.isalpha() or char.isspace(): - yield char - continue - - case_start = 65 if char.isupper() else 97 - true_offset = (ord(char) - case_start + offset) % 26 - - yield chr(case_start + true_offset) - - -class Fun(Cog): - """A collection of general commands for fun.""" - - def __init__(self, bot: Bot): - self.bot = bot - - self._caesar_cipher_embed = json.loads(Path("bot/resources/evergreen/caesar_info.json").read_text("UTF-8")) - - @staticmethod - def _get_random_die() -> str: - """Generate a random die emoji, ready to be sent on Discord.""" - die_name = f"dice_{random.randint(1, 6)}" - return getattr(Emojis, die_name) - - @commands.command() - async def roll(self, ctx: Context, num_rolls: int = 1) -> None: - """Outputs a number of random dice emotes (up to 6).""" - if 1 <= num_rolls <= 6: - dice = " ".join(self._get_random_die() for _ in range(num_rolls)) - await ctx.send(dice) - else: - raise BadArgument(f"`{Client.prefix}roll` only supports between 1 and 6 rolls.") - - @commands.command(name="uwu", aliases=("uwuwize", "uwuify",)) - async def uwu_command(self, ctx: Context, *, text: clean_content(fix_channel_mentions=True)) -> None: - """Converts a given `text` into it's uwu equivalent.""" - conversion_func = functools.partial( - utils.replace_many, replacements=UWU_WORDS, ignore_case=True, match_case=True - ) - text, embed = await Fun._get_text_and_embed(ctx, text) - # Convert embed if it exists - if embed is not None: - embed = Fun._convert_embed(conversion_func, embed) - converted_text = conversion_func(text) - converted_text = helpers.suppress_links(converted_text) - # Don't put >>> if only embed present - if converted_text: - converted_text = f">>> {converted_text.lstrip('> ')}" - await ctx.send(content=converted_text, embed=embed) - - @commands.command(name="randomcase", aliases=("rcase", "randomcaps", "rcaps",)) - async def randomcase_command(self, ctx: Context, *, text: clean_content(fix_channel_mentions=True)) -> None: - """Randomly converts the casing of a given `text`.""" - def conversion_func(text: str) -> str: - """Randomly converts the casing of a given string.""" - return "".join( - char.upper() if round(random.random()) else char.lower() for char in text - ) - text, embed = await Fun._get_text_and_embed(ctx, text) - # Convert embed if it exists - if embed is not None: - embed = Fun._convert_embed(conversion_func, embed) - converted_text = conversion_func(text) - converted_text = helpers.suppress_links(converted_text) - # Don't put >>> if only embed present - if converted_text: - converted_text = f">>> {converted_text.lstrip('> ')}" - await ctx.send(content=converted_text, embed=embed) - - @commands.group(name="caesarcipher", aliases=("caesar", "cc",)) - async def caesarcipher_group(self, ctx: Context) -> None: - """ - Translates a message using the Caesar Cipher. - - See `decrypt`, `encrypt`, and `info` subcommands. - """ - if ctx.invoked_subcommand is None: - await ctx.invoke(self.bot.get_command("help"), "caesarcipher") - - @caesarcipher_group.command(name="info") - async def caesarcipher_info(self, ctx: Context) -> None: - """Information about the Caesar Cipher.""" - embed = Embed.from_dict(self._caesar_cipher_embed) - embed.colour = Colours.dark_green - - await ctx.send(embed=embed) - - @staticmethod - async def _caesar_cipher(ctx: Context, offset: int, msg: str, left_shift: bool = False) -> None: - """ - Given a positive integer `offset`, translates and sends the given `msg`. - - Performs a right shift by default unless `left_shift` is specified as `True`. - - Also accepts a valid Discord Message ID or link. - """ - if offset < 0: - await ctx.send(":no_entry: Cannot use a negative offset.") - return - - if left_shift: - offset = -offset - - def conversion_func(text: str) -> str: - """Encrypts the given string using the Caesar Cipher.""" - return "".join(caesar_cipher(text, offset)) - - text, embed = await Fun._get_text_and_embed(ctx, msg) - - if embed is not None: - embed = Fun._convert_embed(conversion_func, embed) - - converted_text = conversion_func(text) - - if converted_text: - converted_text = f">>> {converted_text.lstrip('> ')}" - - await ctx.send(content=converted_text, embed=embed) - - @caesarcipher_group.command(name="encrypt", aliases=("rightshift", "rshift", "enc",)) - async def caesarcipher_encrypt(self, ctx: Context, offset: int, *, msg: str) -> None: - """ - Given a positive integer `offset`, encrypt the given `msg`. - - Performs a right shift of the letters in the message. - - Also accepts a valid Discord Message ID or link. - """ - await self._caesar_cipher(ctx, offset, msg, left_shift=False) - - @caesarcipher_group.command(name="decrypt", aliases=("leftshift", "lshift", "dec",)) - async def caesarcipher_decrypt(self, ctx: Context, offset: int, *, msg: str) -> None: - """ - Given a positive integer `offset`, decrypt the given `msg`. - - Performs a left shift of the letters in the message. - - Also accepts a valid Discord Message ID or link. - """ - await self._caesar_cipher(ctx, offset, msg, left_shift=True) - - @staticmethod - async def _get_text_and_embed(ctx: Context, text: str) -> tuple[str, Optional[Embed]]: - """ - Attempts to extract the text and embed from a possible link to a discord Message. - - Does not retrieve the text and embed from the Message if it is in a channel the user does - not have read permissions in. - - Returns a tuple of: - str: If `text` is a valid discord Message, the contents of the message, else `text`. - Optional[Embed]: The embed if found in the valid Message, else None - """ - embed = None - - msg = await Fun._get_discord_message(ctx, text) - # Ensure the user has read permissions for the channel the message is in - if isinstance(msg, Message): - permissions = msg.channel.permissions_for(ctx.author) - if permissions.read_messages: - text = msg.clean_content - # Take first embed because we can't send multiple embeds - if msg.embeds: - embed = msg.embeds[0] - - return (text, embed) - - @staticmethod - async def _get_discord_message(ctx: Context, text: str) -> Union[Message, str]: - """ - Attempts to convert a given `text` to a discord Message object and return it. - - Conversion will succeed if given a discord Message ID or link. - Returns `text` if the conversion fails. - """ - try: - text = await MessageConverter().convert(ctx, text) - except commands.BadArgument: - log.debug(f"Input '{text:.20}...' is not a valid Discord Message") - return text - - @staticmethod - def _convert_embed(func: Callable[[str, ], str], embed: Embed) -> Embed: - """ - Converts the text in an embed using a given conversion function, then return the embed. - - Only modifies the following fields: title, description, footer, fields - """ - embed_dict = embed.to_dict() - - embed_dict["title"] = func(embed_dict.get("title", "")) - embed_dict["description"] = func(embed_dict.get("description", "")) - - if "footer" in embed_dict: - embed_dict["footer"]["text"] = func(embed_dict["footer"].get("text", "")) - - if "fields" in embed_dict: - for field in embed_dict["fields"]: - field["name"] = func(field.get("name", "")) - field["value"] = func(field.get("value", "")) - - return Embed.from_dict(embed_dict) - - -def setup(bot: Bot) -> None: - """Load the Fun cog.""" - bot.add_cog(Fun(bot)) diff --git a/bot/exts/evergreen/game.py b/bot/exts/evergreen/game.py deleted file mode 100644 index f9c150e6..00000000 --- a/bot/exts/evergreen/game.py +++ /dev/null @@ -1,485 +0,0 @@ -import difflib -import logging -import random -import re -from asyncio import sleep -from datetime import datetime as dt, timedelta -from enum import IntEnum -from typing import Any, Optional - -from aiohttp import ClientSession -from discord import Embed -from discord.ext import tasks -from discord.ext.commands import Cog, Context, group - -from bot.bot import Bot -from bot.constants import STAFF_ROLES, Tokens -from bot.utils.decorators import with_role -from bot.utils.extensions import invoke_help_command -from bot.utils.pagination import ImagePaginator, LinePaginator - -# Base URL of IGDB API -BASE_URL = "https://api.igdb.com/v4" - -CLIENT_ID = Tokens.igdb_client_id -CLIENT_SECRET = Tokens.igdb_client_secret - -# The number of seconds before expiry that we attempt to re-fetch a new access token -ACCESS_TOKEN_RENEWAL_WINDOW = 60*60*24*2 - -# URL to request API access token -OAUTH_URL = "https://id.twitch.tv/oauth2/token" - -OAUTH_PARAMS = { - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, - "grant_type": "client_credentials" -} - -BASE_HEADERS = { - "Client-ID": CLIENT_ID, - "Accept": "application/json" -} - -logger = logging.getLogger(__name__) - -REGEX_NON_ALPHABET = re.compile(r"[^a-z0-9]", re.IGNORECASE) - -# --------- -# TEMPLATES -# --------- - -# Body templates -# Request body template for get_games_list -GAMES_LIST_BODY = ( - "fields cover.image_id, first_release_date, total_rating, name, storyline, url, platforms.name, status," - "involved_companies.company.name, summary, age_ratings.category, age_ratings.rating, total_rating_count;" - "{sort} {limit} {offset} {genre} {additional}" -) - -# Request body template for get_companies_list -COMPANIES_LIST_BODY = ( - "fields name, url, start_date, logo.image_id, developed.name, published.name, description;" - "offset {offset}; limit {limit};" -) - -# Request body template for games search -SEARCH_BODY = 'fields name, url, storyline, total_rating, total_rating_count; limit 50; search "{term}";' - -# Pages templates -# Game embed layout -GAME_PAGE = ( - "**[{name}]({url})**\n" - "{description}" - "**Release Date:** {release_date}\n" - "**Rating:** {rating}/100 :star: (based on {rating_count} ratings)\n" - "**Platforms:** {platforms}\n" - "**Status:** {status}\n" - "**Age Ratings:** {age_ratings}\n" - "**Made by:** {made_by}\n\n" - "{storyline}" -) - -# .games company command page layout -COMPANY_PAGE = ( - "**[{name}]({url})**\n" - "{description}" - "**Founded:** {founded}\n" - "**Developed:** {developed}\n" - "**Published:** {published}" -) - -# For .games search command line layout -GAME_SEARCH_LINE = ( - "**[{name}]({url})**\n" - "{rating}/100 :star: (based on {rating_count} ratings)\n" -) - -# URL templates -COVER_URL = "https://images.igdb.com/igdb/image/upload/t_cover_big/{image_id}.jpg" -LOGO_URL = "https://images.igdb.com/igdb/image/upload/t_logo_med/{image_id}.png" - -# Create aliases for complex genre names -ALIASES = { - "Role-playing (rpg)": ["Role playing", "Rpg"], - "Turn-based strategy (tbs)": ["Turn based strategy", "Tbs"], - "Real time strategy (rts)": ["Real time strategy", "Rts"], - "Hack and slash/beat 'em up": ["Hack and slash"] -} - - -class GameStatus(IntEnum): - """Game statuses in IGDB API.""" - - Released = 0 - Alpha = 2 - Beta = 3 - Early = 4 - Offline = 5 - Cancelled = 6 - Rumored = 7 - - -class AgeRatingCategories(IntEnum): - """IGDB API Age Rating categories IDs.""" - - ESRB = 1 - PEGI = 2 - - -class AgeRatings(IntEnum): - """PEGI/ESRB ratings IGDB API IDs.""" - - Three = 1 - Seven = 2 - Twelve = 3 - Sixteen = 4 - Eighteen = 5 - RP = 6 - EC = 7 - E = 8 - E10 = 9 - T = 10 - M = 11 - AO = 12 - - -class Games(Cog): - """Games Cog contains commands that collect data from IGDB.""" - - def __init__(self, bot: Bot): - self.bot = bot - self.http_session: ClientSession = bot.http_session - - self.genres: dict[str, int] = {} - self.headers = BASE_HEADERS - - self.bot.loop.create_task(self.renew_access_token()) - - async def renew_access_token(self) -> None: - """Refeshes V4 access token a number of seconds before expiry. See `ACCESS_TOKEN_RENEWAL_WINDOW`.""" - while True: - async with self.http_session.post(OAUTH_URL, params=OAUTH_PARAMS) as resp: - result = await resp.json() - if resp.status != 200: - # If there is a valid access token continue to use that, - # otherwise unload cog. - if "Authorization" in self.headers: - time_delta = timedelta(seconds=ACCESS_TOKEN_RENEWAL_WINDOW) - logger.error( - "Failed to renew IGDB access token. " - f"Current token will last for {time_delta} " - f"OAuth response message: {result['message']}" - ) - else: - logger.warning( - "Invalid OAuth credentials. Unloading Games cog. " - f"OAuth response message: {result['message']}" - ) - self.bot.remove_cog("Games") - - return - - self.headers["Authorization"] = f"Bearer {result['access_token']}" - - # Attempt to renew before the token expires - next_renewal = result["expires_in"] - ACCESS_TOKEN_RENEWAL_WINDOW - - time_delta = timedelta(seconds=next_renewal) - logger.info(f"Successfully renewed access token. Refreshing again in {time_delta}") - - # This will be true the first time this loop runs. - # Since we now have an access token, its safe to start this task. - if self.genres == {}: - self.refresh_genres_task.start() - await sleep(next_renewal) - - @tasks.loop(hours=24.0) - async def refresh_genres_task(self) -> None: - """Refresh genres in every hour.""" - try: - await self._get_genres() - except Exception as e: - logger.warning(f"There was error while refreshing genres: {e}") - return - logger.info("Successfully refreshed genres.") - - def cog_unload(self) -> None: - """Cancel genres refreshing start when unloading Cog.""" - self.refresh_genres_task.cancel() - logger.info("Successfully stopped Genres Refreshing task.") - - async def _get_genres(self) -> None: - """Create genres variable for games command.""" - body = "fields name; limit 100;" - async with self.http_session.post(f"{BASE_URL}/genres", data=body, headers=self.headers) as resp: - result = await resp.json() - genres = {genre["name"].capitalize(): genre["id"] for genre in result} - - # Replace complex names with names from ALIASES - for genre_name, genre in genres.items(): - if genre_name in ALIASES: - for alias in ALIASES[genre_name]: - self.genres[alias] = genre - else: - self.genres[genre_name] = genre - - @group(name="games", aliases=("game",), invoke_without_command=True) - async def games(self, ctx: Context, amount: Optional[int] = 5, *, genre: Optional[str]) -> None: - """ - Get random game(s) by genre from IGDB. Use .games genres command to get all available genres. - - Also support amount parameter, what max is 25 and min 1, default 5. Supported formats: - - .games - - .games - """ - # When user didn't specified genre, send help message - if genre is None: - await invoke_help_command(ctx) - return - - # Capitalize genre for check - genre = "".join(genre).capitalize() - - # Check for amounts, max is 25 and min 1 - if not 1 <= amount <= 25: - await ctx.send("Your provided amount is out of range. Our minimum is 1 and maximum 25.") - return - - # Get games listing, if genre don't exist, show error message with possibilities. - # Offset must be random, due otherwise we will get always same result (offset show in which position should - # API start returning result) - try: - games = await self.get_games_list(amount, self.genres[genre], offset=random.randint(0, 150)) - except KeyError: - possibilities = await self.get_best_results(genre) - # If there is more than 1 possibilities, show these. - # If there is only 1 possibility, use it as genre. - # Otherwise send message about invalid genre. - if len(possibilities) > 1: - display_possibilities = "`, `".join(p[1] for p in possibilities) - await ctx.send( - f"Invalid genre `{genre}`. " - f"{f'Maybe you meant `{display_possibilities}`?' if display_possibilities else ''}" - ) - return - elif len(possibilities) == 1: - games = await self.get_games_list( - amount, self.genres[possibilities[0][1]], offset=random.randint(0, 150) - ) - genre = possibilities[0][1] - else: - await ctx.send(f"Invalid genre `{genre}`.") - return - - # Create pages and paginate - pages = [await self.create_page(game) for game in games] - - await ImagePaginator.paginate(pages, ctx, Embed(title=f"Random {genre.title()} Games")) - - @games.command(name="top", aliases=("t",)) - async def top(self, ctx: Context, amount: int = 10) -> None: - """ - Get current Top games in IGDB. - - Support amount parameter. Max is 25, min is 1. - """ - if not 1 <= amount <= 25: - await ctx.send("Your provided amount is out of range. Our minimum is 1 and maximum 25.") - return - - games = await self.get_games_list(amount, sort="total_rating desc", - additional_body="where total_rating >= 90; sort total_rating_count desc;") - - pages = [await self.create_page(game) for game in games] - await ImagePaginator.paginate(pages, ctx, Embed(title=f"Top {amount} Games")) - - @games.command(name="genres", aliases=("genre", "g")) - async def genres(self, ctx: Context) -> None: - """Get all available genres.""" - await ctx.send(f"Currently available genres: {', '.join(f'`{genre}`' for genre in self.genres)}") - - @games.command(name="search", aliases=("s",)) - async def search(self, ctx: Context, *, search_term: str) -> None: - """Find games by name.""" - lines = await self.search_games(search_term) - - await LinePaginator.paginate(lines, ctx, Embed(title=f"Game Search Results: {search_term}"), empty=False) - - @games.command(name="company", aliases=("companies",)) - async def company(self, ctx: Context, amount: int = 5) -> None: - """ - Get random Game Companies companies from IGDB API. - - Support amount parameter. Max is 25, min is 1. - """ - if not 1 <= amount <= 25: - await ctx.send("Your provided amount is out of range. Our minimum is 1 and maximum 25.") - return - - # Get companies listing. Provide limit for limiting how much companies will be returned. Get random offset to - # get (almost) every time different companies (offset show in which position should API start returning result) - companies = await self.get_companies_list(limit=amount, offset=random.randint(0, 150)) - pages = [await self.create_company_page(co) for co in companies] - - await ImagePaginator.paginate(pages, ctx, Embed(title="Random Game Companies")) - - @with_role(*STAFF_ROLES) - @games.command(name="refresh", aliases=("r",)) - async def refresh_genres_command(self, ctx: Context) -> None: - """Refresh .games command genres.""" - try: - await self._get_genres() - except Exception as e: - await ctx.send(f"There was error while refreshing genres: `{e}`") - return - await ctx.send("Successfully refreshed genres.") - - async def get_games_list( - self, - amount: int, - genre: Optional[str] = None, - sort: Optional[str] = None, - additional_body: str = "", - offset: int = 0 - ) -> list[dict[str, Any]]: - """ - Get list of games from IGDB API by parameters that is provided. - - Amount param show how much games this get, genre is genre ID and at least one genre in game must this when - provided. Sort is sorting by specific field and direction, ex. total_rating desc/asc (total_rating is field, - desc/asc is direction). Additional_body is field where you can pass extra search parameters. Offset show start - position in API. - """ - # Create body of IGDB API request, define fields, sorting, offset, limit and genre - params = { - "sort": f"sort {sort};" if sort else "", - "limit": f"limit {amount};", - "offset": f"offset {offset};" if offset else "", - "genre": f"where genres = ({genre});" if genre else "", - "additional": additional_body - } - body = GAMES_LIST_BODY.format(**params) - - # Do request to IGDB API, create headers, URL, define body, return result - async with self.http_session.post(url=f"{BASE_URL}/games", data=body, headers=self.headers) as resp: - return await resp.json() - - async def create_page(self, data: dict[str, Any]) -> tuple[str, str]: - """Create content of Game Page.""" - # Create cover image URL from template - url = COVER_URL.format(**{"image_id": data["cover"]["image_id"] if "cover" in data else ""}) - - # Get release date separately with checking - release_date = dt.utcfromtimestamp(data["first_release_date"]).date() if "first_release_date" in data else "?" - - # Create Age Ratings value - rating = ", ".join( - f"{AgeRatingCategories(age['category']).name} {AgeRatings(age['rating']).name}" - for age in data["age_ratings"] - ) if "age_ratings" in data else "?" - - companies = [c["company"]["name"] for c in data["involved_companies"]] if "involved_companies" in data else "?" - - # Create formatting for template page - formatting = { - "name": data["name"], - "url": data["url"], - "description": f"{data['summary']}\n\n" if "summary" in data else "\n", - "release_date": release_date, - "rating": round(data["total_rating"] if "total_rating" in data else 0, 2), - "rating_count": data["total_rating_count"] if "total_rating_count" in data else "?", - "platforms": ", ".join(platform["name"] for platform in data["platforms"]) if "platforms" in data else "?", - "status": GameStatus(data["status"]).name if "status" in data else "?", - "age_ratings": rating, - "made_by": ", ".join(companies), - "storyline": data["storyline"] if "storyline" in data else "" - } - page = GAME_PAGE.format(**formatting) - - return page, url - - async def search_games(self, search_term: str) -> list[str]: - """Search game from IGDB API by string, return listing of pages.""" - lines = [] - - # Define request body of IGDB API request and do request - body = SEARCH_BODY.format(**{"term": search_term}) - - async with self.http_session.post(url=f"{BASE_URL}/games", data=body, headers=self.headers) as resp: - data = await resp.json() - - # Loop over games, format them to good format, make line and append this to total lines - for game in data: - formatting = { - "name": game["name"], - "url": game["url"], - "rating": round(game["total_rating"] if "total_rating" in game else 0, 2), - "rating_count": game["total_rating_count"] if "total_rating" in game else "?" - } - line = GAME_SEARCH_LINE.format(**formatting) - lines.append(line) - - return lines - - async def get_companies_list(self, limit: int, offset: int = 0) -> list[dict[str, Any]]: - """ - Get random Game Companies from IGDB API. - - Limit is parameter, that show how much movies this should return, offset show in which position should API start - returning results. - """ - # Create request body from template - body = COMPANIES_LIST_BODY.format(**{ - "limit": limit, - "offset": offset - }) - - async with self.http_session.post(url=f"{BASE_URL}/companies", data=body, headers=self.headers) as resp: - return await resp.json() - - async def create_company_page(self, data: dict[str, Any]) -> tuple[str, str]: - """Create good formatted Game Company page.""" - # Generate URL of company logo - url = LOGO_URL.format(**{"image_id": data["logo"]["image_id"] if "logo" in data else ""}) - - # Try to get found date of company - founded = dt.utcfromtimestamp(data["start_date"]).date() if "start_date" in data else "?" - - # Generate list of games, that company have developed or published - developed = ", ".join(game["name"] for game in data["developed"]) if "developed" in data else "?" - published = ", ".join(game["name"] for game in data["published"]) if "published" in data else "?" - - formatting = { - "name": data["name"], - "url": data["url"], - "description": f"{data['description']}\n\n" if "description" in data else "\n", - "founded": founded, - "developed": developed, - "published": published - } - page = COMPANY_PAGE.format(**formatting) - - return page, url - - async def get_best_results(self, query: str) -> list[tuple[float, str]]: - """Get best match result of genre when original genre is invalid.""" - results = [] - for genre in self.genres: - ratios = [difflib.SequenceMatcher(None, query, genre).ratio()] - for word in REGEX_NON_ALPHABET.split(genre): - ratios.append(difflib.SequenceMatcher(None, query, word).ratio()) - results.append((round(max(ratios), 2), genre)) - return sorted((item for item in results if item[0] >= 0.60), reverse=True)[:4] - - -def setup(bot: Bot) -> None: - """Load the Games cog.""" - # Check does IGDB API key exist, if not, log warning and don't load cog - if not Tokens.igdb_client_id: - logger.warning("No IGDB client ID. Not loading Games cog.") - return - if not Tokens.igdb_client_secret: - logger.warning("No IGDB client secret. Not loading Games cog.") - return - bot.add_cog(Games(bot)) diff --git a/bot/exts/evergreen/magic_8ball.py b/bot/exts/evergreen/magic_8ball.py deleted file mode 100644 index 28ddcea0..00000000 --- a/bot/exts/evergreen/magic_8ball.py +++ /dev/null @@ -1,30 +0,0 @@ -import json -import logging -import random -from pathlib import Path - -from discord.ext import commands - -from bot.bot import Bot - -log = logging.getLogger(__name__) - -ANSWERS = json.loads(Path("bot/resources/evergreen/magic8ball.json").read_text("utf8")) - - -class Magic8ball(commands.Cog): - """A Magic 8ball command to respond to a user's question.""" - - @commands.command(name="8ball") - async def output_answer(self, ctx: commands.Context, *, question: str) -> None: - """Return a Magic 8ball answer from answers list.""" - if len(question.split()) >= 3: - answer = random.choice(ANSWERS) - await ctx.send(answer) - else: - await ctx.send("Usage: .8ball (minimum length of 3 eg: `will I win?`)") - - -def setup(bot: Bot) -> None: - """Load the Magic8Ball Cog.""" - bot.add_cog(Magic8ball()) diff --git a/bot/exts/evergreen/minesweeper.py b/bot/exts/evergreen/minesweeper.py deleted file mode 100644 index a48b5051..00000000 --- a/bot/exts/evergreen/minesweeper.py +++ /dev/null @@ -1,270 +0,0 @@ -import logging -from collections.abc import Iterator -from dataclasses import dataclass -from random import randint, random -from typing import Union - -import discord -from discord.ext import commands - -from bot.bot import Bot -from bot.constants import Client -from bot.utils.converters import CoordinateConverter -from bot.utils.exceptions import UserNotPlayingError -from bot.utils.extensions import invoke_help_command - -MESSAGE_MAPPING = { - 0: ":stop_button:", - 1: ":one:", - 2: ":two:", - 3: ":three:", - 4: ":four:", - 5: ":five:", - 6: ":six:", - 7: ":seven:", - 8: ":eight:", - 9: ":nine:", - 10: ":keycap_ten:", - "bomb": ":bomb:", - "hidden": ":grey_question:", - "flag": ":flag_black:", - "x": ":x:" -} - -log = logging.getLogger(__name__) - - -GameBoard = list[list[Union[str, int]]] - - -@dataclass -class Game: - """The data for a game.""" - - board: GameBoard - revealed: GameBoard - dm_msg: discord.Message - chat_msg: discord.Message - activated_on_server: bool - - -class Minesweeper(commands.Cog): - """Play a game of Minesweeper.""" - - def __init__(self): - self.games: dict[int, Game] = {} - - @commands.group(name="minesweeper", aliases=("ms",), invoke_without_command=True) - async def minesweeper_group(self, ctx: commands.Context) -> None: - """Commands for Playing Minesweeper.""" - await invoke_help_command(ctx) - - @staticmethod - def get_neighbours(x: int, y: int) -> Iterator[tuple[int, int]]: - """Get all the neighbouring x and y including it self.""" - for x_ in [x - 1, x, x + 1]: - for y_ in [y - 1, y, y + 1]: - if x_ != -1 and x_ != 10 and y_ != -1 and y_ != 10: - yield x_, y_ - - def generate_board(self, bomb_chance: float) -> GameBoard: - """Generate a 2d array for the board.""" - board: GameBoard = [ - [ - "bomb" if random() <= bomb_chance else "number" - for _ in range(10) - ] for _ in range(10) - ] - - # make sure there is always a free cell - board[randint(0, 9)][randint(0, 9)] = "number" - - for y, row in enumerate(board): - for x, cell in enumerate(row): - if cell == "number": - # calculate bombs near it - bombs = 0 - for x_, y_ in self.get_neighbours(x, y): - if board[y_][x_] == "bomb": - bombs += 1 - board[y][x] = bombs - return board - - @staticmethod - def format_for_discord(board: GameBoard) -> str: - """Format the board as a string for Discord.""" - discord_msg = ( - ":stop_button: :regional_indicator_a: :regional_indicator_b: :regional_indicator_c: " - ":regional_indicator_d: :regional_indicator_e: :regional_indicator_f: :regional_indicator_g: " - ":regional_indicator_h: :regional_indicator_i: :regional_indicator_j:\n\n" - ) - rows = [] - for row_number, row in enumerate(board): - new_row = f"{MESSAGE_MAPPING[row_number + 1]} " - new_row += " ".join(MESSAGE_MAPPING[cell] for cell in row) - rows.append(new_row) - - discord_msg += "\n".join(rows) - return discord_msg - - @minesweeper_group.command(name="start") - async def start_command(self, ctx: commands.Context, bomb_chance: float = .2) -> None: - """Start a game of Minesweeper.""" - if ctx.author.id in self.games: # Player is already playing - await ctx.send(f"{ctx.author.mention} you already have a game running!", delete_after=2) - await ctx.message.delete(delay=2) - return - - try: - await ctx.author.send( - f"Play by typing: `{Client.prefix}ms reveal xy [xy]` or `{Client.prefix}ms flag xy [xy]` \n" - f"Close the game with `{Client.prefix}ms end`\n" - ) - except discord.errors.Forbidden: - log.debug(f"{ctx.author.name} ({ctx.author.id}) has disabled DMs from server members.") - await ctx.send(f":x: {ctx.author.mention}, please enable DMs to play minesweeper.") - return - - # Add game to list - board: GameBoard = self.generate_board(bomb_chance) - revealed_board: GameBoard = [["hidden"] * 10 for _ in range(10)] - dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(revealed_board)}") - - if ctx.guild: - await ctx.send(f"{ctx.author.mention} is playing Minesweeper.") - chat_msg = await ctx.send(f"Here's their board!\n{self.format_for_discord(revealed_board)}") - else: - chat_msg = None - - self.games[ctx.author.id] = Game( - board=board, - revealed=revealed_board, - dm_msg=dm_msg, - chat_msg=chat_msg, - activated_on_server=ctx.guild is not None - ) - - async def update_boards(self, ctx: commands.Context) -> None: - """Update both playing boards.""" - game = self.games[ctx.author.id] - await game.dm_msg.delete() - game.dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(game.revealed)}") - if game.activated_on_server: - await game.chat_msg.edit(content=f"Here's their board!\n{self.format_for_discord(game.revealed)}") - - @commands.dm_only() - @minesweeper_group.command(name="flag") - async def flag_command(self, ctx: commands.Context, *coordinates: CoordinateConverter) -> None: - """Place multiple flags on the board.""" - if ctx.author.id not in self.games: - raise UserNotPlayingError - board: GameBoard = self.games[ctx.author.id].revealed - for x, y in coordinates: - if board[y][x] == "hidden": - board[y][x] = "flag" - - await self.update_boards(ctx) - - @staticmethod - def reveal_bombs(revealed: GameBoard, board: GameBoard) -> None: - """Reveals all the bombs.""" - for y, row in enumerate(board): - for x, cell in enumerate(row): - if cell == "bomb": - revealed[y][x] = cell - - async def lost(self, ctx: commands.Context) -> None: - """The player lost the game.""" - game = self.games[ctx.author.id] - self.reveal_bombs(game.revealed, game.board) - await ctx.author.send(":fire: You lost! :fire:") - if game.activated_on_server: - await game.chat_msg.channel.send(f":fire: {ctx.author.mention} just lost Minesweeper! :fire:") - - async def won(self, ctx: commands.Context) -> None: - """The player won the game.""" - game = self.games[ctx.author.id] - await ctx.author.send(":tada: You won! :tada:") - if game.activated_on_server: - await game.chat_msg.channel.send(f":tada: {ctx.author.mention} just won Minesweeper! :tada:") - - def reveal_zeros(self, revealed: GameBoard, board: GameBoard, x: int, y: int) -> None: - """Recursively reveal adjacent cells when a 0 cell is encountered.""" - for x_, y_ in self.get_neighbours(x, y): - if revealed[y_][x_] != "hidden": - continue - revealed[y_][x_] = board[y_][x_] - if board[y_][x_] == 0: - self.reveal_zeros(revealed, board, x_, y_) - - async def check_if_won(self, ctx: commands.Context, revealed: GameBoard, board: GameBoard) -> bool: - """Checks if a player has won.""" - if any( - revealed[y][x] in ["hidden", "flag"] and board[y][x] != "bomb" - for x in range(10) - for y in range(10) - ): - return False - else: - await self.won(ctx) - return True - - async def reveal_one( - self, - ctx: commands.Context, - revealed: GameBoard, - board: GameBoard, - x: int, - y: int - ) -> bool: - """ - Reveal one square. - - return is True if the game ended, breaking the loop in `reveal_command` and deleting the game. - """ - revealed[y][x] = board[y][x] - if board[y][x] == "bomb": - await self.lost(ctx) - revealed[y][x] = "x" # mark bomb that made you lose with a x - return True - elif board[y][x] == 0: - self.reveal_zeros(revealed, board, x, y) - return await self.check_if_won(ctx, revealed, board) - - @commands.dm_only() - @minesweeper_group.command(name="reveal") - async def reveal_command(self, ctx: commands.Context, *coordinates: CoordinateConverter) -> None: - """Reveal multiple cells.""" - if ctx.author.id not in self.games: - raise UserNotPlayingError - game = self.games[ctx.author.id] - revealed: GameBoard = game.revealed - board: GameBoard = game.board - - for x, y in coordinates: - # reveal_one returns True if the revealed cell is a bomb or the player won, ending the game - if await self.reveal_one(ctx, revealed, board, x, y): - await self.update_boards(ctx) - del self.games[ctx.author.id] - break - else: - await self.update_boards(ctx) - - @minesweeper_group.command(name="end") - async def end_command(self, ctx: commands.Context) -> None: - """End your current game.""" - if ctx.author.id not in self.games: - raise UserNotPlayingError - game = self.games[ctx.author.id] - game.revealed = game.board - await self.update_boards(ctx) - new_msg = f":no_entry: Game canceled. :no_entry:\n{game.dm_msg.content}" - await game.dm_msg.edit(content=new_msg) - if game.activated_on_server: - await game.chat_msg.edit(content=new_msg) - del self.games[ctx.author.id] - - -def setup(bot: Bot) -> None: - """Load the Minesweeper cog.""" - bot.add_cog(Minesweeper()) diff --git a/bot/exts/evergreen/movie.py b/bot/exts/evergreen/movie.py deleted file mode 100644 index a04eeb41..00000000 --- a/bot/exts/evergreen/movie.py +++ /dev/null @@ -1,205 +0,0 @@ -import logging -import random -from enum import Enum -from typing import Any - -from aiohttp import ClientSession -from discord import Embed -from discord.ext.commands import Cog, Context, group - -from bot.bot import Bot -from bot.constants import Tokens -from bot.utils.extensions import invoke_help_command -from bot.utils.pagination import ImagePaginator - -# Define base URL of TMDB -BASE_URL = "https://api.themoviedb.org/3/" - -logger = logging.getLogger(__name__) - -# Define movie params, that will be used for every movie request -MOVIE_PARAMS = { - "api_key": Tokens.tmdb, - "language": "en-US" -} - - -class MovieGenres(Enum): - """Movies Genre names and IDs.""" - - Action = "28" - Adventure = "12" - Animation = "16" - Comedy = "35" - Crime = "80" - Documentary = "99" - Drama = "18" - Family = "10751" - Fantasy = "14" - History = "36" - Horror = "27" - Music = "10402" - Mystery = "9648" - Romance = "10749" - Science = "878" - Thriller = "53" - Western = "37" - - -class Movie(Cog): - """Movie Cog contains movies command that grab random movies from TMDB.""" - - def __init__(self, bot: Bot): - self.http_session: ClientSession = bot.http_session - - @group(name="movies", aliases=("movie",), invoke_without_command=True) - async def movies(self, ctx: Context, genre: str = "", amount: int = 5) -> None: - """ - Get random movies by specifying genre. Also support amount parameter, that define how much movies will be shown. - - Default 5. Use .movies genres to get all available genres. - """ - # Check is there more than 20 movies specified, due TMDB return 20 movies - # per page, so this is max. Also you can't get less movies than 1, just logic - if amount > 20: - await ctx.send("You can't get more than 20 movies at once. (TMDB limits)") - return - elif amount < 1: - await ctx.send("You can't get less than 1 movie.") - return - - # Capitalize genre for getting data from Enum, get random page, send help when genre don't exist. - genre = genre.capitalize() - try: - result = await self.get_movies_data(self.http_session, MovieGenres[genre].value, 1) - except KeyError: - await invoke_help_command(ctx) - return - - # Check if "results" is in result. If not, throw error. - if "results" not in result: - err_msg = ( - f"There is problem while making TMDB API request. Response Code: {result['status_code']}, " - f"{result['status_message']}." - ) - await ctx.send(err_msg) - logger.warning(err_msg) - - # Get random page. Max page is last page where is movies with this genre. - page = random.randint(1, result["total_pages"]) - - # Get movies list from TMDB, check if results key in result. When not, raise error. - movies = await self.get_movies_data(self.http_session, MovieGenres[genre].value, page) - if "results" not in movies: - err_msg = f"There is problem while making TMDB API request. Response Code: {result['status_code']}, " \ - f"{result['status_message']}." - await ctx.send(err_msg) - logger.warning(err_msg) - - # Get all pages and embed - pages = await self.get_pages(self.http_session, movies, amount) - embed = await self.get_embed(genre) - - await ImagePaginator.paginate(pages, ctx, embed) - - @movies.command(name="genres", aliases=("genre", "g")) - async def genres(self, ctx: Context) -> None: - """Show all currently available genres for .movies command.""" - await ctx.send(f"Current available genres: {', '.join('`' + genre.name + '`' for genre in MovieGenres)}") - - async def get_movies_data(self, client: ClientSession, genre_id: str, page: int) -> list[dict[str, Any]]: - """Return JSON of TMDB discover request.""" - # Define params of request - params = { - "api_key": Tokens.tmdb, - "language": "en-US", - "sort_by": "popularity.desc", - "include_adult": "false", - "include_video": "false", - "page": page, - "with_genres": genre_id - } - - url = BASE_URL + "discover/movie" - - # Make discover request to TMDB, return result - async with client.get(url, params=params) as resp: - return await resp.json() - - async def get_pages(self, client: ClientSession, movies: dict[str, Any], amount: int) -> list[tuple[str, str]]: - """Fetch all movie pages from movies dictionary. Return list of pages.""" - pages = [] - - for i in range(amount): - movie_id = movies["results"][i]["id"] - movie = await self.get_movie(client, movie_id) - - page, img = await self.create_page(movie) - pages.append((page, img)) - - return pages - - async def get_movie(self, client: ClientSession, movie: int) -> dict[str, Any]: - """Get Movie by movie ID from TMDB. Return result dictionary.""" - if not isinstance(movie, int): - raise ValueError("Error while fetching movie from TMDB, movie argument must be integer. ") - url = BASE_URL + f"movie/{movie}" - - async with client.get(url, params=MOVIE_PARAMS) as resp: - return await resp.json() - - async def create_page(self, movie: dict[str, Any]) -> tuple[str, str]: - """Create page from TMDB movie request result. Return formatted page + image.""" - text = "" - - # Add title + tagline (if not empty) - text += f"**{movie['title']}**\n" - if movie["tagline"]: - text += f"{movie['tagline']}\n\n" - else: - text += "\n" - - # Add other information - text += f"**Rating:** {movie['vote_average']}/10 :star:\n" - text += f"**Release Date:** {movie['release_date']}\n\n" - - text += "__**Production Information**__\n" - - companies = movie["production_companies"] - countries = movie["production_countries"] - - text += f"**Made by:** {', '.join(company['name'] for company in companies)}\n" - text += f"**Made in:** {', '.join(country['name'] for country in countries)}\n\n" - - text += "__**Some Numbers**__\n" - - budget = f"{movie['budget']:,d}" if movie['budget'] else "?" - revenue = f"{movie['revenue']:,d}" if movie['revenue'] else "?" - - if movie["runtime"] is not None: - duration = divmod(movie["runtime"], 60) - else: - duration = ("?", "?") - - text += f"**Budget:** ${budget}\n" - text += f"**Revenue:** ${revenue}\n" - text += f"**Duration:** {f'{duration[0]} hour(s) {duration[1]} minute(s)'}\n\n" - - text += movie["overview"] - - img = f"http://image.tmdb.org/t/p/w200{movie['poster_path']}" - - # Return page content and image - return text, img - - async def get_embed(self, name: str) -> Embed: - """Return embed of random movies. Uses name in title.""" - embed = Embed(title=f"Random {name} Movies") - embed.set_footer(text="This product uses the TMDb API but is not endorsed or certified by TMDb.") - embed.set_thumbnail(url="https://i.imgur.com/LtFtC8H.png") - return embed - - -def setup(bot: Bot) -> None: - """Load the Movie Cog.""" - bot.add_cog(Movie(bot)) diff --git a/bot/exts/evergreen/recommend_game.py b/bot/exts/evergreen/recommend_game.py deleted file mode 100644 index bdd3acb1..00000000 --- a/bot/exts/evergreen/recommend_game.py +++ /dev/null @@ -1,51 +0,0 @@ -import json -import logging -from pathlib import Path -from random import shuffle - -import discord -from discord.ext import commands - -from bot.bot import Bot - -log = logging.getLogger(__name__) -game_recs = [] - -# Populate the list `game_recs` with resource files -for rec_path in Path("bot/resources/evergreen/game_recs").glob("*.json"): - data = json.loads(rec_path.read_text("utf8")) - game_recs.append(data) -shuffle(game_recs) - - -class RecommendGame(commands.Cog): - """Commands related to recommending games.""" - - def __init__(self, bot: Bot): - self.bot = bot - self.index = 0 - - @commands.command(name="recommendgame", aliases=("gamerec",)) - async def recommend_game(self, ctx: commands.Context) -> None: - """Sends an Embed of a random game recommendation.""" - if self.index >= len(game_recs): - self.index = 0 - shuffle(game_recs) - game = game_recs[self.index] - self.index += 1 - - author = self.bot.get_user(int(game["author"])) - - # Creating and formatting Embed - embed = discord.Embed(color=discord.Colour.blue()) - if author is not None: - embed.set_author(name=author.name, icon_url=author.display_avatar.url) - embed.set_image(url=game["image"]) - embed.add_field(name=f"Recommendation: {game['title']}\n{game['link']}", value=game["description"]) - - await ctx.send(embed=embed) - - -def setup(bot: Bot) -> None: - """Loads the RecommendGame cog.""" - bot.add_cog(RecommendGame(bot)) diff --git a/bot/exts/evergreen/rps.py b/bot/exts/evergreen/rps.py deleted file mode 100644 index c6bbff46..00000000 --- a/bot/exts/evergreen/rps.py +++ /dev/null @@ -1,57 +0,0 @@ -from random import choice - -from discord.ext import commands - -from bot.bot import Bot - -CHOICES = ["rock", "paper", "scissors"] -SHORT_CHOICES = ["r", "p", "s"] - -# Using a dictionary instead of conditions to check for the winner. -WINNER_DICT = { - "r": { - "r": 0, - "p": -1, - "s": 1, - }, - "p": { - "r": 1, - "p": 0, - "s": -1, - }, - "s": { - "r": -1, - "p": 1, - "s": 0, - } -} - - -class RPS(commands.Cog): - """Rock Paper Scissors. The Classic Game!""" - - @commands.command(case_insensitive=True) - async def rps(self, ctx: commands.Context, move: str) -> None: - """Play the classic game of Rock Paper Scissors with your own sir-lancebot!""" - move = move.lower() - player_mention = ctx.author.mention - - if move not in CHOICES and move not in SHORT_CHOICES: - raise commands.BadArgument(f"Invalid move. Please make move from options: {', '.join(CHOICES).upper()}.") - - bot_move = choice(CHOICES) - # value of player_result will be from (-1, 0, 1) as (lost, tied, won). - player_result = WINNER_DICT[move[0]][bot_move[0]] - - if player_result == 0: - message_string = f"{player_mention} You and Sir Lancebot played {bot_move}, it's a tie." - await ctx.send(message_string) - elif player_result == 1: - await ctx.send(f"Sir Lancebot played {bot_move}! {player_mention} won!") - else: - await ctx.send(f"Sir Lancebot played {bot_move}! {player_mention} lost!") - - -def setup(bot: Bot) -> None: - """Load the RPS Cog.""" - bot.add_cog(RPS(bot)) diff --git a/bot/exts/evergreen/space.py b/bot/exts/evergreen/space.py deleted file mode 100644 index 48ad0f96..00000000 --- a/bot/exts/evergreen/space.py +++ /dev/null @@ -1,236 +0,0 @@ -import logging -import random -from datetime import date, datetime -from typing import Any, Optional -from urllib.parse import urlencode - -from discord import Embed -from discord.ext import tasks -from discord.ext.commands import Cog, Context, group - -from bot.bot import Bot -from bot.constants import Tokens -from bot.utils.converters import DateConverter -from bot.utils.extensions import invoke_help_command - -logger = logging.getLogger(__name__) - -NASA_BASE_URL = "https://api.nasa.gov" -NASA_IMAGES_BASE_URL = "https://images-api.nasa.gov" -NASA_EPIC_BASE_URL = "https://epic.gsfc.nasa.gov" - -APOD_MIN_DATE = date(1995, 6, 16) - - -class Space(Cog): - """Space Cog contains commands, that show images, facts or other information about space.""" - - def __init__(self, bot: Bot): - self.http_session = bot.http_session - - self.rovers = {} - self.get_rovers.start() - - def cog_unload(self) -> None: - """Cancel `get_rovers` task when Cog will unload.""" - self.get_rovers.cancel() - - @tasks.loop(hours=24) - async def get_rovers(self) -> None: - """Get listing of rovers from NASA API and info about their start and end dates.""" - data = await self.fetch_from_nasa("mars-photos/api/v1/rovers") - - for rover in data["rovers"]: - self.rovers[rover["name"].lower()] = { - "min_date": rover["landing_date"], - "max_date": rover["max_date"], - "max_sol": rover["max_sol"] - } - - @group(name="space", invoke_without_command=True) - async def space(self, ctx: Context) -> None: - """Head command that contains commands about space.""" - await invoke_help_command(ctx) - - @space.command(name="apod") - async def apod(self, ctx: Context, date: Optional[str]) -> None: - """ - Get Astronomy Picture of Day from NASA API. Date is optional parameter, what formatting is YYYY-MM-DD. - - If date is not specified, this will get today APOD. - """ - params = {} - # Parse date to params, when provided. Show error message when invalid formatting - if date: - try: - apod_date = datetime.strptime(date, "%Y-%m-%d").date() - except ValueError: - await ctx.send(f"Invalid date {date}. Please make sure your date is in format YYYY-MM-DD.") - return - - now = datetime.now().date() - if APOD_MIN_DATE > apod_date or now < apod_date: - await ctx.send(f"Date must be between {APOD_MIN_DATE.isoformat()} and {now.isoformat()} (today).") - return - - params["date"] = apod_date.isoformat() - - result = await self.fetch_from_nasa("planetary/apod", params) - - await ctx.send( - embed=self.create_nasa_embed( - f"Astronomy Picture of the Day - {result['date']}", - result["explanation"], - result["url"] - ) - ) - - @space.command(name="nasa") - async def nasa(self, ctx: Context, *, search_term: Optional[str]) -> None: - """Get random NASA information/facts + image. Support `search_term` parameter for more specific search.""" - params = { - "media_type": "image" - } - if search_term: - params["q"] = search_term - - # Don't use API key, no need for this. - data = await self.fetch_from_nasa("search", params, NASA_IMAGES_BASE_URL, use_api_key=False) - if len(data["collection"]["items"]) == 0: - await ctx.send(f"Can't find any items with search term `{search_term}`.") - return - - item = random.choice(data["collection"]["items"]) - - await ctx.send( - embed=self.create_nasa_embed( - item["data"][0]["title"], - item["data"][0]["description"], - item["links"][0]["href"] - ) - ) - - @space.command(name="epic") - async def epic(self, ctx: Context, date: Optional[str]) -> None: - """Get a random image of the Earth from the NASA EPIC API. Support date parameter, format is YYYY-MM-DD.""" - if date: - try: - show_date = datetime.strptime(date, "%Y-%m-%d").date().isoformat() - except ValueError: - await ctx.send(f"Invalid date {date}. Please make sure your date is in format YYYY-MM-DD.") - return - else: - show_date = None - - # Don't use API key, no need for this. - data = await self.fetch_from_nasa( - f"api/natural{f'/date/{show_date}' if show_date else ''}", - base=NASA_EPIC_BASE_URL, - use_api_key=False - ) - if len(data) < 1: - await ctx.send("Can't find any images in this date.") - return - - item = random.choice(data) - - year, month, day = item["date"].split(" ")[0].split("-") - image_url = f"{NASA_EPIC_BASE_URL}/archive/natural/{year}/{month}/{day}/jpg/{item['image']}.jpg" - - await ctx.send( - embed=self.create_nasa_embed( - "Earth Image", item["caption"], image_url, f" \u2022 Identifier: {item['identifier']}" - ) - ) - - @space.group(name="mars", invoke_without_command=True) - async def mars( - self, - ctx: Context, - date: Optional[DateConverter], - rover: str = "curiosity" - ) -> None: - """ - Get random Mars image by date. Support both SOL (martian solar day) and earth date and rovers. - - Earth date formatting is YYYY-MM-DD. Use `.space mars dates` to get all currently available rovers. - """ - rover = rover.lower() - if rover not in self.rovers: - await ctx.send( - ( - f"Invalid rover `{rover}`.\n" - f"**Rovers:** `{'`, `'.join(f'{r.capitalize()}' for r in self.rovers)}`" - ) - ) - return - - # When date not provided, get random SOL date between 0 and rover's max. - if date is None: - date = random.randint(0, self.rovers[rover]["max_sol"]) - - params = {} - if isinstance(date, int): - params["sol"] = date - else: - params["earth_date"] = date.date().isoformat() - - result = await self.fetch_from_nasa(f"mars-photos/api/v1/rovers/{rover}/photos", params) - if len(result["photos"]) < 1: - err_msg = ( - f"We can't find result in date " - f"{date.date().isoformat() if isinstance(date, datetime) else f'{date} SOL'}.\n" - f"**Note:** Dates must match with rover's working dates. Please use `{ctx.prefix}space mars dates` to " - "see working dates for each rover." - ) - await ctx.send(err_msg) - return - - item = random.choice(result["photos"]) - await ctx.send( - embed=self.create_nasa_embed( - f"{item['rover']['name']}'s {item['camera']['full_name']} Mars Image", "", item["img_src"], - ) - ) - - @mars.command(name="dates", aliases=("d", "date", "rover", "rovers", "r")) - async def dates(self, ctx: Context) -> None: - """Get current available rovers photo date ranges.""" - await ctx.send("\n".join( - f"**{r.capitalize()}:** {i['min_date']} **-** {i['max_date']}" for r, i in self.rovers.items() - )) - - async def fetch_from_nasa( - self, - endpoint: str, - additional_params: Optional[dict[str, Any]] = None, - base: Optional[str] = NASA_BASE_URL, - use_api_key: bool = True - ) -> dict[str, Any]: - """Fetch information from NASA API, return result.""" - params = {} - if use_api_key: - params["api_key"] = Tokens.nasa - - # Add additional parameters to request parameters only when they provided by user - if additional_params is not None: - params.update(additional_params) - - async with self.http_session.get(url=f"{base}/{endpoint}?{urlencode(params)}") as resp: - return await resp.json() - - def create_nasa_embed(self, title: str, description: str, image: str, footer: Optional[str] = "") -> Embed: - """Generate NASA commands embeds. Required: title, description and image URL, footer (addition) is optional.""" - return Embed( - title=title, - description=description - ).set_image(url=image).set_footer(text="Powered by NASA API" + footer) - - -def setup(bot: Bot) -> None: - """Load the Space cog.""" - if not Tokens.nasa: - logger.warning("Can't find NASA API key. Not loading Space Cog.") - return - - bot.add_cog(Space(bot)) diff --git a/bot/exts/evergreen/speedrun.py b/bot/exts/evergreen/speedrun.py deleted file mode 100644 index 774eff81..00000000 --- a/bot/exts/evergreen/speedrun.py +++ /dev/null @@ -1,26 +0,0 @@ -import json -import logging -from pathlib import Path -from random import choice - -from discord.ext import commands - -from bot.bot import Bot - -log = logging.getLogger(__name__) - -LINKS = json.loads(Path("bot/resources/evergreen/speedrun_links.json").read_text("utf8")) - - -class Speedrun(commands.Cog): - """Commands about the video game speedrunning community.""" - - @commands.command(name="speedrun") - async def get_speedrun(self, ctx: commands.Context) -> None: - """Sends a link to a video of a random speedrun.""" - await ctx.send(choice(LINKS)) - - -def setup(bot: Bot) -> None: - """Load the Speedrun cog.""" - bot.add_cog(Speedrun()) diff --git a/bot/exts/evergreen/status_codes.py b/bot/exts/evergreen/status_codes.py deleted file mode 100644 index 501cbe0a..00000000 --- a/bot/exts/evergreen/status_codes.py +++ /dev/null @@ -1,87 +0,0 @@ -from random import choice - -import discord -from discord.ext import commands - -from bot.bot import Bot - -HTTP_DOG_URL = "https://httpstatusdogs.com/img/{code}.jpg" -HTTP_CAT_URL = "https://http.cat/{code}.jpg" -STATUS_TEMPLATE = "**Status: {code}**" -ERR_404 = "Unable to find status floof for {code}." -ERR_UNKNOWN = "Error attempting to retrieve status floof for {code}." -ERROR_LENGTH_EMBED = discord.Embed( - title="Input status code does not exist", - description="The range of valid status codes is 100 to 599", -) - - -class HTTPStatusCodes(commands.Cog): - """ - Fetch an image depicting HTTP status codes as a dog or a cat. - - If neither animal is selected a cat or dog is chosen randomly for the given status code. - """ - - def __init__(self, bot: Bot): - self.bot = bot - - @commands.group( - name="http_status", - aliases=("status", "httpstatus"), - invoke_without_command=True, - ) - async def http_status_group(self, ctx: commands.Context, code: int) -> None: - """Choose a cat or dog randomly for the given status code.""" - subcmd = choice((self.http_cat, self.http_dog)) - await subcmd(ctx, code) - - @http_status_group.command(name="cat") - async def http_cat(self, ctx: commands.Context, code: int) -> None: - """Send a cat version of the requested HTTP status code.""" - if code in range(100, 600): - await self.build_embed(url=HTTP_CAT_URL.format(code=code), ctx=ctx, code=code) - return - await ctx.send(embed=ERROR_LENGTH_EMBED) - - @http_status_group.command(name="dog") - async def http_dog(self, ctx: commands.Context, code: int) -> None: - """Send a dog version of the requested HTTP status code.""" - if code in range(100, 600): - await self.build_embed(url=HTTP_DOG_URL.format(code=code), ctx=ctx, code=code) - return - await ctx.send(embed=ERROR_LENGTH_EMBED) - - async def build_embed(self, url: str, ctx: commands.Context, code: int) -> None: - """Attempt to build and dispatch embed. Append error message instead if something goes wrong.""" - async with self.bot.http_session.get(url, allow_redirects=False) as response: - if response.status in range(200, 300): - await ctx.send( - embed=discord.Embed( - title=STATUS_TEMPLATE.format(code=code) - ).set_image(url=url) - ) - elif response.status in (302, 404): # dog URL returns 302 instead of 404 - if "dog" in url: - await ctx.send( - embed=discord.Embed( - title=ERR_404.format(code=code) - ).set_image(url="https://httpstatusdogs.com/img/404.jpg") - ) - return - await ctx.send( - embed=discord.Embed( - title=ERR_404.format(code=code) - ).set_image(url="https://http.cat/404.jpg") - ) - else: - await ctx.send( - embed=discord.Embed( - title=STATUS_TEMPLATE.format(code=code) - ).set_footer(text=ERR_UNKNOWN.format(code=code)) - ) - - -def setup(bot: Bot) -> None: - """Load the HTTPStatusCodes cog.""" - bot.add_cog(HTTPStatusCodes(bot)) diff --git a/bot/exts/evergreen/tic_tac_toe.py b/bot/exts/evergreen/tic_tac_toe.py deleted file mode 100644 index 5c4f8051..00000000 --- a/bot/exts/evergreen/tic_tac_toe.py +++ /dev/null @@ -1,335 +0,0 @@ -import asyncio -import random -from typing import Callable, Optional, Union - -import discord -from discord.ext.commands import Cog, Context, check, group, guild_only - -from bot.bot import Bot -from bot.constants import Emojis -from bot.utils.pagination import LinePaginator - -CONFIRMATION_MESSAGE = ( - "{opponent}, {requester} wants to play Tic-Tac-Toe against you." - f"\nReact to this message with {Emojis.confirmation} to accept or with {Emojis.decline} to decline." -) - - -def check_win(board: dict[int, str]) -> bool: - """Check from board, is any player won game.""" - return any( - ( - # Horizontal - board[1] == board[2] == board[3], - board[4] == board[5] == board[6], - board[7] == board[8] == board[9], - # Vertical - board[1] == board[4] == board[7], - board[2] == board[5] == board[8], - board[3] == board[6] == board[9], - # Diagonal - board[1] == board[5] == board[9], - board[3] == board[5] == board[7], - ) - ) - - -class Player: - """Class that contains information about player and functions that interact with player.""" - - def __init__(self, user: discord.User, ctx: Context, symbol: str): - self.user = user - self.ctx = ctx - self.symbol = symbol - - async def get_move(self, board: dict[int, str], msg: discord.Message) -> tuple[bool, Optional[int]]: - """ - Get move from user. - - Return is timeout reached and position of field what user will fill when timeout don't reach. - """ - def check_for_move(r: discord.Reaction, u: discord.User) -> bool: - """Check does user who reacted is user who we want, message is board and emoji is in board values.""" - return ( - u.id == self.user.id - and msg.id == r.message.id - and r.emoji in board.values() - and r.emoji in Emojis.number_emojis.values() - ) - - try: - react, _ = await self.ctx.bot.wait_for("reaction_add", timeout=30.0, check=check_for_move) - except asyncio.TimeoutError: - return True, None - else: - return False, list(Emojis.number_emojis.keys())[list(Emojis.number_emojis.values()).index(react.emoji)] - - def __str__(self) -> str: - """Return mention of user.""" - return self.user.mention - - -class AI: - """Tic Tac Toe AI class for against computer gaming.""" - - def __init__(self, symbol: str): - self.symbol = symbol - - async def get_move(self, board: dict[int, str], _: discord.Message) -> tuple[bool, int]: - """Get move from AI. AI use Minimax strategy.""" - possible_moves = [i for i, emoji in board.items() if emoji in list(Emojis.number_emojis.values())] - - for symbol in (Emojis.o_square, Emojis.x_square): - for move in possible_moves: - board_copy = board.copy() - board_copy[move] = symbol - if check_win(board_copy): - return False, move - - open_corners = [i for i in possible_moves if i in (1, 3, 7, 9)] - if len(open_corners) > 0: - return False, random.choice(open_corners) - - if 5 in possible_moves: - return False, 5 - - open_edges = [i for i in possible_moves if i in (2, 4, 6, 8)] - return False, random.choice(open_edges) - - def __str__(self) -> str: - """Return `AI` as user name.""" - return "AI" - - -class Game: - """Class that contains information and functions about Tic Tac Toe game.""" - - def __init__(self, players: list[Union[Player, AI]], ctx: Context): - self.players = players - self.ctx = ctx - self.board = { - 1: Emojis.number_emojis[1], - 2: Emojis.number_emojis[2], - 3: Emojis.number_emojis[3], - 4: Emojis.number_emojis[4], - 5: Emojis.number_emojis[5], - 6: Emojis.number_emojis[6], - 7: Emojis.number_emojis[7], - 8: Emojis.number_emojis[8], - 9: Emojis.number_emojis[9] - } - - self.current = self.players[0] - self.next = self.players[1] - - self.winner: Optional[Union[Player, AI]] = None - self.loser: Optional[Union[Player, AI]] = None - self.over = False - self.canceled = False - self.draw = False - - async def get_confirmation(self) -> tuple[bool, Optional[str]]: - """ - Ask does user want to play TicTacToe against requester. First player is always requester. - - This return tuple that have: - - first element boolean (is game accepted?) - - (optional, only when first element is False, otherwise None) reason for declining. - """ - confirm_message = await self.ctx.send( - CONFIRMATION_MESSAGE.format( - opponent=self.players[1].user.mention, - requester=self.players[0].user.mention - ) - ) - await confirm_message.add_reaction(Emojis.confirmation) - await confirm_message.add_reaction(Emojis.decline) - - def confirm_check(reaction: discord.Reaction, user: discord.User) -> bool: - """Check is user who reacted from who this was requested, message is confirmation and emoji is valid.""" - return ( - reaction.emoji in (Emojis.confirmation, Emojis.decline) - and reaction.message.id == confirm_message.id - and user == self.players[1].user - ) - - try: - reaction, user = await self.ctx.bot.wait_for( - "reaction_add", - timeout=60.0, - check=confirm_check - ) - except asyncio.TimeoutError: - self.over = True - self.canceled = True - await confirm_message.delete() - return False, "Running out of time... Cancelled game." - - await confirm_message.delete() - if reaction.emoji == Emojis.confirmation: - return True, None - else: - self.over = True - self.canceled = True - return False, "User declined" - - async def add_reactions(self, msg: discord.Message) -> None: - """Add number emojis to message.""" - for nr in Emojis.number_emojis.values(): - await msg.add_reaction(nr) - - def format_board(self) -> str: - """Get formatted tic-tac-toe board for message.""" - board = list(self.board.values()) - return "\n".join( - (f"{board[line]} {board[line + 1]} {board[line + 2]}" for line in range(0, len(board), 3)) - ) - - async def play(self) -> None: - """Start and handle game.""" - await self.ctx.send("It's time for the game! Let's begin.") - board = await self.ctx.send( - embed=discord.Embed(description=self.format_board()) - ) - await self.add_reactions(board) - - for _ in range(9): - if isinstance(self.current, Player): - announce = await self.ctx.send( - f"{self.current.user.mention}, it's your turn! " - "React with an emoji to take your go." - ) - timeout, pos = await self.current.get_move(self.board, board) - if isinstance(self.current, Player): - await announce.delete() - if timeout: - await self.ctx.send(f"{self.current.user.mention} ran out of time. Canceling game.") - self.over = True - self.canceled = True - return - self.board[pos] = self.current.symbol - await board.edit( - embed=discord.Embed(description=self.format_board()) - ) - await board.clear_reaction(Emojis.number_emojis[pos]) - if check_win(self.board): - self.winner = self.current - self.loser = self.next - await self.ctx.send( - f":tada: {self.current} won this game! :tada:" - ) - await board.clear_reactions() - break - self.current, self.next = self.next, self.current - if not self.winner: - self.draw = True - await self.ctx.send("It's a DRAW!") - self.over = True - - -def is_channel_free() -> Callable: - """Check is channel where command will be invoked free.""" - async def predicate(ctx: Context) -> bool: - return all(game.channel != ctx.channel for game in ctx.cog.games if not game.over) - return check(predicate) - - -def is_requester_free() -> Callable: - """Check is requester not already in any game.""" - async def predicate(ctx: Context) -> bool: - return all( - ctx.author not in (player.user for player in game.players) for game in ctx.cog.games if not game.over - ) - return check(predicate) - - -class TicTacToe(Cog): - """TicTacToe cog contains tic-tac-toe game commands.""" - - def __init__(self): - self.games: list[Game] = [] - - @guild_only() - @is_channel_free() - @is_requester_free() - @group(name="tictactoe", aliases=("ttt", "tic"), invoke_without_command=True) - async def tic_tac_toe(self, ctx: Context, opponent: Optional[discord.User]) -> None: - """Tic Tac Toe game. Play against friends or AI. Use reactions to add your mark to field.""" - if opponent == ctx.author: - await ctx.send("You can't play against yourself.") - return - if opponent is not None and not all( - opponent not in (player.user for player in g.players) for g in ctx.cog.games if not g.over - ): - await ctx.send("Opponent is already in game.") - return - if opponent is None: - game = Game( - [Player(ctx.author, ctx, Emojis.x_square), AI(Emojis.o_square)], - ctx - ) - else: - game = Game( - [Player(ctx.author, ctx, Emojis.x_square), Player(opponent, ctx, Emojis.o_square)], - ctx - ) - self.games.append(game) - if opponent is not None: - if opponent.bot: # check whether the opponent is a bot or not - await ctx.send("You can't play Tic-Tac-Toe with bots!") - return - - confirmed, msg = await game.get_confirmation() - - if not confirmed: - if msg: - await ctx.send(msg) - return - await game.play() - - @tic_tac_toe.group(name="history", aliases=("log",), invoke_without_command=True) - async def tic_tac_toe_logs(self, ctx: Context) -> None: - """Show most recent tic-tac-toe games.""" - if len(self.games) < 1: - await ctx.send("No recent games.") - return - log_games = [] - for i, game in enumerate(self.games): - if game.over and not game.canceled: - if game.draw: - log_games.append( - f"**#{i+1}**: {game.players[0]} vs {game.players[1]} (draw)" - ) - else: - log_games.append( - f"**#{i+1}**: {game.winner} :trophy: vs {game.loser}" - ) - await LinePaginator.paginate( - log_games, - ctx, - discord.Embed(title="Most recent Tic Tac Toe games") - ) - - @tic_tac_toe_logs.command(name="show", aliases=("s",)) - async def show_tic_tac_toe_board(self, ctx: Context, game_id: int) -> None: - """View game board by ID (ID is possible to get by `.tictactoe history`).""" - if len(self.games) < game_id: - await ctx.send("Game don't exist.") - return - game = self.games[game_id - 1] - - if game.draw: - description = f"{game.players[0]} vs {game.players[1]} (draw)\n\n{game.format_board()}" - else: - description = f"{game.winner} :trophy: vs {game.loser}\n\n{game.format_board()}" - - embed = discord.Embed( - title=f"Match #{game_id} Game Board", - description=description, - ) - await ctx.send(embed=embed) - - -def setup(bot: Bot) -> None: - """Load the TicTacToe cog.""" - bot.add_cog(TicTacToe()) diff --git a/bot/exts/evergreen/trivia_quiz.py b/bot/exts/evergreen/trivia_quiz.py deleted file mode 100644 index aa4020d6..00000000 --- a/bot/exts/evergreen/trivia_quiz.py +++ /dev/null @@ -1,593 +0,0 @@ -import asyncio -import json -import logging -import operator -import random -from dataclasses import dataclass -from pathlib import Path -from typing import Callable, Optional - -import discord -from discord.ext import commands -from rapidfuzz import fuzz - -from bot.bot import Bot -from bot.constants import Colours, NEGATIVE_REPLIES, Roles - -logger = logging.getLogger(__name__) - -DEFAULT_QUESTION_LIMIT = 6 -STANDARD_VARIATION_TOLERANCE = 88 -DYNAMICALLY_GEN_VARIATION_TOLERANCE = 97 - -WRONG_ANS_RESPONSE = [ - "No one answered correctly!", - "Better luck next time...", -] - -N_PREFIX_STARTS_AT = 5 -N_PREFIXES = [ - "penta", "hexa", "hepta", "octa", "nona", - "deca", "hendeca", "dodeca", "trideca", "tetradeca", -] - -PLANETS = [ - ("1st", "Mercury"), - ("2nd", "Venus"), - ("3rd", "Earth"), - ("4th", "Mars"), - ("5th", "Jupiter"), - ("6th", "Saturn"), - ("7th", "Uranus"), - ("8th", "Neptune"), -] - -TAXONOMIC_HIERARCHY = [ - "species", "genus", "family", "order", - "class", "phylum", "kingdom", "domain", -] - -UNITS_TO_BASE_UNITS = { - "hertz": ("(unit of frequency)", "s^-1"), - "newton": ("(unit of force)", "m*kg*s^-2"), - "pascal": ("(unit of pressure & stress)", "m^-1*kg*s^-2"), - "joule": ("(unit of energy & quantity of heat)", "m^2*kg*s^-2"), - "watt": ("(unit of power)", "m^2*kg*s^-3"), - "coulomb": ("(unit of electric charge & quantity of electricity)", "s*A"), - "volt": ("(unit of voltage & electromotive force)", "m^2*kg*s^-3*A^-1"), - "farad": ("(unit of capacitance)", "m^-2*kg^-1*s^4*A^2"), - "ohm": ("(unit of electric resistance)", "m^2*kg*s^-3*A^-2"), - "weber": ("(unit of magnetic flux)", "m^2*kg*s^-2*A^-1"), - "tesla": ("(unit of magnetic flux density)", "kg*s^-2*A^-1"), -} - - -@dataclass(frozen=True) -class QuizEntry: - """Dataclass for a quiz entry (a question and a string containing answers separated by commas).""" - - question: str - answer: str - - -def linear_system(q_format: str, a_format: str) -> QuizEntry: - """Generate a system of linear equations with two unknowns.""" - x, y = random.randint(2, 5), random.randint(2, 5) - answer = a_format.format(x, y) - - coeffs = random.sample(range(1, 6), 4) - - question = q_format.format( - coeffs[0], - coeffs[1], - coeffs[0] * x + coeffs[1] * y, - coeffs[2], - coeffs[3], - coeffs[2] * x + coeffs[3] * y, - ) - - return QuizEntry(question, answer) - - -def mod_arith(q_format: str, a_format: str) -> QuizEntry: - """Generate a basic modular arithmetic question.""" - quotient, m, b = random.randint(30, 40), random.randint(10, 20), random.randint(200, 350) - ans = random.randint(0, 9) # max remainder is 9, since the minimum modulus is 10 - a = quotient * m + ans - b - - question = q_format.format(a, b, m) - answer = a_format.format(ans) - - return QuizEntry(question, answer) - - -def ngonal_prism(q_format: str, a_format: str) -> QuizEntry: - """Generate a question regarding vertices on n-gonal prisms.""" - n = random.randint(0, len(N_PREFIXES) - 1) - - question = q_format.format(N_PREFIXES[n]) - answer = a_format.format((n + N_PREFIX_STARTS_AT) * 2) - - return QuizEntry(question, answer) - - -def imag_sqrt(q_format: str, a_format: str) -> QuizEntry: - """Generate a negative square root question.""" - ans_coeff = random.randint(3, 10) - - question = q_format.format(ans_coeff ** 2) - answer = a_format.format(ans_coeff) - - return QuizEntry(question, answer) - - -def binary_calc(q_format: str, a_format: str) -> QuizEntry: - """Generate a binary calculation question.""" - a = random.randint(15, 20) - b = random.randint(10, a) - oper = random.choice( - ( - ("+", operator.add), - ("-", operator.sub), - ("*", operator.mul), - ) - ) - - # if the operator is multiplication, lower the values of the two operands to make it easier - if oper[0] == "*": - a -= 5 - b -= 5 - - question = q_format.format(a, oper[0], b) - answer = a_format.format(oper[1](a, b)) - - return QuizEntry(question, answer) - - -def solar_system(q_format: str, a_format: str) -> QuizEntry: - """Generate a question on the planets of the Solar System.""" - planet = random.choice(PLANETS) - - question = q_format.format(planet[0]) - answer = a_format.format(planet[1]) - - return QuizEntry(question, answer) - - -def taxonomic_rank(q_format: str, a_format: str) -> QuizEntry: - """Generate a question on taxonomic classification.""" - level = random.randint(0, len(TAXONOMIC_HIERARCHY) - 2) - - question = q_format.format(TAXONOMIC_HIERARCHY[level]) - answer = a_format.format(TAXONOMIC_HIERARCHY[level + 1]) - - return QuizEntry(question, answer) - - -def base_units_convert(q_format: str, a_format: str) -> QuizEntry: - """Generate a SI base units conversion question.""" - unit = random.choice(list(UNITS_TO_BASE_UNITS)) - - question = q_format.format( - unit + " " + UNITS_TO_BASE_UNITS[unit][0] - ) - answer = a_format.format( - UNITS_TO_BASE_UNITS[unit][1] - ) - - return QuizEntry(question, answer) - - -DYNAMIC_QUESTIONS_FORMAT_FUNCS = { - 201: linear_system, - 202: mod_arith, - 203: ngonal_prism, - 204: imag_sqrt, - 205: binary_calc, - 301: solar_system, - 302: taxonomic_rank, - 303: base_units_convert, -} - - -class TriviaQuiz(commands.Cog): - """A cog for all quiz commands.""" - - def __init__(self, bot: Bot): - self.bot = bot - - self.game_status = {} # A variable to store the game status: either running or not running. - self.game_owners = {} # A variable to store the person's ID who started the quiz game in a channel. - - self.questions = self.load_questions() - self.question_limit = 0 - - self.player_scores = {} # A variable to store all player's scores for a bot session. - self.game_player_scores = {} # A variable to store temporary game player's scores. - - self.categories = { - "general": "Test your general knowledge.", - "retro": "Questions related to retro gaming.", - "math": "General questions about mathematics ranging from grade 8 to grade 12.", - "science": "Put your understanding of science to the test!", - "cs": "A large variety of computer science questions.", - "python": "Trivia on our amazing language, Python!", - } - - @staticmethod - def load_questions() -> dict: - """Load the questions from the JSON file.""" - p = Path("bot", "resources", "evergreen", "trivia_quiz.json") - - return json.loads(p.read_text(encoding="utf-8")) - - @commands.group(name="quiz", aliases=["trivia"], invoke_without_command=True) - async def quiz_game(self, ctx: commands.Context, category: Optional[str], questions: Optional[int]) -> None: - """ - Start a quiz! - - Questions for the quiz can be selected from the following categories: - - general: Test your general knowledge. - - retro: Questions related to retro gaming. - - math: General questions about mathematics ranging from grade 8 to grade 12. - - science: Put your understanding of science to the test! - - cs: A large variety of computer science questions. - - python: Trivia on our amazing language, Python! - - (More to come!) - """ - if ctx.channel.id not in self.game_status: - self.game_status[ctx.channel.id] = False - - if ctx.channel.id not in self.game_player_scores: - self.game_player_scores[ctx.channel.id] = {} - - # Stop game if running. - if self.game_status[ctx.channel.id]: - await ctx.send( - "Game is already running... " - f"do `{self.bot.command_prefix}quiz stop`" - ) - return - - # Send embed showing available categories if inputted category is invalid. - if category is None: - category = random.choice(list(self.categories)) - - category = category.lower() - if category not in self.categories: - embed = self.category_embed() - await ctx.send(embed=embed) - return - - topic = self.questions[category] - topic_length = len(topic) - - if questions is None: - self.question_limit = DEFAULT_QUESTION_LIMIT - else: - if questions > topic_length: - await ctx.send( - embed=self.make_error_embed( - f"This category only has {topic_length} questions. " - "Please input a lower value!" - ) - ) - return - - elif questions < 1: - await ctx.send( - embed=self.make_error_embed( - "You must choose to complete at least one question. " - f"(or enter nothing for the default value of {DEFAULT_QUESTION_LIMIT + 1} questions)" - ) - ) - return - - else: - self.question_limit = questions - 1 - - # Start game if not running. - if not self.game_status[ctx.channel.id]: - self.game_owners[ctx.channel.id] = ctx.author - self.game_status[ctx.channel.id] = True - start_embed = self.make_start_embed(category) - - await ctx.send(embed=start_embed) # send an embed with the rules - await asyncio.sleep(5) - - done_question = [] - hint_no = 0 - answers = None - - while self.game_status[ctx.channel.id]: - # Exit quiz if number of questions for a round are already sent. - if len(done_question) > self.question_limit and hint_no == 0: - await ctx.send("The round has ended.") - await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id]) - - self.game_status[ctx.channel.id] = False - del self.game_owners[ctx.channel.id] - self.game_player_scores[ctx.channel.id] = {} - - break - - # If no hint has been sent or any time alert. Basically if hint_no = 0 means it is a new question. - if hint_no == 0: - # Select a random question which has not been used yet. - while True: - question_dict = random.choice(topic) - if question_dict["id"] not in done_question: - done_question.append(question_dict["id"]) - break - - if "dynamic_id" not in question_dict: - question = question_dict["question"] - answers = question_dict["answer"].split(", ") - - var_tol = STANDARD_VARIATION_TOLERANCE - else: - format_func = DYNAMIC_QUESTIONS_FORMAT_FUNCS[question_dict["dynamic_id"]] - - quiz_entry = format_func( - question_dict["question"], - question_dict["answer"], - ) - - question, answers = quiz_entry.question, quiz_entry.answer - answers = [answers] - - var_tol = DYNAMICALLY_GEN_VARIATION_TOLERANCE - - embed = discord.Embed( - colour=Colours.gold, - title=f"Question #{len(done_question)}", - description=question, - ) - - if img_url := question_dict.get("img_url"): - embed.set_image(url=img_url) - - await ctx.send(embed=embed) - - def check_func(variation_tolerance: int) -> Callable[[discord.Message], bool]: - def contains_correct_answer(m: discord.Message) -> bool: - return m.channel == ctx.channel and any( - fuzz.ratio(answer.lower(), m.content.lower()) > variation_tolerance - for answer in answers - ) - - return contains_correct_answer - - try: - msg = await self.bot.wait_for("message", check=check_func(var_tol), timeout=10) - except asyncio.TimeoutError: - # In case of TimeoutError and the game has been stopped, then do nothing. - if not self.game_status[ctx.channel.id]: - break - - if hint_no < 2: - hint_no += 1 - - if "hints" in question_dict: - hints = question_dict["hints"] - - await ctx.send(f"**Hint #{hint_no}\n**{hints[hint_no - 1]}") - else: - await ctx.send(f"{30 - hint_no * 10}s left!") - - # Once hint or time alerts has been sent 2 times, the hint_no value will be 3 - # If hint_no > 2, then it means that all hints/time alerts have been sent. - # Also means that the answer is not yet given and the bot sends the answer and the next question. - else: - if self.game_status[ctx.channel.id] is False: - break - - response = random.choice(WRONG_ANS_RESPONSE) - await ctx.send(response) - - await self.send_answer( - ctx.channel, - answers, - False, - question_dict, - self.question_limit - len(done_question) + 1, - ) - await asyncio.sleep(1) - - hint_no = 0 # Reset the hint counter so that on the next round, it's in the initial state - - await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id]) - await asyncio.sleep(2) - else: - if self.game_status[ctx.channel.id] is False: - break - - points = 100 - 25 * hint_no - if msg.author in self.game_player_scores[ctx.channel.id]: - self.game_player_scores[ctx.channel.id][msg.author] += points - else: - self.game_player_scores[ctx.channel.id][msg.author] = points - - # Also updating the overall scoreboard. - if msg.author in self.player_scores: - self.player_scores[msg.author] += points - else: - self.player_scores[msg.author] = points - - hint_no = 0 - - await ctx.send(f"{msg.author.mention} got the correct answer :tada: {points} points!") - - await self.send_answer( - ctx.channel, - answers, - True, - question_dict, - self.question_limit - len(done_question) + 1, - ) - await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id]) - - await asyncio.sleep(2) - - def make_start_embed(self, category: str) -> discord.Embed: - """Generate a starting/introduction embed for the quiz.""" - start_embed = discord.Embed( - colour=Colours.blue, - title="A quiz game is starting!", - description=( - f"This game consists of {self.question_limit + 1} questions.\n\n" - "**Rules: **\n" - "1. Only enclose your answer in backticks when the question tells you to.\n" - "2. If the question specifies an answer format, follow it or else it won't be accepted.\n" - "3. You have 30s per question. Points for each question reduces by 25 after 10s or after a hint.\n" - "4. No cheating and have fun!\n\n" - f"**Category**: {category}" - ), - ) - - return start_embed - - @staticmethod - def make_error_embed(desc: str) -> discord.Embed: - """Generate an error embed with the given description.""" - error_embed = discord.Embed( - colour=Colours.soft_red, - title=random.choice(NEGATIVE_REPLIES), - description=desc, - ) - - return error_embed - - @quiz_game.command(name="stop") - async def stop_quiz(self, ctx: commands.Context) -> None: - """ - Stop a quiz game if its running in the channel. - - Note: Only mods or the owner of the quiz can stop it. - """ - try: - if self.game_status[ctx.channel.id]: - # Check if the author is the game starter or a moderator. - if ctx.author == self.game_owners[ctx.channel.id] or any( - Roles.moderator == role.id for role in ctx.author.roles - ): - self.game_status[ctx.channel.id] = False - del self.game_owners[ctx.channel.id] - self.game_player_scores[ctx.channel.id] = {} - - await ctx.send("Quiz stopped.") - await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id]) - - else: - await ctx.send(f"{ctx.author.mention}, you are not authorised to stop this game :ghost:!") - else: - await ctx.send("No quiz running.") - except KeyError: - await ctx.send("No quiz running.") - - @quiz_game.command(name="leaderboard") - async def leaderboard(self, ctx: commands.Context) -> None: - """View everyone's score for this bot session.""" - await self.send_score(ctx.channel, self.player_scores) - - @staticmethod - async def send_score(channel: discord.TextChannel, player_data: dict) -> None: - """Send the current scores of players in the game channel.""" - if len(player_data) == 0: - await channel.send("No one has made it onto the leaderboard yet.") - return - - embed = discord.Embed( - colour=Colours.blue, - title="Score Board", - description="", - ) - - sorted_dict = sorted(player_data.items(), key=operator.itemgetter(1), reverse=True) - for item in sorted_dict: - embed.description += f"{item[0]}: {item[1]}\n" - - await channel.send(embed=embed) - - @staticmethod - async def declare_winner(channel: discord.TextChannel, player_data: dict) -> None: - """Announce the winner of the quiz in the game channel.""" - if player_data: - highest_points = max(list(player_data.values())) - no_of_winners = list(player_data.values()).count(highest_points) - - # Check if more than 1 player has highest points. - if no_of_winners > 1: - winners = [] - points_copy = list(player_data.values()).copy() - - for _ in range(no_of_winners): - index = points_copy.index(highest_points) - winners.append(list(player_data.keys())[index]) - points_copy[index] = 0 - - winners_mention = " ".join(winner.mention for winner in winners) - else: - author_index = list(player_data.values()).index(highest_points) - winner = list(player_data.keys())[author_index] - winners_mention = winner.mention - - await channel.send( - f"Congratulations {winners_mention} :tada: " - f"You have won this quiz game with a grand total of {highest_points} points!" - ) - - def category_embed(self) -> discord.Embed: - """Build an embed showing all available trivia categories.""" - embed = discord.Embed( - colour=Colours.blue, - title="The available question categories are:", - description="", - ) - - embed.set_footer(text="If a category is not chosen, a random one will be selected.") - - for cat, description in self.categories.items(): - embed.description += ( - f"**- {cat.capitalize()}**\n" - f"{description.capitalize()}\n" - ) - - return embed - - @staticmethod - async def send_answer( - channel: discord.TextChannel, - answers: list[str], - answer_is_correct: bool, - question_dict: dict, - q_left: int, - ) -> None: - """Send the correct answer of a question to the game channel.""" - info = question_dict.get("info") - - plurality = " is" if len(answers) == 1 else "s are" - - embed = discord.Embed( - color=Colours.bright_green, - title=( - ("You got it! " if answer_is_correct else "") - + f"The correct answer{plurality} **`{', '.join(answers)}`**\n" - ), - description="", - ) - - if info is not None: - embed.description += f"**Information**\n{info}\n\n" - - embed.description += ( - ("Let's move to the next question." if q_left > 0 else "") - + f"\nRemaining questions: {q_left}" - ) - await channel.send(embed=embed) - - -def setup(bot: Bot) -> None: - """Load the TriviaQuiz cog.""" - bot.add_cog(TriviaQuiz(bot)) diff --git a/bot/exts/evergreen/wonder_twins.py b/bot/exts/evergreen/wonder_twins.py deleted file mode 100644 index 40edf785..00000000 --- a/bot/exts/evergreen/wonder_twins.py +++ /dev/null @@ -1,49 +0,0 @@ -import random -from pathlib import Path - -import yaml -from discord.ext.commands import Cog, Context, command - -from bot.bot import Bot - - -class WonderTwins(Cog): - """Cog for a Wonder Twins inspired command.""" - - def __init__(self): - with open(Path.cwd() / "bot" / "resources" / "evergreen" / "wonder_twins.yaml", "r", encoding="utf-8") as f: - info = yaml.load(f, Loader=yaml.FullLoader) - self.water_types = info["water_types"] - self.objects = info["objects"] - self.adjectives = info["adjectives"] - - @staticmethod - def append_onto(phrase: str, insert_word: str) -> str: - """Appends one word onto the end of another phrase in order to format with the proper determiner.""" - if insert_word.endswith("s"): - phrase = phrase.split() - del phrase[0] - phrase = " ".join(phrase) - - insert_word = insert_word.split()[-1] - return " ".join([phrase, insert_word]) - - def format_phrase(self) -> str: - """Creates a transformation phrase from available words.""" - adjective = random.choice((None, random.choice(self.adjectives))) - object_name = random.choice(self.objects) - water_type = random.choice(self.water_types) - - if adjective: - object_name = self.append_onto(adjective, object_name) - return f"{object_name} of {water_type}" - - @command(name="formof", aliases=("wondertwins", "wondertwin", "fo")) - async def form_of(self, ctx: Context) -> None: - """Command to send a Wonder Twins inspired phrase to the user invoking the command.""" - await ctx.send(f"Form of {self.format_phrase()}!") - - -def setup(bot: Bot) -> None: - """Load the WonderTwins cog.""" - bot.add_cog(WonderTwins()) diff --git a/bot/exts/evergreen/xkcd.py b/bot/exts/evergreen/xkcd.py deleted file mode 100644 index b56c53d9..00000000 --- a/bot/exts/evergreen/xkcd.py +++ /dev/null @@ -1,91 +0,0 @@ -import logging -import re -from random import randint -from typing import Optional, Union - -from discord import Embed -from discord.ext import tasks -from discord.ext.commands import Cog, Context, command - -from bot.bot import Bot -from bot.constants import Colours - -log = logging.getLogger(__name__) - -COMIC_FORMAT = re.compile(r"latest|[0-9]+") -BASE_URL = "https://xkcd.com" - - -class XKCD(Cog): - """Retrieving XKCD comics.""" - - def __init__(self, bot: Bot): - self.bot = bot - self.latest_comic_info: dict[str, Union[str, int]] = {} - self.get_latest_comic_info.start() - - def cog_unload(self) -> None: - """Cancels refreshing of the task for refreshing the most recent comic info.""" - self.get_latest_comic_info.cancel() - - @tasks.loop(minutes=30) - async def get_latest_comic_info(self) -> None: - """Refreshes latest comic's information ever 30 minutes. Also used for finding a random comic.""" - async with self.bot.http_session.get(f"{BASE_URL}/info.0.json") as resp: - if resp.status == 200: - self.latest_comic_info = await resp.json() - else: - log.debug(f"Failed to get latest XKCD comic information. Status code {resp.status}") - - @command(name="xkcd") - async def fetch_xkcd_comics(self, ctx: Context, comic: Optional[str]) -> None: - """ - Getting an xkcd comic's information along with the image. - - To get a random comic, don't type any number as an argument. To get the latest, type 'latest'. - """ - embed = Embed(title=f"XKCD comic '{comic}'") - - embed.colour = Colours.soft_red - - if comic and (comic := re.match(COMIC_FORMAT, comic)) is None: - embed.description = "Comic parameter should either be an integer or 'latest'." - await ctx.send(embed=embed) - return - - comic = randint(1, self.latest_comic_info["num"]) if comic is None else comic.group(0) - - if comic == "latest": - info = self.latest_comic_info - else: - async with self.bot.http_session.get(f"{BASE_URL}/{comic}/info.0.json") as resp: - if resp.status == 200: - info = await resp.json() - else: - embed.title = f"XKCD comic #{comic}" - embed.description = f"{resp.status}: Could not retrieve xkcd comic #{comic}." - log.debug(f"Retrieving xkcd comic #{comic} failed with status code {resp.status}.") - await ctx.send(embed=embed) - return - - embed.title = f"XKCD comic #{info['num']}" - embed.description = info["alt"] - embed.url = f"{BASE_URL}/{info['num']}" - - if info["img"][-3:] in ("jpg", "png", "gif"): - embed.set_image(url=info["img"]) - date = f"{info['year']}/{info['month']}/{info['day']}" - embed.set_footer(text=f"{date} - #{info['num']}, \'{info['safe_title']}\'") - embed.colour = Colours.soft_green - else: - embed.description = ( - "The selected comic is interactive, and cannot be displayed within an embed.\n" - f"Comic can be viewed [here](https://xkcd.com/{info['num']})." - ) - - await ctx.send(embed=embed) - - -def setup(bot: Bot) -> None: - """Load the XKCD cog.""" - bot.add_cog(XKCD(bot)) diff --git a/bot/exts/fun/__init__.py b/bot/exts/fun/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bot/exts/fun/battleship.py b/bot/exts/fun/battleship.py new file mode 100644 index 00000000..f4351954 --- /dev/null +++ b/bot/exts/fun/battleship.py @@ -0,0 +1,448 @@ +import asyncio +import logging +import random +import re +from dataclasses import dataclass +from functools import partial +from typing import Optional + +import discord +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Colours + +log = logging.getLogger(__name__) + + +@dataclass +class Square: + """Each square on the battleship grid - if they contain a boat and if they've been aimed at.""" + + boat: Optional[str] + aimed: bool + + +Grid = list[list[Square]] +EmojiSet = dict[tuple[bool, bool], str] + + +@dataclass +class Player: + """Each player in the game - their messages for the boards and their current grid.""" + + user: Optional[discord.Member] + board: Optional[discord.Message] + opponent_board: discord.Message + grid: Grid + + +# The name of the ship and its size +SHIPS = { + "Carrier": 5, + "Battleship": 4, + "Cruiser": 3, + "Submarine": 3, + "Destroyer": 2, +} + + +# For these two variables, the first boolean is whether the square is a ship (True) or not (False). +# The second boolean is whether the player has aimed for that square (True) or not (False) + +# This is for the player's own board which shows the location of their own ships. +SHIP_EMOJIS = { + (True, True): ":fire:", + (True, False): ":ship:", + (False, True): ":anger:", + (False, False): ":ocean:", +} + +# This is for the opposing player's board which only shows aimed locations. +HIDDEN_EMOJIS = { + (True, True): ":red_circle:", + (True, False): ":black_circle:", + (False, True): ":white_circle:", + (False, False): ":black_circle:", +} + +# For the top row of the board +LETTERS = ( + ":stop_button::regional_indicator_a::regional_indicator_b::regional_indicator_c::regional_indicator_d:" + ":regional_indicator_e::regional_indicator_f::regional_indicator_g::regional_indicator_h:" + ":regional_indicator_i::regional_indicator_j:" +) + +# For the first column of the board +NUMBERS = [ + ":one:", + ":two:", + ":three:", + ":four:", + ":five:", + ":six:", + ":seven:", + ":eight:", + ":nine:", + ":keycap_ten:", +] + +CROSS_EMOJI = "\u274e" +HAND_RAISED_EMOJI = "\U0001f64b" + + +class Game: + """A Battleship Game.""" + + def __init__( + self, + bot: Bot, + channel: discord.TextChannel, + player1: discord.Member, + player2: discord.Member + ): + + self.bot = bot + self.public_channel = channel + + self.p1 = Player(player1, None, None, self.generate_grid()) + self.p2 = Player(player2, None, None, self.generate_grid()) + + self.gameover: bool = False + + self.turn: Optional[discord.Member] = None + self.next: Optional[discord.Member] = None + + self.match: Optional[re.Match] = None + self.surrender: bool = False + + self.setup_grids() + + @staticmethod + def generate_grid() -> Grid: + """Generates a grid by instantiating the Squares.""" + return [[Square(None, False) for _ in range(10)] for _ in range(10)] + + @staticmethod + def format_grid(player: Player, emojiset: EmojiSet) -> str: + """ + Gets and formats the grid as a list into a string to be output to the DM. + + Also adds the Letter and Number indexes. + """ + grid = [ + [emojiset[bool(square.boat), square.aimed] for square in row] + for row in player.grid + ] + + rows = ["".join([number] + row) for number, row in zip(NUMBERS, grid)] + return "\n".join([LETTERS] + rows) + + @staticmethod + def get_square(grid: Grid, square: str) -> Square: + """Grabs a square from a grid with an inputted key.""" + index = ord(square[0].upper()) - ord("A") + number = int(square[1:]) + + return grid[number-1][index] # -1 since lists are indexed from 0 + + async def game_over( + self, + *, + winner: discord.Member, + loser: discord.Member + ) -> None: + """Removes games from list of current games and announces to public chat.""" + await self.public_channel.send(f"Game Over! {winner.mention} won against {loser.mention}") + + for player in (self.p1, self.p2): + grid = self.format_grid(player, SHIP_EMOJIS) + await self.public_channel.send(f"{player.user}'s Board:\n{grid}") + + @staticmethod + def check_sink(grid: Grid, boat: str) -> bool: + """Checks if all squares containing a given boat have sunk.""" + return all(square.aimed for row in grid for square in row if square.boat == boat) + + @staticmethod + def check_gameover(grid: Grid) -> bool: + """Checks if all boats have been sunk.""" + return all(square.aimed for row in grid for square in row if square.boat) + + def setup_grids(self) -> None: + """Places the boats on the grids to initialise the game.""" + for player in (self.p1, self.p2): + for name, size in SHIPS.items(): + while True: # Repeats if about to overwrite another boat + ship_collision = False + coords = [] + + coord1 = random.randint(0, 9) + coord2 = random.randint(0, 10 - size) + + if random.choice((True, False)): # Vertical or Horizontal + x, y = coord1, coord2 + xincr, yincr = 0, 1 + else: + x, y = coord2, coord1 + xincr, yincr = 1, 0 + + for i in range(size): + new_x = x + (xincr * i) + new_y = y + (yincr * i) + if player.grid[new_x][new_y].boat: # Check if there's already a boat + ship_collision = True + break + coords.append((new_x, new_y)) + if not ship_collision: # If not overwriting any other boat spaces, break loop + break + + for x, y in coords: + player.grid[x][y].boat = name + + async def print_grids(self) -> None: + """Prints grids to the DM channels.""" + # Convert squares into Emoji + + boards = [ + self.format_grid(player, emojiset) + for emojiset in (HIDDEN_EMOJIS, SHIP_EMOJIS) + for player in (self.p1, self.p2) + ] + + locations = ( + (self.p2, "opponent_board"), (self.p1, "opponent_board"), + (self.p1, "board"), (self.p2, "board") + ) + + for board, location in zip(boards, locations): + player, attr = location + if getattr(player, attr): + await getattr(player, attr).edit(content=board) + else: + setattr(player, attr, await player.user.send(board)) + + def predicate(self, message: discord.Message) -> bool: + """Predicate checking the message typed for each turn.""" + if message.author == self.turn.user and message.channel == self.turn.user.dm_channel: + if message.content.lower() == "surrender": + self.surrender = True + return True + self.match = re.fullmatch("([A-J]|[a-j]) ?((10)|[1-9])", message.content.strip()) + if not self.match: + self.bot.loop.create_task(message.add_reaction(CROSS_EMOJI)) + return bool(self.match) + + async def take_turn(self) -> Optional[Square]: + """Lets the player who's turn it is choose a square.""" + square = None + turn_message = await self.turn.user.send( + "It's your turn! Type the square you want to fire at. Format it like this: A1\n" + "Type `surrender` to give up." + ) + await self.next.user.send("Their turn", delete_after=3.0) + while True: + try: + await self.bot.wait_for("message", check=self.predicate, timeout=60.0) + except asyncio.TimeoutError: + await self.turn.user.send("You took too long. Game over!") + await self.next.user.send(f"{self.turn.user} took too long. Game over!") + await self.public_channel.send( + f"Game over! {self.turn.user.mention} timed out so {self.next.user.mention} wins!" + ) + self.gameover = True + break + else: + if self.surrender: + await self.next.user.send(f"{self.turn.user} surrendered. Game over!") + await self.public_channel.send( + f"Game over! {self.turn.user.mention} surrendered to {self.next.user.mention}!" + ) + self.gameover = True + break + square = self.get_square(self.next.grid, self.match.string) + if square.aimed: + await self.turn.user.send("You've already aimed at this square!", delete_after=3.0) + else: + break + await turn_message.delete() + return square + + async def hit(self, square: Square, alert_messages: list[discord.Message]) -> None: + """Occurs when a player successfully aims for a ship.""" + await self.turn.user.send("Hit!", delete_after=3.0) + alert_messages.append(await self.next.user.send("Hit!")) + if self.check_sink(self.next.grid, square.boat): + await self.turn.user.send(f"You've sunk their {square.boat} ship!", delete_after=3.0) + alert_messages.append(await self.next.user.send(f"Oh no! Your {square.boat} ship sunk!")) + if self.check_gameover(self.next.grid): + await self.turn.user.send("You win!") + await self.next.user.send("You lose!") + self.gameover = True + await self.game_over(winner=self.turn.user, loser=self.next.user) + + async def start_game(self) -> None: + """Begins the game.""" + await self.p1.user.send(f"You're playing battleship with {self.p2.user}.") + await self.p2.user.send(f"You're playing battleship with {self.p1.user}.") + + alert_messages = [] + + self.turn = self.p1 + self.next = self.p2 + + while True: + await self.print_grids() + + if self.gameover: + return + + square = await self.take_turn() + if not square: + return + square.aimed = True + + for message in alert_messages: + await message.delete() + + alert_messages = [] + alert_messages.append(await self.next.user.send(f"{self.turn.user} aimed at {self.match.string}!")) + + if square.boat: + await self.hit(square, alert_messages) + if self.gameover: + return + else: + await self.turn.user.send("Miss!", delete_after=3.0) + alert_messages.append(await self.next.user.send("Miss!")) + + self.turn, self.next = self.next, self.turn + + +class Battleship(commands.Cog): + """Play the classic game Battleship!""" + + def __init__(self, bot: Bot): + self.bot = bot + self.games: list[Game] = [] + self.waiting: list[discord.Member] = [] + + def predicate( + self, + ctx: commands.Context, + announcement: discord.Message, + reaction: discord.Reaction, + user: discord.Member + ) -> bool: + """Predicate checking the criteria for the announcement message.""" + if self.already_playing(ctx.author): # If they've joined a game since requesting a player 2 + return True # Is dealt with later on + if ( + user.id not in (ctx.me.id, ctx.author.id) + and str(reaction.emoji) == HAND_RAISED_EMOJI + and reaction.message.id == announcement.id + ): + if self.already_playing(user): + self.bot.loop.create_task(ctx.send(f"{user.mention} You're already playing a game!")) + self.bot.loop.create_task(announcement.remove_reaction(reaction, user)) + return False + + if user in self.waiting: + self.bot.loop.create_task(ctx.send( + f"{user.mention} Please cancel your game first before joining another one." + )) + self.bot.loop.create_task(announcement.remove_reaction(reaction, user)) + return False + + return True + + if ( + user.id == ctx.author.id + and str(reaction.emoji) == CROSS_EMOJI + and reaction.message.id == announcement.id + ): + return True + return False + + def already_playing(self, player: discord.Member) -> bool: + """Check if someone is already in a game.""" + return any(player in (game.p1.user, game.p2.user) for game in self.games) + + @commands.group(invoke_without_command=True) + @commands.guild_only() + async def battleship(self, ctx: commands.Context) -> None: + """ + Play a game of Battleship with someone else! + + This will set up a message waiting for someone else to react and play along. + The game takes place entirely in DMs. + Make sure you have your DMs open so that the bot can message you. + """ + if self.already_playing(ctx.author): + await ctx.send("You're already playing a game!") + return + + if ctx.author in self.waiting: + await ctx.send("You've already sent out a request for a player 2.") + return + + announcement = await ctx.send( + "**Battleship**: A new game is about to start!\n" + f"Press {HAND_RAISED_EMOJI} to play against {ctx.author.mention}!\n" + f"(Cancel the game with {CROSS_EMOJI}.)" + ) + self.waiting.append(ctx.author) + await announcement.add_reaction(HAND_RAISED_EMOJI) + await announcement.add_reaction(CROSS_EMOJI) + + try: + reaction, user = await self.bot.wait_for( + "reaction_add", + check=partial(self.predicate, ctx, announcement), + timeout=60.0 + ) + except asyncio.TimeoutError: + self.waiting.remove(ctx.author) + await announcement.delete() + await ctx.send(f"{ctx.author.mention} Seems like there's no one here to play...") + return + + if str(reaction.emoji) == CROSS_EMOJI: + self.waiting.remove(ctx.author) + await announcement.delete() + await ctx.send(f"{ctx.author.mention} Game cancelled.") + return + + await announcement.delete() + self.waiting.remove(ctx.author) + if self.already_playing(ctx.author): + return + game = Game(self.bot, ctx.channel, ctx.author, user) + self.games.append(game) + try: + await game.start_game() + self.games.remove(game) + except discord.Forbidden: + await ctx.send( + f"{ctx.author.mention} {user.mention} " + "Game failed. This is likely due to you not having your DMs open. Check and try again." + ) + self.games.remove(game) + except Exception: + # End the game in the event of an unforseen error so the players aren't stuck in a game + await ctx.send(f"{ctx.author.mention} {user.mention} An error occurred. Game failed.") + self.games.remove(game) + raise + + @battleship.command(name="ships", aliases=("boats",)) + async def battleship_ships(self, ctx: commands.Context) -> None: + """Lists the ships that are found on the battleship grid.""" + embed = discord.Embed(colour=Colours.blue) + embed.add_field(name="Name", value="\n".join(SHIPS)) + embed.add_field(name="Size", value="\n".join(str(size) for size in SHIPS.values())) + await ctx.send(embed=embed) + + +def setup(bot: Bot) -> None: + """Load the Battleship Cog.""" + bot.add_cog(Battleship(bot)) diff --git a/bot/exts/fun/catify.py b/bot/exts/fun/catify.py new file mode 100644 index 00000000..32dfae09 --- /dev/null +++ b/bot/exts/fun/catify.py @@ -0,0 +1,86 @@ +import random +from contextlib import suppress +from typing import Optional + +from discord import AllowedMentions, Embed, Forbidden +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Cats, Colours, NEGATIVE_REPLIES +from bot.utils import helpers + + +class Catify(commands.Cog): + """Cog for the catify command.""" + + @commands.command(aliases=("ᓚᘏᗢify", "ᓚᘏᗢ")) + @commands.cooldown(1, 5, commands.BucketType.user) + async def catify(self, ctx: commands.Context, *, text: Optional[str]) -> None: + """ + Convert the provided text into a cat themed sentence by interspercing cats throughout text. + + If no text is given then the users nickname is edited. + """ + if not text: + display_name = ctx.author.display_name + + if len(display_name) > 26: + embed = Embed( + title=random.choice(NEGATIVE_REPLIES), + description=( + "Your display name is too long to be catified! " + "Please change it to be under 26 characters." + ), + color=Colours.soft_red + ) + await ctx.send(embed=embed) + return + + else: + display_name += f" | {random.choice(Cats.cats)}" + + await ctx.send(f"Your catified nickname is: `{display_name}`", allowed_mentions=AllowedMentions.none()) + + with suppress(Forbidden): + await ctx.author.edit(nick=display_name) + else: + if len(text) >= 1500: + embed = Embed( + title=random.choice(NEGATIVE_REPLIES), + description="Submitted text was too large! Please submit something under 1500 characters.", + color=Colours.soft_red + ) + await ctx.send(embed=embed) + return + + string_list = text.split() + for index, name in enumerate(string_list): + name = name.lower() + if "cat" in name: + if random.randint(0, 5) == 5: + string_list[index] = name.replace("cat", f"**{random.choice(Cats.cats)}**") + else: + string_list[index] = name.replace("cat", random.choice(Cats.cats)) + for element in Cats.cats: + if element in name: + string_list[index] = name.replace(element, "cat") + + string_len = len(string_list) // 3 or len(string_list) + + for _ in range(random.randint(1, string_len)): + # insert cat at random index + if random.randint(0, 5) == 5: + string_list.insert(random.randint(0, len(string_list)), f"**{random.choice(Cats.cats)}**") + else: + string_list.insert(random.randint(0, len(string_list)), random.choice(Cats.cats)) + + text = helpers.suppress_links(" ".join(string_list)) + await ctx.send( + f">>> {text}", + allowed_mentions=AllowedMentions.none() + ) + + +def setup(bot: Bot) -> None: + """Loads the catify cog.""" + bot.add_cog(Catify()) diff --git a/bot/exts/fun/coinflip.py b/bot/exts/fun/coinflip.py new file mode 100644 index 00000000..804306bd --- /dev/null +++ b/bot/exts/fun/coinflip.py @@ -0,0 +1,53 @@ +import random + +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Emojis + + +class CoinSide(commands.Converter): + """Class used to convert the `side` parameter of coinflip command.""" + + HEADS = ("h", "head", "heads") + TAILS = ("t", "tail", "tails") + + async def convert(self, ctx: commands.Context, side: str) -> str: + """Converts the provided `side` into the corresponding string.""" + side = side.lower() + if side in self.HEADS: + return "heads" + + if side in self.TAILS: + return "tails" + + raise commands.BadArgument(f"{side!r} is not a valid coin side.") + + +class CoinFlip(commands.Cog): + """Cog for the CoinFlip command.""" + + @commands.command(name="coinflip", aliases=("flip", "coin", "cf")) + async def coinflip_command(self, ctx: commands.Context, side: CoinSide = None) -> None: + """ + Flips a coin. + + If `side` is provided will state whether you guessed the side correctly. + """ + flipped_side = random.choice(["heads", "tails"]) + + message = f"{ctx.author.mention} flipped **{flipped_side}**. " + if not side: + await ctx.send(message) + return + + if side == flipped_side: + message += f"You guessed correctly! {Emojis.lemon_hyperpleased}" + else: + message += f"You guessed incorrectly. {Emojis.lemon_pensive}" + await ctx.send(message) + + +def setup(bot: Bot) -> None: + """Loads the coinflip cog.""" + bot.add_cog(CoinFlip()) diff --git a/bot/exts/fun/connect_four.py b/bot/exts/fun/connect_four.py new file mode 100644 index 00000000..647bb2b7 --- /dev/null +++ b/bot/exts/fun/connect_four.py @@ -0,0 +1,452 @@ +import asyncio +import random +from functools import partial +from typing import Optional, Union + +import discord +import emojis +from discord.ext import commands +from discord.ext.commands import guild_only + +from bot.bot import Bot +from bot.constants import Emojis + +NUMBERS = list(Emojis.number_emojis.values()) +CROSS_EMOJI = Emojis.incident_unactioned + +Coordinate = Optional[tuple[int, int]] +EMOJI_CHECK = Union[discord.Emoji, str] + + +class Game: + """A Connect 4 Game.""" + + def __init__( + self, + bot: Bot, + channel: discord.TextChannel, + player1: discord.Member, + player2: Optional[discord.Member], + tokens: list[str], + size: int = 7 + ): + self.bot = bot + self.channel = channel + self.player1 = player1 + self.player2 = player2 or AI(self.bot, game=self) + self.tokens = tokens + + self.grid = self.generate_board(size) + self.grid_size = size + + self.unicode_numbers = NUMBERS[:self.grid_size] + + self.message = None + + self.player_active = None + self.player_inactive = None + + @staticmethod + def generate_board(size: int) -> list[list[int]]: + """Generate the connect 4 board.""" + return [[0 for _ in range(size)] for _ in range(size)] + + async def print_grid(self) -> None: + """Formats and outputs the Connect Four grid to the channel.""" + title = ( + f"Connect 4: {self.player1.display_name}" + f" VS {self.bot.user.display_name if isinstance(self.player2, AI) else self.player2.display_name}" + ) + + rows = [" ".join(self.tokens[s] for s in row) for row in self.grid] + first_row = " ".join(x for x in NUMBERS[:self.grid_size]) + formatted_grid = "\n".join([first_row] + rows) + embed = discord.Embed(title=title, description=formatted_grid) + + if self.message: + await self.message.edit(embed=embed) + else: + self.message = await self.channel.send(content="Loading...") + for emoji in self.unicode_numbers: + await self.message.add_reaction(emoji) + await self.message.add_reaction(CROSS_EMOJI) + await self.message.edit(content=None, embed=embed) + + async def game_over(self, action: str, player1: discord.user, player2: discord.user) -> None: + """Announces to public chat.""" + if action == "win": + await self.channel.send(f"Game Over! {player1.mention} won against {player2.mention}") + elif action == "draw": + await self.channel.send(f"Game Over! {player1.mention} {player2.mention} It's A Draw :tada:") + elif action == "quit": + await self.channel.send(f"{self.player1.mention} surrendered. Game over!") + await self.print_grid() + + async def start_game(self) -> None: + """Begins the game.""" + self.player_active, self.player_inactive = self.player1, self.player2 + + while True: + await self.print_grid() + + if isinstance(self.player_active, AI): + coords = self.player_active.play() + if not coords: + await self.game_over( + "draw", + self.bot.user if isinstance(self.player_active, AI) else self.player_active, + self.bot.user if isinstance(self.player_inactive, AI) else self.player_inactive, + ) + else: + coords = await self.player_turn() + + if not coords: + return + + if self.check_win(coords, 1 if self.player_active == self.player1 else 2): + await self.game_over( + "win", + self.bot.user if isinstance(self.player_active, AI) else self.player_active, + self.bot.user if isinstance(self.player_inactive, AI) else self.player_inactive, + ) + return + + self.player_active, self.player_inactive = self.player_inactive, self.player_active + + def predicate(self, reaction: discord.Reaction, user: discord.Member) -> bool: + """The predicate to check for the player's reaction.""" + return ( + reaction.message.id == self.message.id + and user.id == self.player_active.id + and str(reaction.emoji) in (*self.unicode_numbers, CROSS_EMOJI) + ) + + async def player_turn(self) -> Coordinate: + """Initiate the player's turn.""" + message = await self.channel.send( + f"{self.player_active.mention}, it's your turn! React with the column you want to place your token in." + ) + player_num = 1 if self.player_active == self.player1 else 2 + while True: + try: + reaction, user = await self.bot.wait_for("reaction_add", check=self.predicate, timeout=30.0) + except asyncio.TimeoutError: + await self.channel.send(f"{self.player_active.mention}, you took too long. Game over!") + return + else: + await message.delete() + if str(reaction.emoji) == CROSS_EMOJI: + await self.game_over("quit", self.player_active, self.player_inactive) + return + + await self.message.remove_reaction(reaction, user) + + column_num = self.unicode_numbers.index(str(reaction.emoji)) + column = [row[column_num] for row in self.grid] + + for row_num, square in reversed(list(enumerate(column))): + if not square: + self.grid[row_num][column_num] = player_num + return row_num, column_num + message = await self.channel.send(f"Column {column_num + 1} is full. Try again") + + def check_win(self, coords: Coordinate, player_num: int) -> bool: + """Check that placing a counter here would cause the player to win.""" + vertical = [(-1, 0), (1, 0)] + horizontal = [(0, 1), (0, -1)] + forward_diag = [(-1, 1), (1, -1)] + backward_diag = [(-1, -1), (1, 1)] + axes = [vertical, horizontal, forward_diag, backward_diag] + + for axis in axes: + counters_in_a_row = 1 # The initial counter that is compared to + for (row_incr, column_incr) in axis: + row, column = coords + row += row_incr + column += column_incr + + while 0 <= row < self.grid_size and 0 <= column < self.grid_size: + if self.grid[row][column] == player_num: + counters_in_a_row += 1 + row += row_incr + column += column_incr + else: + break + if counters_in_a_row >= 4: + return True + return False + + +class AI: + """The Computer Player for Single-Player games.""" + + def __init__(self, bot: Bot, game: Game): + self.game = game + self.mention = bot.user.mention + + def get_possible_places(self) -> list[Coordinate]: + """Gets all the coordinates where the AI could possibly place a counter.""" + possible_coords = [] + for column_num in range(self.game.grid_size): + column = [row[column_num] for row in self.game.grid] + for row_num, square in reversed(list(enumerate(column))): + if not square: + possible_coords.append((row_num, column_num)) + break + return possible_coords + + def check_ai_win(self, coord_list: list[Coordinate]) -> Optional[Coordinate]: + """ + Check AI win. + + Check if placing a counter in any possible coordinate would cause the AI to win + with 10% chance of not winning and returning None + """ + if random.randint(1, 10) == 1: + return + for coords in coord_list: + if self.game.check_win(coords, 2): + return coords + + def check_player_win(self, coord_list: list[Coordinate]) -> Optional[Coordinate]: + """ + Check Player win. + + Check if placing a counter in possible coordinates would stop the player + from winning with 25% of not blocking them and returning None. + """ + if random.randint(1, 4) == 1: + return + for coords in coord_list: + if self.game.check_win(coords, 1): + return coords + + @staticmethod + def random_coords(coord_list: list[Coordinate]) -> Coordinate: + """Picks a random coordinate from the possible ones.""" + return random.choice(coord_list) + + def play(self) -> Union[Coordinate, bool]: + """ + Plays for the AI. + + Gets all possible coords, and determins the move: + 1. coords where it can win. + 2. coords where the player can win. + 3. Random coord + The first possible value is choosen. + """ + possible_coords = self.get_possible_places() + + if not possible_coords: + return False + + coords = ( + self.check_ai_win(possible_coords) + or self.check_player_win(possible_coords) + or self.random_coords(possible_coords) + ) + + row, column = coords + self.game.grid[row][column] = 2 + return coords + + +class ConnectFour(commands.Cog): + """Connect Four. The Classic Vertical Four-in-a-row Game!""" + + def __init__(self, bot: Bot): + self.bot = bot + self.games: list[Game] = [] + self.waiting: list[discord.Member] = [] + + self.tokens = [":white_circle:", ":blue_circle:", ":red_circle:"] + + self.max_board_size = 9 + self.min_board_size = 5 + + async def check_author(self, ctx: commands.Context, board_size: int) -> bool: + """Check if the requester is free and the board size is correct.""" + if self.already_playing(ctx.author): + await ctx.send("You're already playing a game!") + return False + + if ctx.author in self.waiting: + await ctx.send("You've already sent out a request for a player 2") + return False + + if not self.min_board_size <= board_size <= self.max_board_size: + await ctx.send( + f"{board_size} is not a valid board size. A valid board size is " + f"between `{self.min_board_size}` and `{self.max_board_size}`." + ) + return False + + return True + + def get_player( + self, + ctx: commands.Context, + announcement: discord.Message, + reaction: discord.Reaction, + user: discord.Member + ) -> bool: + """Predicate checking the criteria for the announcement message.""" + if self.already_playing(ctx.author): # If they've joined a game since requesting a player 2 + return True # Is dealt with later on + + if ( + user.id not in (ctx.me.id, ctx.author.id) + and str(reaction.emoji) == Emojis.hand_raised + and reaction.message.id == announcement.id + ): + if self.already_playing(user): + self.bot.loop.create_task(ctx.send(f"{user.mention} You're already playing a game!")) + self.bot.loop.create_task(announcement.remove_reaction(reaction, user)) + return False + + if user in self.waiting: + self.bot.loop.create_task(ctx.send( + f"{user.mention} Please cancel your game first before joining another one." + )) + self.bot.loop.create_task(announcement.remove_reaction(reaction, user)) + return False + + return True + + if ( + user.id == ctx.author.id + and str(reaction.emoji) == CROSS_EMOJI + and reaction.message.id == announcement.id + ): + return True + return False + + def already_playing(self, player: discord.Member) -> bool: + """Check if someone is already in a game.""" + return any(player in (game.player1, game.player2) for game in self.games) + + @staticmethod + def check_emojis( + e1: EMOJI_CHECK, e2: EMOJI_CHECK + ) -> tuple[bool, Optional[str]]: + """Validate the emojis, the user put.""" + if isinstance(e1, str) and emojis.count(e1) != 1: + return False, e1 + if isinstance(e2, str) and emojis.count(e2) != 1: + return False, e2 + return True, None + + async def _play_game( + self, + ctx: commands.Context, + user: Optional[discord.Member], + board_size: int, + emoji1: str, + emoji2: str + ) -> None: + """Helper for playing a game of connect four.""" + self.tokens = [":white_circle:", str(emoji1), str(emoji2)] + game = None # if game fails to intialize in try...except + + try: + game = Game(self.bot, ctx.channel, ctx.author, user, self.tokens, size=board_size) + self.games.append(game) + await game.start_game() + self.games.remove(game) + except Exception: + # End the game in the event of an unforeseen error so the players aren't stuck in a game + await ctx.send(f"{ctx.author.mention} {user.mention if user else ''} An error occurred. Game failed.") + if game in self.games: + self.games.remove(game) + raise + + @guild_only() + @commands.group( + invoke_without_command=True, + aliases=("4inarow", "connect4", "connectfour", "c4"), + case_insensitive=True + ) + async def connect_four( + self, + ctx: commands.Context, + board_size: int = 7, + emoji1: EMOJI_CHECK = "\U0001f535", + emoji2: EMOJI_CHECK = "\U0001f534" + ) -> None: + """ + Play the classic game of Connect Four with someone! + + Sets up a message waiting for someone else to react and play along. + The game will start once someone has reacted. + All inputs will be through reactions. + """ + check, emoji = self.check_emojis(emoji1, emoji2) + if not check: + raise commands.EmojiNotFound(emoji) + + check_author_result = await self.check_author(ctx, board_size) + if not check_author_result: + return + + announcement = await ctx.send( + "**Connect Four**: A new game is about to start!\n" + f"Press {Emojis.hand_raised} to play against {ctx.author.mention}!\n" + f"(Cancel the game with {CROSS_EMOJI}.)" + ) + self.waiting.append(ctx.author) + await announcement.add_reaction(Emojis.hand_raised) + await announcement.add_reaction(CROSS_EMOJI) + + try: + reaction, user = await self.bot.wait_for( + "reaction_add", + check=partial(self.get_player, ctx, announcement), + timeout=60.0 + ) + except asyncio.TimeoutError: + self.waiting.remove(ctx.author) + await announcement.delete() + await ctx.send( + f"{ctx.author.mention} Seems like there's no one here to play. " + f"Use `{ctx.prefix}{ctx.invoked_with} ai` to play against a computer." + ) + return + + if str(reaction.emoji) == CROSS_EMOJI: + self.waiting.remove(ctx.author) + await announcement.delete() + await ctx.send(f"{ctx.author.mention} Game cancelled.") + return + + await announcement.delete() + self.waiting.remove(ctx.author) + if self.already_playing(ctx.author): + return + + await self._play_game(ctx, user, board_size, str(emoji1), str(emoji2)) + + @guild_only() + @connect_four.command(aliases=("bot", "computer", "cpu")) + async def ai( + self, + ctx: commands.Context, + board_size: int = 7, + emoji1: EMOJI_CHECK = "\U0001f535", + emoji2: EMOJI_CHECK = "\U0001f534" + ) -> None: + """Play Connect Four against a computer player.""" + check, emoji = self.check_emojis(emoji1, emoji2) + if not check: + raise commands.EmojiNotFound(emoji) + + check_author_result = await self.check_author(ctx, board_size) + if not check_author_result: + return + + await self._play_game(ctx, None, board_size, str(emoji1), str(emoji2)) + + +def setup(bot: Bot) -> None: + """Load ConnectFour Cog.""" + bot.add_cog(ConnectFour(bot)) diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py new file mode 100644 index 00000000..1ef7513f --- /dev/null +++ b/bot/exts/fun/duck_game.py @@ -0,0 +1,336 @@ +import asyncio +import random +import re +from collections import defaultdict +from io import BytesIO +from itertools import product +from pathlib import Path + +import discord +from PIL import Image, ImageDraw, ImageFont +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Colours, MODERATION_ROLES +from bot.utils.decorators import with_role + +DECK = list(product(*[(0, 1, 2)]*4)) + +GAME_DURATION = 180 + +# Scoring +CORRECT_SOLN = 1 +INCORRECT_SOLN = -1 +CORRECT_GOOSE = 2 +INCORRECT_GOOSE = -1 + +# Distribution of minimum acceptable solutions at board generation. +# This is for gameplay reasons, to shift the number of solutions per board up, +# while still making the end of the game unpredictable. +# Note: this is *not* the same as the distribution of number of solutions. + +SOLN_DISTR = 0, 0.05, 0.05, 0.1, 0.15, 0.25, 0.2, 0.15, .05 + +IMAGE_PATH = Path("bot", "resources", "fun", "all_cards.png") +FONT_PATH = Path("bot", "resources", "fun", "LuckiestGuy-Regular.ttf") +HELP_IMAGE_PATH = Path("bot", "resources", "fun", "ducks_help_ex.png") + +ALL_CARDS = Image.open(IMAGE_PATH) +LABEL_FONT = ImageFont.truetype(str(FONT_PATH), size=16) +CARD_WIDTH = 155 +CARD_HEIGHT = 97 + +EMOJI_WRONG = "\u274C" + +ANSWER_REGEX = re.compile(r'^\D*(\d+)\D+(\d+)\D+(\d+)\D*$') + +HELP_TEXT = """ +**Each card has 4 features** +Color, Number, Hat, and Accessory + +**A valid flight** +3 cards where each feature is either all the same or all different + +**Call "GOOSE"** +if you think there are no more flights + +**+1** for each valid flight +**+2** for a correct "GOOSE" call +**-1** for any wrong answer + +The first flight below is invalid: the first card has swords while the other two have no accessory.\ + It would be valid if the first card was empty-handed, or one of the other two had paintbrushes. + +The second flight is valid because there are no 2:1 splits; each feature is either all the same or all different. +""" + + +def assemble_board_image(board: list[tuple[int]], rows: int, columns: int) -> Image: + """Cut and paste images representing the given cards into an image representing the board.""" + new_im = Image.new("RGBA", (CARD_WIDTH*columns, CARD_HEIGHT*rows)) + draw = ImageDraw.Draw(new_im) + for idx, card in enumerate(board): + card_image = get_card_image(card) + row, col = divmod(idx, columns) + top, left = row * CARD_HEIGHT, col * CARD_WIDTH + new_im.paste(card_image, (left, top)) + draw.text( + xy=(left+5, top+5), # magic numbers are buffers for the card labels + text=str(idx), + fill=(0, 0, 0), + font=LABEL_FONT, + ) + return new_im + + +def get_card_image(card: tuple[int]) -> Image: + """Slice the image containing all the cards to get just this card.""" + # The master card image file should have 9x9 cards, + # arranged such that their features can be interpreted as ordered trinary. + row, col = divmod(as_trinary(card), 9) + x1 = col * CARD_WIDTH + x2 = x1 + CARD_WIDTH + y1 = row * CARD_HEIGHT + y2 = y1 + CARD_HEIGHT + return ALL_CARDS.crop((x1, y1, x2, y2)) + + +def as_trinary(card: tuple[int]) -> int: + """Find the card's unique index by interpreting its features as trinary.""" + return int(''.join(str(x) for x in card), base=3) + + +class DuckGame: + """A class for a single game.""" + + def __init__( + self, + rows: int = 4, + columns: int = 3, + minimum_solutions: int = 1, + ): + """ + Take samples from the deck to generate a board. + + Args: + rows (int, optional): Rows in the game board. Defaults to 4. + columns (int, optional): Columns in the game board. Defaults to 3. + minimum_solutions (int, optional): Minimum acceptable number of solutions in the board. Defaults to 1. + """ + self.rows = rows + self.columns = columns + size = rows * columns + + self._solutions = None + self.claimed_answers = {} + self.scores = defaultdict(int) + self.editing_embed = asyncio.Lock() + + self.board = random.sample(DECK, size) + while len(self.solutions) < minimum_solutions: + self.board = random.sample(DECK, size) + + @property + def board(self) -> list[tuple[int]]: + """Accesses board property.""" + return self._board + + @board.setter + def board(self, val: list[tuple[int]]) -> None: + """Erases calculated solutions if the board changes.""" + self._solutions = None + self._board = val + + @property + def solutions(self) -> None: + """Calculate valid solutions and cache to avoid redoing work.""" + if self._solutions is None: + self._solutions = set() + for idx_a, card_a in enumerate(self.board): + for idx_b, card_b in enumerate(self.board[idx_a+1:], start=idx_a+1): + # Two points determine a line, and there are exactly 3 points per line in {0,1,2}^4. + # The completion of a line will only be a duplicate point if the other two points are the same, + # which is prevented by the triangle iteration. + completion = tuple( + feat_a if feat_a == feat_b else 3-feat_a-feat_b + for feat_a, feat_b in zip(card_a, card_b) + ) + try: + idx_c = self.board.index(completion) + except ValueError: + continue + + # Indices within the solution are sorted to detect duplicate solutions modulo order. + solution = tuple(sorted((idx_a, idx_b, idx_c))) + self._solutions.add(solution) + + return self._solutions + + +class DuckGamesDirector(commands.Cog): + """A cog for running Duck Duck Duck Goose games.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.current_games = {} + + @commands.group( + name='duckduckduckgoose', + aliases=['dddg', 'ddg', 'duckduckgoose', 'duckgoose'], + invoke_without_command=True + ) + @commands.cooldown(rate=1, per=2, type=commands.BucketType.channel) + async def start_game(self, ctx: commands.Context) -> None: + """Generate a board, send the game embed, and end the game after a time limit.""" + if ctx.channel.id in self.current_games: + await ctx.send("There's already a game running!") + return + + minimum_solutions, = random.choices(range(len(SOLN_DISTR)), weights=SOLN_DISTR) + game = DuckGame(minimum_solutions=minimum_solutions) + game.running = True + self.current_games[ctx.channel.id] = game + + game.msg_content = "" + game.embed_msg = await self.send_board_embed(ctx, game) + await asyncio.sleep(GAME_DURATION) + + # Checking for the channel ID in the currently running games is not sufficient. + # The game could have been ended by a player, and a new game already started in the same channel. + if game.running: + try: + del self.current_games[ctx.channel.id] + await self.end_game(ctx.channel, game, end_message="Time's up!") + except KeyError: + pass + + @commands.Cog.listener() + async def on_message(self, msg: discord.Message) -> None: + """Listen for messages and process them as answers if appropriate.""" + if msg.author.bot: + return + + channel = msg.channel + if channel.id not in self.current_games: + return + + game = self.current_games[channel.id] + if msg.content.strip().lower() == 'goose': + # If all of the solutions have been claimed, i.e. the "goose" call is correct. + if len(game.solutions) == len(game.claimed_answers): + try: + del self.current_games[channel.id] + game.scores[msg.author] += CORRECT_GOOSE + await self.end_game(channel, game, end_message=f"{msg.author.display_name} GOOSED!") + except KeyError: + pass + else: + await msg.add_reaction(EMOJI_WRONG) + game.scores[msg.author] += INCORRECT_GOOSE + return + + # Valid answers contain 3 numbers. + if not (match := re.match(ANSWER_REGEX, msg.content)): + return + answer = tuple(sorted(int(m) for m in match.groups())) + + # Be forgiving for answers that use indices not on the board. + if not all(0 <= n < len(game.board) for n in answer): + return + + # Also be forgiving for answers that have already been claimed (and avoid penalizing for racing conditions). + if answer in game.claimed_answers: + return + + if answer in game.solutions: + game.claimed_answers[answer] = msg.author + game.scores[msg.author] += CORRECT_SOLN + await self.display_claimed_answer(game, msg.author, answer) + else: + await msg.add_reaction(EMOJI_WRONG) + game.scores[msg.author] += INCORRECT_SOLN + + async def send_board_embed(self, ctx: commands.Context, game: DuckGame) -> discord.Message: + """Create and send the initial game embed. This will be edited as the game goes on.""" + image = assemble_board_image(game.board, game.rows, game.columns) + with BytesIO() as image_stream: + image.save(image_stream, format="png") + image_stream.seek(0) + file = discord.File(fp=image_stream, filename="board.png") + embed = discord.Embed( + title="Duck Duck Duck Goose!", + color=Colours.bright_green, + ) + embed.set_image(url="attachment://board.png") + return await ctx.send(embed=embed, file=file) + + async def display_claimed_answer(self, game: DuckGame, author: discord.Member, answer: tuple[int]) -> None: + """Add a claimed answer to the game embed.""" + async with game.editing_embed: + # We specifically edit the message contents instead of the embed + # Because we load in the image from the file, editing any portion of the embed + # Does weird things to the image and this works around that weirdness + game.msg_content = f"{game.msg_content}\n{str(answer):12s} - {author.display_name}" + await game.embed_msg.edit(content=game.msg_content) + + async def end_game(self, channel: discord.TextChannel, game: DuckGame, end_message: str) -> None: + """Edit the game embed to reflect the end of the game and mark the game as not running.""" + game.running = False + + scoreboard_embed = discord.Embed( + title=end_message, + color=discord.Color.dark_purple(), + ) + scores = sorted( + game.scores.items(), + key=lambda item: item[1], + reverse=True, + ) + scoreboard = "Final scores:\n\n" + scoreboard += "\n".join(f"{member.display_name}: {score}" for member, score in scores) + scoreboard_embed.description = scoreboard + await channel.send(embed=scoreboard_embed) + + missed = [ans for ans in game.solutions if ans not in game.claimed_answers] + if missed: + missed_text = "Flights everyone missed:\n" + "\n".join(f"{ans}" for ans in missed) + else: + missed_text = "All the flights were found!" + + await game.embed_msg.edit(content=f"{missed_text}") + + @start_game.command(name="help") + async def show_rules(self, ctx: commands.Context) -> None: + """Explain the rules of the game.""" + await self.send_help_embed(ctx) + + @start_game.command(name="stop") + @with_role(*MODERATION_ROLES) + async def stop_game(self, ctx: commands.Context) -> None: + """Stop a currently running game. Only available to mods.""" + try: + game = self.current_games.pop(ctx.channel.id) + except KeyError: + await ctx.send("No game currently running in this channel") + return + await self.end_game(ctx.channel, game, end_message="Game canceled.") + + @staticmethod + async def send_help_embed(ctx: commands.Context) -> discord.Message: + """Send rules embed.""" + embed = discord.Embed( + title="Compete against other players to find valid flights!", + color=discord.Color.dark_purple(), + ) + embed.description = HELP_TEXT + file = discord.File(HELP_IMAGE_PATH, filename="help.png") + embed.set_image(url="attachment://help.png") + embed.set_footer( + text="Tip: using Discord's compact message display mode can help keep the board on the screen" + ) + return await ctx.send(file=file, embed=embed) + + +def setup(bot: Bot) -> None: + """Load the DuckGamesDirector cog.""" + bot.add_cog(DuckGamesDirector(bot)) diff --git a/bot/exts/fun/fun.py b/bot/exts/fun/fun.py new file mode 100644 index 00000000..b148f1f3 --- /dev/null +++ b/bot/exts/fun/fun.py @@ -0,0 +1,250 @@ +import functools +import json +import logging +import random +from collections.abc import Iterable +from pathlib import Path +from typing import Callable, Optional, Union + +from discord import Embed, Message +from discord.ext import commands +from discord.ext.commands import BadArgument, Cog, Context, MessageConverter, clean_content + +from bot import utils +from bot.bot import Bot +from bot.constants import Client, Colours, Emojis +from bot.utils import helpers + +log = logging.getLogger(__name__) + +UWU_WORDS = { + "fi": "fwi", + "l": "w", + "r": "w", + "some": "sum", + "th": "d", + "thing": "fing", + "tho": "fo", + "you're": "yuw'we", + "your": "yur", + "you": "yuw", +} + + +def caesar_cipher(text: str, offset: int) -> Iterable[str]: + """ + Implements a lazy Caesar Cipher algorithm. + + Encrypts a `text` given a specific integer `offset`. The sign + of the `offset` dictates the direction in which it shifts to, + with a negative value shifting to the left, and a positive + value shifting to the right. + """ + for char in text: + if not char.isascii() or not char.isalpha() or char.isspace(): + yield char + continue + + case_start = 65 if char.isupper() else 97 + true_offset = (ord(char) - case_start + offset) % 26 + + yield chr(case_start + true_offset) + + +class Fun(Cog): + """A collection of general commands for fun.""" + + def __init__(self, bot: Bot): + self.bot = bot + + self._caesar_cipher_embed = json.loads(Path("bot/resources/fun/caesar_info.json").read_text("UTF-8")) + + @staticmethod + def _get_random_die() -> str: + """Generate a random die emoji, ready to be sent on Discord.""" + die_name = f"dice_{random.randint(1, 6)}" + return getattr(Emojis, die_name) + + @commands.command() + async def roll(self, ctx: Context, num_rolls: int = 1) -> None: + """Outputs a number of random dice emotes (up to 6).""" + if 1 <= num_rolls <= 6: + dice = " ".join(self._get_random_die() for _ in range(num_rolls)) + await ctx.send(dice) + else: + raise BadArgument(f"`{Client.prefix}roll` only supports between 1 and 6 rolls.") + + @commands.command(name="uwu", aliases=("uwuwize", "uwuify",)) + async def uwu_command(self, ctx: Context, *, text: clean_content(fix_channel_mentions=True)) -> None: + """Converts a given `text` into it's uwu equivalent.""" + conversion_func = functools.partial( + utils.replace_many, replacements=UWU_WORDS, ignore_case=True, match_case=True + ) + text, embed = await Fun._get_text_and_embed(ctx, text) + # Convert embed if it exists + if embed is not None: + embed = Fun._convert_embed(conversion_func, embed) + converted_text = conversion_func(text) + converted_text = helpers.suppress_links(converted_text) + # Don't put >>> if only embed present + if converted_text: + converted_text = f">>> {converted_text.lstrip('> ')}" + await ctx.send(content=converted_text, embed=embed) + + @commands.command(name="randomcase", aliases=("rcase", "randomcaps", "rcaps",)) + async def randomcase_command(self, ctx: Context, *, text: clean_content(fix_channel_mentions=True)) -> None: + """Randomly converts the casing of a given `text`.""" + def conversion_func(text: str) -> str: + """Randomly converts the casing of a given string.""" + return "".join( + char.upper() if round(random.random()) else char.lower() for char in text + ) + text, embed = await Fun._get_text_and_embed(ctx, text) + # Convert embed if it exists + if embed is not None: + embed = Fun._convert_embed(conversion_func, embed) + converted_text = conversion_func(text) + converted_text = helpers.suppress_links(converted_text) + # Don't put >>> if only embed present + if converted_text: + converted_text = f">>> {converted_text.lstrip('> ')}" + await ctx.send(content=converted_text, embed=embed) + + @commands.group(name="caesarcipher", aliases=("caesar", "cc",)) + async def caesarcipher_group(self, ctx: Context) -> None: + """ + Translates a message using the Caesar Cipher. + + See `decrypt`, `encrypt`, and `info` subcommands. + """ + if ctx.invoked_subcommand is None: + await ctx.invoke(self.bot.get_command("help"), "caesarcipher") + + @caesarcipher_group.command(name="info") + async def caesarcipher_info(self, ctx: Context) -> None: + """Information about the Caesar Cipher.""" + embed = Embed.from_dict(self._caesar_cipher_embed) + embed.colour = Colours.dark_green + + await ctx.send(embed=embed) + + @staticmethod + async def _caesar_cipher(ctx: Context, offset: int, msg: str, left_shift: bool = False) -> None: + """ + Given a positive integer `offset`, translates and sends the given `msg`. + + Performs a right shift by default unless `left_shift` is specified as `True`. + + Also accepts a valid Discord Message ID or link. + """ + if offset < 0: + await ctx.send(":no_entry: Cannot use a negative offset.") + return + + if left_shift: + offset = -offset + + def conversion_func(text: str) -> str: + """Encrypts the given string using the Caesar Cipher.""" + return "".join(caesar_cipher(text, offset)) + + text, embed = await Fun._get_text_and_embed(ctx, msg) + + if embed is not None: + embed = Fun._convert_embed(conversion_func, embed) + + converted_text = conversion_func(text) + + if converted_text: + converted_text = f">>> {converted_text.lstrip('> ')}" + + await ctx.send(content=converted_text, embed=embed) + + @caesarcipher_group.command(name="encrypt", aliases=("rightshift", "rshift", "enc",)) + async def caesarcipher_encrypt(self, ctx: Context, offset: int, *, msg: str) -> None: + """ + Given a positive integer `offset`, encrypt the given `msg`. + + Performs a right shift of the letters in the message. + + Also accepts a valid Discord Message ID or link. + """ + await self._caesar_cipher(ctx, offset, msg, left_shift=False) + + @caesarcipher_group.command(name="decrypt", aliases=("leftshift", "lshift", "dec",)) + async def caesarcipher_decrypt(self, ctx: Context, offset: int, *, msg: str) -> None: + """ + Given a positive integer `offset`, decrypt the given `msg`. + + Performs a left shift of the letters in the message. + + Also accepts a valid Discord Message ID or link. + """ + await self._caesar_cipher(ctx, offset, msg, left_shift=True) + + @staticmethod + async def _get_text_and_embed(ctx: Context, text: str) -> tuple[str, Optional[Embed]]: + """ + Attempts to extract the text and embed from a possible link to a discord Message. + + Does not retrieve the text and embed from the Message if it is in a channel the user does + not have read permissions in. + + Returns a tuple of: + str: If `text` is a valid discord Message, the contents of the message, else `text`. + Optional[Embed]: The embed if found in the valid Message, else None + """ + embed = None + + msg = await Fun._get_discord_message(ctx, text) + # Ensure the user has read permissions for the channel the message is in + if isinstance(msg, Message): + permissions = msg.channel.permissions_for(ctx.author) + if permissions.read_messages: + text = msg.clean_content + # Take first embed because we can't send multiple embeds + if msg.embeds: + embed = msg.embeds[0] + + return (text, embed) + + @staticmethod + async def _get_discord_message(ctx: Context, text: str) -> Union[Message, str]: + """ + Attempts to convert a given `text` to a discord Message object and return it. + + Conversion will succeed if given a discord Message ID or link. + Returns `text` if the conversion fails. + """ + try: + text = await MessageConverter().convert(ctx, text) + except commands.BadArgument: + log.debug(f"Input '{text:.20}...' is not a valid Discord Message") + return text + + @staticmethod + def _convert_embed(func: Callable[[str, ], str], embed: Embed) -> Embed: + """ + Converts the text in an embed using a given conversion function, then return the embed. + + Only modifies the following fields: title, description, footer, fields + """ + embed_dict = embed.to_dict() + + embed_dict["title"] = func(embed_dict.get("title", "")) + embed_dict["description"] = func(embed_dict.get("description", "")) + + if "footer" in embed_dict: + embed_dict["footer"]["text"] = func(embed_dict["footer"].get("text", "")) + + if "fields" in embed_dict: + for field in embed_dict["fields"]: + field["name"] = func(field.get("name", "")) + field["value"] = func(field.get("value", "")) + + return Embed.from_dict(embed_dict) + + +def setup(bot: Bot) -> None: + """Load the Fun cog.""" + bot.add_cog(Fun(bot)) diff --git a/bot/exts/fun/game.py b/bot/exts/fun/game.py new file mode 100644 index 00000000..f9c150e6 --- /dev/null +++ b/bot/exts/fun/game.py @@ -0,0 +1,485 @@ +import difflib +import logging +import random +import re +from asyncio import sleep +from datetime import datetime as dt, timedelta +from enum import IntEnum +from typing import Any, Optional + +from aiohttp import ClientSession +from discord import Embed +from discord.ext import tasks +from discord.ext.commands import Cog, Context, group + +from bot.bot import Bot +from bot.constants import STAFF_ROLES, Tokens +from bot.utils.decorators import with_role +from bot.utils.extensions import invoke_help_command +from bot.utils.pagination import ImagePaginator, LinePaginator + +# Base URL of IGDB API +BASE_URL = "https://api.igdb.com/v4" + +CLIENT_ID = Tokens.igdb_client_id +CLIENT_SECRET = Tokens.igdb_client_secret + +# The number of seconds before expiry that we attempt to re-fetch a new access token +ACCESS_TOKEN_RENEWAL_WINDOW = 60*60*24*2 + +# URL to request API access token +OAUTH_URL = "https://id.twitch.tv/oauth2/token" + +OAUTH_PARAMS = { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "client_credentials" +} + +BASE_HEADERS = { + "Client-ID": CLIENT_ID, + "Accept": "application/json" +} + +logger = logging.getLogger(__name__) + +REGEX_NON_ALPHABET = re.compile(r"[^a-z0-9]", re.IGNORECASE) + +# --------- +# TEMPLATES +# --------- + +# Body templates +# Request body template for get_games_list +GAMES_LIST_BODY = ( + "fields cover.image_id, first_release_date, total_rating, name, storyline, url, platforms.name, status," + "involved_companies.company.name, summary, age_ratings.category, age_ratings.rating, total_rating_count;" + "{sort} {limit} {offset} {genre} {additional}" +) + +# Request body template for get_companies_list +COMPANIES_LIST_BODY = ( + "fields name, url, start_date, logo.image_id, developed.name, published.name, description;" + "offset {offset}; limit {limit};" +) + +# Request body template for games search +SEARCH_BODY = 'fields name, url, storyline, total_rating, total_rating_count; limit 50; search "{term}";' + +# Pages templates +# Game embed layout +GAME_PAGE = ( + "**[{name}]({url})**\n" + "{description}" + "**Release Date:** {release_date}\n" + "**Rating:** {rating}/100 :star: (based on {rating_count} ratings)\n" + "**Platforms:** {platforms}\n" + "**Status:** {status}\n" + "**Age Ratings:** {age_ratings}\n" + "**Made by:** {made_by}\n\n" + "{storyline}" +) + +# .games company command page layout +COMPANY_PAGE = ( + "**[{name}]({url})**\n" + "{description}" + "**Founded:** {founded}\n" + "**Developed:** {developed}\n" + "**Published:** {published}" +) + +# For .games search command line layout +GAME_SEARCH_LINE = ( + "**[{name}]({url})**\n" + "{rating}/100 :star: (based on {rating_count} ratings)\n" +) + +# URL templates +COVER_URL = "https://images.igdb.com/igdb/image/upload/t_cover_big/{image_id}.jpg" +LOGO_URL = "https://images.igdb.com/igdb/image/upload/t_logo_med/{image_id}.png" + +# Create aliases for complex genre names +ALIASES = { + "Role-playing (rpg)": ["Role playing", "Rpg"], + "Turn-based strategy (tbs)": ["Turn based strategy", "Tbs"], + "Real time strategy (rts)": ["Real time strategy", "Rts"], + "Hack and slash/beat 'em up": ["Hack and slash"] +} + + +class GameStatus(IntEnum): + """Game statuses in IGDB API.""" + + Released = 0 + Alpha = 2 + Beta = 3 + Early = 4 + Offline = 5 + Cancelled = 6 + Rumored = 7 + + +class AgeRatingCategories(IntEnum): + """IGDB API Age Rating categories IDs.""" + + ESRB = 1 + PEGI = 2 + + +class AgeRatings(IntEnum): + """PEGI/ESRB ratings IGDB API IDs.""" + + Three = 1 + Seven = 2 + Twelve = 3 + Sixteen = 4 + Eighteen = 5 + RP = 6 + EC = 7 + E = 8 + E10 = 9 + T = 10 + M = 11 + AO = 12 + + +class Games(Cog): + """Games Cog contains commands that collect data from IGDB.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.http_session: ClientSession = bot.http_session + + self.genres: dict[str, int] = {} + self.headers = BASE_HEADERS + + self.bot.loop.create_task(self.renew_access_token()) + + async def renew_access_token(self) -> None: + """Refeshes V4 access token a number of seconds before expiry. See `ACCESS_TOKEN_RENEWAL_WINDOW`.""" + while True: + async with self.http_session.post(OAUTH_URL, params=OAUTH_PARAMS) as resp: + result = await resp.json() + if resp.status != 200: + # If there is a valid access token continue to use that, + # otherwise unload cog. + if "Authorization" in self.headers: + time_delta = timedelta(seconds=ACCESS_TOKEN_RENEWAL_WINDOW) + logger.error( + "Failed to renew IGDB access token. " + f"Current token will last for {time_delta} " + f"OAuth response message: {result['message']}" + ) + else: + logger.warning( + "Invalid OAuth credentials. Unloading Games cog. " + f"OAuth response message: {result['message']}" + ) + self.bot.remove_cog("Games") + + return + + self.headers["Authorization"] = f"Bearer {result['access_token']}" + + # Attempt to renew before the token expires + next_renewal = result["expires_in"] - ACCESS_TOKEN_RENEWAL_WINDOW + + time_delta = timedelta(seconds=next_renewal) + logger.info(f"Successfully renewed access token. Refreshing again in {time_delta}") + + # This will be true the first time this loop runs. + # Since we now have an access token, its safe to start this task. + if self.genres == {}: + self.refresh_genres_task.start() + await sleep(next_renewal) + + @tasks.loop(hours=24.0) + async def refresh_genres_task(self) -> None: + """Refresh genres in every hour.""" + try: + await self._get_genres() + except Exception as e: + logger.warning(f"There was error while refreshing genres: {e}") + return + logger.info("Successfully refreshed genres.") + + def cog_unload(self) -> None: + """Cancel genres refreshing start when unloading Cog.""" + self.refresh_genres_task.cancel() + logger.info("Successfully stopped Genres Refreshing task.") + + async def _get_genres(self) -> None: + """Create genres variable for games command.""" + body = "fields name; limit 100;" + async with self.http_session.post(f"{BASE_URL}/genres", data=body, headers=self.headers) as resp: + result = await resp.json() + genres = {genre["name"].capitalize(): genre["id"] for genre in result} + + # Replace complex names with names from ALIASES + for genre_name, genre in genres.items(): + if genre_name in ALIASES: + for alias in ALIASES[genre_name]: + self.genres[alias] = genre + else: + self.genres[genre_name] = genre + + @group(name="games", aliases=("game",), invoke_without_command=True) + async def games(self, ctx: Context, amount: Optional[int] = 5, *, genre: Optional[str]) -> None: + """ + Get random game(s) by genre from IGDB. Use .games genres command to get all available genres. + + Also support amount parameter, what max is 25 and min 1, default 5. Supported formats: + - .games + - .games + """ + # When user didn't specified genre, send help message + if genre is None: + await invoke_help_command(ctx) + return + + # Capitalize genre for check + genre = "".join(genre).capitalize() + + # Check for amounts, max is 25 and min 1 + if not 1 <= amount <= 25: + await ctx.send("Your provided amount is out of range. Our minimum is 1 and maximum 25.") + return + + # Get games listing, if genre don't exist, show error message with possibilities. + # Offset must be random, due otherwise we will get always same result (offset show in which position should + # API start returning result) + try: + games = await self.get_games_list(amount, self.genres[genre], offset=random.randint(0, 150)) + except KeyError: + possibilities = await self.get_best_results(genre) + # If there is more than 1 possibilities, show these. + # If there is only 1 possibility, use it as genre. + # Otherwise send message about invalid genre. + if len(possibilities) > 1: + display_possibilities = "`, `".join(p[1] for p in possibilities) + await ctx.send( + f"Invalid genre `{genre}`. " + f"{f'Maybe you meant `{display_possibilities}`?' if display_possibilities else ''}" + ) + return + elif len(possibilities) == 1: + games = await self.get_games_list( + amount, self.genres[possibilities[0][1]], offset=random.randint(0, 150) + ) + genre = possibilities[0][1] + else: + await ctx.send(f"Invalid genre `{genre}`.") + return + + # Create pages and paginate + pages = [await self.create_page(game) for game in games] + + await ImagePaginator.paginate(pages, ctx, Embed(title=f"Random {genre.title()} Games")) + + @games.command(name="top", aliases=("t",)) + async def top(self, ctx: Context, amount: int = 10) -> None: + """ + Get current Top games in IGDB. + + Support amount parameter. Max is 25, min is 1. + """ + if not 1 <= amount <= 25: + await ctx.send("Your provided amount is out of range. Our minimum is 1 and maximum 25.") + return + + games = await self.get_games_list(amount, sort="total_rating desc", + additional_body="where total_rating >= 90; sort total_rating_count desc;") + + pages = [await self.create_page(game) for game in games] + await ImagePaginator.paginate(pages, ctx, Embed(title=f"Top {amount} Games")) + + @games.command(name="genres", aliases=("genre", "g")) + async def genres(self, ctx: Context) -> None: + """Get all available genres.""" + await ctx.send(f"Currently available genres: {', '.join(f'`{genre}`' for genre in self.genres)}") + + @games.command(name="search", aliases=("s",)) + async def search(self, ctx: Context, *, search_term: str) -> None: + """Find games by name.""" + lines = await self.search_games(search_term) + + await LinePaginator.paginate(lines, ctx, Embed(title=f"Game Search Results: {search_term}"), empty=False) + + @games.command(name="company", aliases=("companies",)) + async def company(self, ctx: Context, amount: int = 5) -> None: + """ + Get random Game Companies companies from IGDB API. + + Support amount parameter. Max is 25, min is 1. + """ + if not 1 <= amount <= 25: + await ctx.send("Your provided amount is out of range. Our minimum is 1 and maximum 25.") + return + + # Get companies listing. Provide limit for limiting how much companies will be returned. Get random offset to + # get (almost) every time different companies (offset show in which position should API start returning result) + companies = await self.get_companies_list(limit=amount, offset=random.randint(0, 150)) + pages = [await self.create_company_page(co) for co in companies] + + await ImagePaginator.paginate(pages, ctx, Embed(title="Random Game Companies")) + + @with_role(*STAFF_ROLES) + @games.command(name="refresh", aliases=("r",)) + async def refresh_genres_command(self, ctx: Context) -> None: + """Refresh .games command genres.""" + try: + await self._get_genres() + except Exception as e: + await ctx.send(f"There was error while refreshing genres: `{e}`") + return + await ctx.send("Successfully refreshed genres.") + + async def get_games_list( + self, + amount: int, + genre: Optional[str] = None, + sort: Optional[str] = None, + additional_body: str = "", + offset: int = 0 + ) -> list[dict[str, Any]]: + """ + Get list of games from IGDB API by parameters that is provided. + + Amount param show how much games this get, genre is genre ID and at least one genre in game must this when + provided. Sort is sorting by specific field and direction, ex. total_rating desc/asc (total_rating is field, + desc/asc is direction). Additional_body is field where you can pass extra search parameters. Offset show start + position in API. + """ + # Create body of IGDB API request, define fields, sorting, offset, limit and genre + params = { + "sort": f"sort {sort};" if sort else "", + "limit": f"limit {amount};", + "offset": f"offset {offset};" if offset else "", + "genre": f"where genres = ({genre});" if genre else "", + "additional": additional_body + } + body = GAMES_LIST_BODY.format(**params) + + # Do request to IGDB API, create headers, URL, define body, return result + async with self.http_session.post(url=f"{BASE_URL}/games", data=body, headers=self.headers) as resp: + return await resp.json() + + async def create_page(self, data: dict[str, Any]) -> tuple[str, str]: + """Create content of Game Page.""" + # Create cover image URL from template + url = COVER_URL.format(**{"image_id": data["cover"]["image_id"] if "cover" in data else ""}) + + # Get release date separately with checking + release_date = dt.utcfromtimestamp(data["first_release_date"]).date() if "first_release_date" in data else "?" + + # Create Age Ratings value + rating = ", ".join( + f"{AgeRatingCategories(age['category']).name} {AgeRatings(age['rating']).name}" + for age in data["age_ratings"] + ) if "age_ratings" in data else "?" + + companies = [c["company"]["name"] for c in data["involved_companies"]] if "involved_companies" in data else "?" + + # Create formatting for template page + formatting = { + "name": data["name"], + "url": data["url"], + "description": f"{data['summary']}\n\n" if "summary" in data else "\n", + "release_date": release_date, + "rating": round(data["total_rating"] if "total_rating" in data else 0, 2), + "rating_count": data["total_rating_count"] if "total_rating_count" in data else "?", + "platforms": ", ".join(platform["name"] for platform in data["platforms"]) if "platforms" in data else "?", + "status": GameStatus(data["status"]).name if "status" in data else "?", + "age_ratings": rating, + "made_by": ", ".join(companies), + "storyline": data["storyline"] if "storyline" in data else "" + } + page = GAME_PAGE.format(**formatting) + + return page, url + + async def search_games(self, search_term: str) -> list[str]: + """Search game from IGDB API by string, return listing of pages.""" + lines = [] + + # Define request body of IGDB API request and do request + body = SEARCH_BODY.format(**{"term": search_term}) + + async with self.http_session.post(url=f"{BASE_URL}/games", data=body, headers=self.headers) as resp: + data = await resp.json() + + # Loop over games, format them to good format, make line and append this to total lines + for game in data: + formatting = { + "name": game["name"], + "url": game["url"], + "rating": round(game["total_rating"] if "total_rating" in game else 0, 2), + "rating_count": game["total_rating_count"] if "total_rating" in game else "?" + } + line = GAME_SEARCH_LINE.format(**formatting) + lines.append(line) + + return lines + + async def get_companies_list(self, limit: int, offset: int = 0) -> list[dict[str, Any]]: + """ + Get random Game Companies from IGDB API. + + Limit is parameter, that show how much movies this should return, offset show in which position should API start + returning results. + """ + # Create request body from template + body = COMPANIES_LIST_BODY.format(**{ + "limit": limit, + "offset": offset + }) + + async with self.http_session.post(url=f"{BASE_URL}/companies", data=body, headers=self.headers) as resp: + return await resp.json() + + async def create_company_page(self, data: dict[str, Any]) -> tuple[str, str]: + """Create good formatted Game Company page.""" + # Generate URL of company logo + url = LOGO_URL.format(**{"image_id": data["logo"]["image_id"] if "logo" in data else ""}) + + # Try to get found date of company + founded = dt.utcfromtimestamp(data["start_date"]).date() if "start_date" in data else "?" + + # Generate list of games, that company have developed or published + developed = ", ".join(game["name"] for game in data["developed"]) if "developed" in data else "?" + published = ", ".join(game["name"] for game in data["published"]) if "published" in data else "?" + + formatting = { + "name": data["name"], + "url": data["url"], + "description": f"{data['description']}\n\n" if "description" in data else "\n", + "founded": founded, + "developed": developed, + "published": published + } + page = COMPANY_PAGE.format(**formatting) + + return page, url + + async def get_best_results(self, query: str) -> list[tuple[float, str]]: + """Get best match result of genre when original genre is invalid.""" + results = [] + for genre in self.genres: + ratios = [difflib.SequenceMatcher(None, query, genre).ratio()] + for word in REGEX_NON_ALPHABET.split(genre): + ratios.append(difflib.SequenceMatcher(None, query, word).ratio()) + results.append((round(max(ratios), 2), genre)) + return sorted((item for item in results if item[0] >= 0.60), reverse=True)[:4] + + +def setup(bot: Bot) -> None: + """Load the Games cog.""" + # Check does IGDB API key exist, if not, log warning and don't load cog + if not Tokens.igdb_client_id: + logger.warning("No IGDB client ID. Not loading Games cog.") + return + if not Tokens.igdb_client_secret: + logger.warning("No IGDB client secret. Not loading Games cog.") + return + bot.add_cog(Games(bot)) diff --git a/bot/exts/fun/magic_8ball.py b/bot/exts/fun/magic_8ball.py new file mode 100644 index 00000000..a7b682ca --- /dev/null +++ b/bot/exts/fun/magic_8ball.py @@ -0,0 +1,30 @@ +import json +import logging +import random +from pathlib import Path + +from discord.ext import commands + +from bot.bot import Bot + +log = logging.getLogger(__name__) + +ANSWERS = json.loads(Path("bot/resources/fun/magic8ball.json").read_text("utf8")) + + +class Magic8ball(commands.Cog): + """A Magic 8ball command to respond to a user's question.""" + + @commands.command(name="8ball") + async def output_answer(self, ctx: commands.Context, *, question: str) -> None: + """Return a Magic 8ball answer from answers list.""" + if len(question.split()) >= 3: + answer = random.choice(ANSWERS) + await ctx.send(answer) + else: + await ctx.send("Usage: .8ball (minimum length of 3 eg: `will I win?`)") + + +def setup(bot: Bot) -> None: + """Load the Magic8Ball Cog.""" + bot.add_cog(Magic8ball()) diff --git a/bot/exts/fun/minesweeper.py b/bot/exts/fun/minesweeper.py new file mode 100644 index 00000000..a48b5051 --- /dev/null +++ b/bot/exts/fun/minesweeper.py @@ -0,0 +1,270 @@ +import logging +from collections.abc import Iterator +from dataclasses import dataclass +from random import randint, random +from typing import Union + +import discord +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Client +from bot.utils.converters import CoordinateConverter +from bot.utils.exceptions import UserNotPlayingError +from bot.utils.extensions import invoke_help_command + +MESSAGE_MAPPING = { + 0: ":stop_button:", + 1: ":one:", + 2: ":two:", + 3: ":three:", + 4: ":four:", + 5: ":five:", + 6: ":six:", + 7: ":seven:", + 8: ":eight:", + 9: ":nine:", + 10: ":keycap_ten:", + "bomb": ":bomb:", + "hidden": ":grey_question:", + "flag": ":flag_black:", + "x": ":x:" +} + +log = logging.getLogger(__name__) + + +GameBoard = list[list[Union[str, int]]] + + +@dataclass +class Game: + """The data for a game.""" + + board: GameBoard + revealed: GameBoard + dm_msg: discord.Message + chat_msg: discord.Message + activated_on_server: bool + + +class Minesweeper(commands.Cog): + """Play a game of Minesweeper.""" + + def __init__(self): + self.games: dict[int, Game] = {} + + @commands.group(name="minesweeper", aliases=("ms",), invoke_without_command=True) + async def minesweeper_group(self, ctx: commands.Context) -> None: + """Commands for Playing Minesweeper.""" + await invoke_help_command(ctx) + + @staticmethod + def get_neighbours(x: int, y: int) -> Iterator[tuple[int, int]]: + """Get all the neighbouring x and y including it self.""" + for x_ in [x - 1, x, x + 1]: + for y_ in [y - 1, y, y + 1]: + if x_ != -1 and x_ != 10 and y_ != -1 and y_ != 10: + yield x_, y_ + + def generate_board(self, bomb_chance: float) -> GameBoard: + """Generate a 2d array for the board.""" + board: GameBoard = [ + [ + "bomb" if random() <= bomb_chance else "number" + for _ in range(10) + ] for _ in range(10) + ] + + # make sure there is always a free cell + board[randint(0, 9)][randint(0, 9)] = "number" + + for y, row in enumerate(board): + for x, cell in enumerate(row): + if cell == "number": + # calculate bombs near it + bombs = 0 + for x_, y_ in self.get_neighbours(x, y): + if board[y_][x_] == "bomb": + bombs += 1 + board[y][x] = bombs + return board + + @staticmethod + def format_for_discord(board: GameBoard) -> str: + """Format the board as a string for Discord.""" + discord_msg = ( + ":stop_button: :regional_indicator_a: :regional_indicator_b: :regional_indicator_c: " + ":regional_indicator_d: :regional_indicator_e: :regional_indicator_f: :regional_indicator_g: " + ":regional_indicator_h: :regional_indicator_i: :regional_indicator_j:\n\n" + ) + rows = [] + for row_number, row in enumerate(board): + new_row = f"{MESSAGE_MAPPING[row_number + 1]} " + new_row += " ".join(MESSAGE_MAPPING[cell] for cell in row) + rows.append(new_row) + + discord_msg += "\n".join(rows) + return discord_msg + + @minesweeper_group.command(name="start") + async def start_command(self, ctx: commands.Context, bomb_chance: float = .2) -> None: + """Start a game of Minesweeper.""" + if ctx.author.id in self.games: # Player is already playing + await ctx.send(f"{ctx.author.mention} you already have a game running!", delete_after=2) + await ctx.message.delete(delay=2) + return + + try: + await ctx.author.send( + f"Play by typing: `{Client.prefix}ms reveal xy [xy]` or `{Client.prefix}ms flag xy [xy]` \n" + f"Close the game with `{Client.prefix}ms end`\n" + ) + except discord.errors.Forbidden: + log.debug(f"{ctx.author.name} ({ctx.author.id}) has disabled DMs from server members.") + await ctx.send(f":x: {ctx.author.mention}, please enable DMs to play minesweeper.") + return + + # Add game to list + board: GameBoard = self.generate_board(bomb_chance) + revealed_board: GameBoard = [["hidden"] * 10 for _ in range(10)] + dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(revealed_board)}") + + if ctx.guild: + await ctx.send(f"{ctx.author.mention} is playing Minesweeper.") + chat_msg = await ctx.send(f"Here's their board!\n{self.format_for_discord(revealed_board)}") + else: + chat_msg = None + + self.games[ctx.author.id] = Game( + board=board, + revealed=revealed_board, + dm_msg=dm_msg, + chat_msg=chat_msg, + activated_on_server=ctx.guild is not None + ) + + async def update_boards(self, ctx: commands.Context) -> None: + """Update both playing boards.""" + game = self.games[ctx.author.id] + await game.dm_msg.delete() + game.dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(game.revealed)}") + if game.activated_on_server: + await game.chat_msg.edit(content=f"Here's their board!\n{self.format_for_discord(game.revealed)}") + + @commands.dm_only() + @minesweeper_group.command(name="flag") + async def flag_command(self, ctx: commands.Context, *coordinates: CoordinateConverter) -> None: + """Place multiple flags on the board.""" + if ctx.author.id not in self.games: + raise UserNotPlayingError + board: GameBoard = self.games[ctx.author.id].revealed + for x, y in coordinates: + if board[y][x] == "hidden": + board[y][x] = "flag" + + await self.update_boards(ctx) + + @staticmethod + def reveal_bombs(revealed: GameBoard, board: GameBoard) -> None: + """Reveals all the bombs.""" + for y, row in enumerate(board): + for x, cell in enumerate(row): + if cell == "bomb": + revealed[y][x] = cell + + async def lost(self, ctx: commands.Context) -> None: + """The player lost the game.""" + game = self.games[ctx.author.id] + self.reveal_bombs(game.revealed, game.board) + await ctx.author.send(":fire: You lost! :fire:") + if game.activated_on_server: + await game.chat_msg.channel.send(f":fire: {ctx.author.mention} just lost Minesweeper! :fire:") + + async def won(self, ctx: commands.Context) -> None: + """The player won the game.""" + game = self.games[ctx.author.id] + await ctx.author.send(":tada: You won! :tada:") + if game.activated_on_server: + await game.chat_msg.channel.send(f":tada: {ctx.author.mention} just won Minesweeper! :tada:") + + def reveal_zeros(self, revealed: GameBoard, board: GameBoard, x: int, y: int) -> None: + """Recursively reveal adjacent cells when a 0 cell is encountered.""" + for x_, y_ in self.get_neighbours(x, y): + if revealed[y_][x_] != "hidden": + continue + revealed[y_][x_] = board[y_][x_] + if board[y_][x_] == 0: + self.reveal_zeros(revealed, board, x_, y_) + + async def check_if_won(self, ctx: commands.Context, revealed: GameBoard, board: GameBoard) -> bool: + """Checks if a player has won.""" + if any( + revealed[y][x] in ["hidden", "flag"] and board[y][x] != "bomb" + for x in range(10) + for y in range(10) + ): + return False + else: + await self.won(ctx) + return True + + async def reveal_one( + self, + ctx: commands.Context, + revealed: GameBoard, + board: GameBoard, + x: int, + y: int + ) -> bool: + """ + Reveal one square. + + return is True if the game ended, breaking the loop in `reveal_command` and deleting the game. + """ + revealed[y][x] = board[y][x] + if board[y][x] == "bomb": + await self.lost(ctx) + revealed[y][x] = "x" # mark bomb that made you lose with a x + return True + elif board[y][x] == 0: + self.reveal_zeros(revealed, board, x, y) + return await self.check_if_won(ctx, revealed, board) + + @commands.dm_only() + @minesweeper_group.command(name="reveal") + async def reveal_command(self, ctx: commands.Context, *coordinates: CoordinateConverter) -> None: + """Reveal multiple cells.""" + if ctx.author.id not in self.games: + raise UserNotPlayingError + game = self.games[ctx.author.id] + revealed: GameBoard = game.revealed + board: GameBoard = game.board + + for x, y in coordinates: + # reveal_one returns True if the revealed cell is a bomb or the player won, ending the game + if await self.reveal_one(ctx, revealed, board, x, y): + await self.update_boards(ctx) + del self.games[ctx.author.id] + break + else: + await self.update_boards(ctx) + + @minesweeper_group.command(name="end") + async def end_command(self, ctx: commands.Context) -> None: + """End your current game.""" + if ctx.author.id not in self.games: + raise UserNotPlayingError + game = self.games[ctx.author.id] + game.revealed = game.board + await self.update_boards(ctx) + new_msg = f":no_entry: Game canceled. :no_entry:\n{game.dm_msg.content}" + await game.dm_msg.edit(content=new_msg) + if game.activated_on_server: + await game.chat_msg.edit(content=new_msg) + del self.games[ctx.author.id] + + +def setup(bot: Bot) -> None: + """Load the Minesweeper cog.""" + bot.add_cog(Minesweeper()) diff --git a/bot/exts/fun/movie.py b/bot/exts/fun/movie.py new file mode 100644 index 00000000..a04eeb41 --- /dev/null +++ b/bot/exts/fun/movie.py @@ -0,0 +1,205 @@ +import logging +import random +from enum import Enum +from typing import Any + +from aiohttp import ClientSession +from discord import Embed +from discord.ext.commands import Cog, Context, group + +from bot.bot import Bot +from bot.constants import Tokens +from bot.utils.extensions import invoke_help_command +from bot.utils.pagination import ImagePaginator + +# Define base URL of TMDB +BASE_URL = "https://api.themoviedb.org/3/" + +logger = logging.getLogger(__name__) + +# Define movie params, that will be used for every movie request +MOVIE_PARAMS = { + "api_key": Tokens.tmdb, + "language": "en-US" +} + + +class MovieGenres(Enum): + """Movies Genre names and IDs.""" + + Action = "28" + Adventure = "12" + Animation = "16" + Comedy = "35" + Crime = "80" + Documentary = "99" + Drama = "18" + Family = "10751" + Fantasy = "14" + History = "36" + Horror = "27" + Music = "10402" + Mystery = "9648" + Romance = "10749" + Science = "878" + Thriller = "53" + Western = "37" + + +class Movie(Cog): + """Movie Cog contains movies command that grab random movies from TMDB.""" + + def __init__(self, bot: Bot): + self.http_session: ClientSession = bot.http_session + + @group(name="movies", aliases=("movie",), invoke_without_command=True) + async def movies(self, ctx: Context, genre: str = "", amount: int = 5) -> None: + """ + Get random movies by specifying genre. Also support amount parameter, that define how much movies will be shown. + + Default 5. Use .movies genres to get all available genres. + """ + # Check is there more than 20 movies specified, due TMDB return 20 movies + # per page, so this is max. Also you can't get less movies than 1, just logic + if amount > 20: + await ctx.send("You can't get more than 20 movies at once. (TMDB limits)") + return + elif amount < 1: + await ctx.send("You can't get less than 1 movie.") + return + + # Capitalize genre for getting data from Enum, get random page, send help when genre don't exist. + genre = genre.capitalize() + try: + result = await self.get_movies_data(self.http_session, MovieGenres[genre].value, 1) + except KeyError: + await invoke_help_command(ctx) + return + + # Check if "results" is in result. If not, throw error. + if "results" not in result: + err_msg = ( + f"There is problem while making TMDB API request. Response Code: {result['status_code']}, " + f"{result['status_message']}." + ) + await ctx.send(err_msg) + logger.warning(err_msg) + + # Get random page. Max page is last page where is movies with this genre. + page = random.randint(1, result["total_pages"]) + + # Get movies list from TMDB, check if results key in result. When not, raise error. + movies = await self.get_movies_data(self.http_session, MovieGenres[genre].value, page) + if "results" not in movies: + err_msg = f"There is problem while making TMDB API request. Response Code: {result['status_code']}, " \ + f"{result['status_message']}." + await ctx.send(err_msg) + logger.warning(err_msg) + + # Get all pages and embed + pages = await self.get_pages(self.http_session, movies, amount) + embed = await self.get_embed(genre) + + await ImagePaginator.paginate(pages, ctx, embed) + + @movies.command(name="genres", aliases=("genre", "g")) + async def genres(self, ctx: Context) -> None: + """Show all currently available genres for .movies command.""" + await ctx.send(f"Current available genres: {', '.join('`' + genre.name + '`' for genre in MovieGenres)}") + + async def get_movies_data(self, client: ClientSession, genre_id: str, page: int) -> list[dict[str, Any]]: + """Return JSON of TMDB discover request.""" + # Define params of request + params = { + "api_key": Tokens.tmdb, + "language": "en-US", + "sort_by": "popularity.desc", + "include_adult": "false", + "include_video": "false", + "page": page, + "with_genres": genre_id + } + + url = BASE_URL + "discover/movie" + + # Make discover request to TMDB, return result + async with client.get(url, params=params) as resp: + return await resp.json() + + async def get_pages(self, client: ClientSession, movies: dict[str, Any], amount: int) -> list[tuple[str, str]]: + """Fetch all movie pages from movies dictionary. Return list of pages.""" + pages = [] + + for i in range(amount): + movie_id = movies["results"][i]["id"] + movie = await self.get_movie(client, movie_id) + + page, img = await self.create_page(movie) + pages.append((page, img)) + + return pages + + async def get_movie(self, client: ClientSession, movie: int) -> dict[str, Any]: + """Get Movie by movie ID from TMDB. Return result dictionary.""" + if not isinstance(movie, int): + raise ValueError("Error while fetching movie from TMDB, movie argument must be integer. ") + url = BASE_URL + f"movie/{movie}" + + async with client.get(url, params=MOVIE_PARAMS) as resp: + return await resp.json() + + async def create_page(self, movie: dict[str, Any]) -> tuple[str, str]: + """Create page from TMDB movie request result. Return formatted page + image.""" + text = "" + + # Add title + tagline (if not empty) + text += f"**{movie['title']}**\n" + if movie["tagline"]: + text += f"{movie['tagline']}\n\n" + else: + text += "\n" + + # Add other information + text += f"**Rating:** {movie['vote_average']}/10 :star:\n" + text += f"**Release Date:** {movie['release_date']}\n\n" + + text += "__**Production Information**__\n" + + companies = movie["production_companies"] + countries = movie["production_countries"] + + text += f"**Made by:** {', '.join(company['name'] for company in companies)}\n" + text += f"**Made in:** {', '.join(country['name'] for country in countries)}\n\n" + + text += "__**Some Numbers**__\n" + + budget = f"{movie['budget']:,d}" if movie['budget'] else "?" + revenue = f"{movie['revenue']:,d}" if movie['revenue'] else "?" + + if movie["runtime"] is not None: + duration = divmod(movie["runtime"], 60) + else: + duration = ("?", "?") + + text += f"**Budget:** ${budget}\n" + text += f"**Revenue:** ${revenue}\n" + text += f"**Duration:** {f'{duration[0]} hour(s) {duration[1]} minute(s)'}\n\n" + + text += movie["overview"] + + img = f"http://image.tmdb.org/t/p/w200{movie['poster_path']}" + + # Return page content and image + return text, img + + async def get_embed(self, name: str) -> Embed: + """Return embed of random movies. Uses name in title.""" + embed = Embed(title=f"Random {name} Movies") + embed.set_footer(text="This product uses the TMDb API but is not endorsed or certified by TMDb.") + embed.set_thumbnail(url="https://i.imgur.com/LtFtC8H.png") + return embed + + +def setup(bot: Bot) -> None: + """Load the Movie Cog.""" + bot.add_cog(Movie(bot)) diff --git a/bot/exts/fun/recommend_game.py b/bot/exts/fun/recommend_game.py new file mode 100644 index 00000000..42c9f7c2 --- /dev/null +++ b/bot/exts/fun/recommend_game.py @@ -0,0 +1,51 @@ +import json +import logging +from pathlib import Path +from random import shuffle + +import discord +from discord.ext import commands + +from bot.bot import Bot + +log = logging.getLogger(__name__) +game_recs = [] + +# Populate the list `game_recs` with resource files +for rec_path in Path("bot/resources/fun/game_recs").glob("*.json"): + data = json.loads(rec_path.read_text("utf8")) + game_recs.append(data) +shuffle(game_recs) + + +class RecommendGame(commands.Cog): + """Commands related to recommending games.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.index = 0 + + @commands.command(name="recommendgame", aliases=("gamerec",)) + async def recommend_game(self, ctx: commands.Context) -> None: + """Sends an Embed of a random game recommendation.""" + if self.index >= len(game_recs): + self.index = 0 + shuffle(game_recs) + game = game_recs[self.index] + self.index += 1 + + author = self.bot.get_user(int(game["author"])) + + # Creating and formatting Embed + embed = discord.Embed(color=discord.Colour.blue()) + if author is not None: + embed.set_author(name=author.name, icon_url=author.display_avatar.url) + embed.set_image(url=game["image"]) + embed.add_field(name=f"Recommendation: {game['title']}\n{game['link']}", value=game["description"]) + + await ctx.send(embed=embed) + + +def setup(bot: Bot) -> None: + """Loads the RecommendGame cog.""" + bot.add_cog(RecommendGame(bot)) diff --git a/bot/exts/fun/rps.py b/bot/exts/fun/rps.py new file mode 100644 index 00000000..c6bbff46 --- /dev/null +++ b/bot/exts/fun/rps.py @@ -0,0 +1,57 @@ +from random import choice + +from discord.ext import commands + +from bot.bot import Bot + +CHOICES = ["rock", "paper", "scissors"] +SHORT_CHOICES = ["r", "p", "s"] + +# Using a dictionary instead of conditions to check for the winner. +WINNER_DICT = { + "r": { + "r": 0, + "p": -1, + "s": 1, + }, + "p": { + "r": 1, + "p": 0, + "s": -1, + }, + "s": { + "r": -1, + "p": 1, + "s": 0, + } +} + + +class RPS(commands.Cog): + """Rock Paper Scissors. The Classic Game!""" + + @commands.command(case_insensitive=True) + async def rps(self, ctx: commands.Context, move: str) -> None: + """Play the classic game of Rock Paper Scissors with your own sir-lancebot!""" + move = move.lower() + player_mention = ctx.author.mention + + if move not in CHOICES and move not in SHORT_CHOICES: + raise commands.BadArgument(f"Invalid move. Please make move from options: {', '.join(CHOICES).upper()}.") + + bot_move = choice(CHOICES) + # value of player_result will be from (-1, 0, 1) as (lost, tied, won). + player_result = WINNER_DICT[move[0]][bot_move[0]] + + if player_result == 0: + message_string = f"{player_mention} You and Sir Lancebot played {bot_move}, it's a tie." + await ctx.send(message_string) + elif player_result == 1: + await ctx.send(f"Sir Lancebot played {bot_move}! {player_mention} won!") + else: + await ctx.send(f"Sir Lancebot played {bot_move}! {player_mention} lost!") + + +def setup(bot: Bot) -> None: + """Load the RPS Cog.""" + bot.add_cog(RPS(bot)) diff --git a/bot/exts/fun/space.py b/bot/exts/fun/space.py new file mode 100644 index 00000000..48ad0f96 --- /dev/null +++ b/bot/exts/fun/space.py @@ -0,0 +1,236 @@ +import logging +import random +from datetime import date, datetime +from typing import Any, Optional +from urllib.parse import urlencode + +from discord import Embed +from discord.ext import tasks +from discord.ext.commands import Cog, Context, group + +from bot.bot import Bot +from bot.constants import Tokens +from bot.utils.converters import DateConverter +from bot.utils.extensions import invoke_help_command + +logger = logging.getLogger(__name__) + +NASA_BASE_URL = "https://api.nasa.gov" +NASA_IMAGES_BASE_URL = "https://images-api.nasa.gov" +NASA_EPIC_BASE_URL = "https://epic.gsfc.nasa.gov" + +APOD_MIN_DATE = date(1995, 6, 16) + + +class Space(Cog): + """Space Cog contains commands, that show images, facts or other information about space.""" + + def __init__(self, bot: Bot): + self.http_session = bot.http_session + + self.rovers = {} + self.get_rovers.start() + + def cog_unload(self) -> None: + """Cancel `get_rovers` task when Cog will unload.""" + self.get_rovers.cancel() + + @tasks.loop(hours=24) + async def get_rovers(self) -> None: + """Get listing of rovers from NASA API and info about their start and end dates.""" + data = await self.fetch_from_nasa("mars-photos/api/v1/rovers") + + for rover in data["rovers"]: + self.rovers[rover["name"].lower()] = { + "min_date": rover["landing_date"], + "max_date": rover["max_date"], + "max_sol": rover["max_sol"] + } + + @group(name="space", invoke_without_command=True) + async def space(self, ctx: Context) -> None: + """Head command that contains commands about space.""" + await invoke_help_command(ctx) + + @space.command(name="apod") + async def apod(self, ctx: Context, date: Optional[str]) -> None: + """ + Get Astronomy Picture of Day from NASA API. Date is optional parameter, what formatting is YYYY-MM-DD. + + If date is not specified, this will get today APOD. + """ + params = {} + # Parse date to params, when provided. Show error message when invalid formatting + if date: + try: + apod_date = datetime.strptime(date, "%Y-%m-%d").date() + except ValueError: + await ctx.send(f"Invalid date {date}. Please make sure your date is in format YYYY-MM-DD.") + return + + now = datetime.now().date() + if APOD_MIN_DATE > apod_date or now < apod_date: + await ctx.send(f"Date must be between {APOD_MIN_DATE.isoformat()} and {now.isoformat()} (today).") + return + + params["date"] = apod_date.isoformat() + + result = await self.fetch_from_nasa("planetary/apod", params) + + await ctx.send( + embed=self.create_nasa_embed( + f"Astronomy Picture of the Day - {result['date']}", + result["explanation"], + result["url"] + ) + ) + + @space.command(name="nasa") + async def nasa(self, ctx: Context, *, search_term: Optional[str]) -> None: + """Get random NASA information/facts + image. Support `search_term` parameter for more specific search.""" + params = { + "media_type": "image" + } + if search_term: + params["q"] = search_term + + # Don't use API key, no need for this. + data = await self.fetch_from_nasa("search", params, NASA_IMAGES_BASE_URL, use_api_key=False) + if len(data["collection"]["items"]) == 0: + await ctx.send(f"Can't find any items with search term `{search_term}`.") + return + + item = random.choice(data["collection"]["items"]) + + await ctx.send( + embed=self.create_nasa_embed( + item["data"][0]["title"], + item["data"][0]["description"], + item["links"][0]["href"] + ) + ) + + @space.command(name="epic") + async def epic(self, ctx: Context, date: Optional[str]) -> None: + """Get a random image of the Earth from the NASA EPIC API. Support date parameter, format is YYYY-MM-DD.""" + if date: + try: + show_date = datetime.strptime(date, "%Y-%m-%d").date().isoformat() + except ValueError: + await ctx.send(f"Invalid date {date}. Please make sure your date is in format YYYY-MM-DD.") + return + else: + show_date = None + + # Don't use API key, no need for this. + data = await self.fetch_from_nasa( + f"api/natural{f'/date/{show_date}' if show_date else ''}", + base=NASA_EPIC_BASE_URL, + use_api_key=False + ) + if len(data) < 1: + await ctx.send("Can't find any images in this date.") + return + + item = random.choice(data) + + year, month, day = item["date"].split(" ")[0].split("-") + image_url = f"{NASA_EPIC_BASE_URL}/archive/natural/{year}/{month}/{day}/jpg/{item['image']}.jpg" + + await ctx.send( + embed=self.create_nasa_embed( + "Earth Image", item["caption"], image_url, f" \u2022 Identifier: {item['identifier']}" + ) + ) + + @space.group(name="mars", invoke_without_command=True) + async def mars( + self, + ctx: Context, + date: Optional[DateConverter], + rover: str = "curiosity" + ) -> None: + """ + Get random Mars image by date. Support both SOL (martian solar day) and earth date and rovers. + + Earth date formatting is YYYY-MM-DD. Use `.space mars dates` to get all currently available rovers. + """ + rover = rover.lower() + if rover not in self.rovers: + await ctx.send( + ( + f"Invalid rover `{rover}`.\n" + f"**Rovers:** `{'`, `'.join(f'{r.capitalize()}' for r in self.rovers)}`" + ) + ) + return + + # When date not provided, get random SOL date between 0 and rover's max. + if date is None: + date = random.randint(0, self.rovers[rover]["max_sol"]) + + params = {} + if isinstance(date, int): + params["sol"] = date + else: + params["earth_date"] = date.date().isoformat() + + result = await self.fetch_from_nasa(f"mars-photos/api/v1/rovers/{rover}/photos", params) + if len(result["photos"]) < 1: + err_msg = ( + f"We can't find result in date " + f"{date.date().isoformat() if isinstance(date, datetime) else f'{date} SOL'}.\n" + f"**Note:** Dates must match with rover's working dates. Please use `{ctx.prefix}space mars dates` to " + "see working dates for each rover." + ) + await ctx.send(err_msg) + return + + item = random.choice(result["photos"]) + await ctx.send( + embed=self.create_nasa_embed( + f"{item['rover']['name']}'s {item['camera']['full_name']} Mars Image", "", item["img_src"], + ) + ) + + @mars.command(name="dates", aliases=("d", "date", "rover", "rovers", "r")) + async def dates(self, ctx: Context) -> None: + """Get current available rovers photo date ranges.""" + await ctx.send("\n".join( + f"**{r.capitalize()}:** {i['min_date']} **-** {i['max_date']}" for r, i in self.rovers.items() + )) + + async def fetch_from_nasa( + self, + endpoint: str, + additional_params: Optional[dict[str, Any]] = None, + base: Optional[str] = NASA_BASE_URL, + use_api_key: bool = True + ) -> dict[str, Any]: + """Fetch information from NASA API, return result.""" + params = {} + if use_api_key: + params["api_key"] = Tokens.nasa + + # Add additional parameters to request parameters only when they provided by user + if additional_params is not None: + params.update(additional_params) + + async with self.http_session.get(url=f"{base}/{endpoint}?{urlencode(params)}") as resp: + return await resp.json() + + def create_nasa_embed(self, title: str, description: str, image: str, footer: Optional[str] = "") -> Embed: + """Generate NASA commands embeds. Required: title, description and image URL, footer (addition) is optional.""" + return Embed( + title=title, + description=description + ).set_image(url=image).set_footer(text="Powered by NASA API" + footer) + + +def setup(bot: Bot) -> None: + """Load the Space cog.""" + if not Tokens.nasa: + logger.warning("Can't find NASA API key. Not loading Space Cog.") + return + + bot.add_cog(Space(bot)) diff --git a/bot/exts/fun/speedrun.py b/bot/exts/fun/speedrun.py new file mode 100644 index 00000000..c2966ce1 --- /dev/null +++ b/bot/exts/fun/speedrun.py @@ -0,0 +1,26 @@ +import json +import logging +from pathlib import Path +from random import choice + +from discord.ext import commands + +from bot.bot import Bot + +log = logging.getLogger(__name__) + +LINKS = json.loads(Path("bot/resources/fun/speedrun_links.json").read_text("utf8")) + + +class Speedrun(commands.Cog): + """Commands about the video game speedrunning community.""" + + @commands.command(name="speedrun") + async def get_speedrun(self, ctx: commands.Context) -> None: + """Sends a link to a video of a random speedrun.""" + await ctx.send(choice(LINKS)) + + +def setup(bot: Bot) -> None: + """Load the Speedrun cog.""" + bot.add_cog(Speedrun()) diff --git a/bot/exts/fun/status_codes.py b/bot/exts/fun/status_codes.py new file mode 100644 index 00000000..501cbe0a --- /dev/null +++ b/bot/exts/fun/status_codes.py @@ -0,0 +1,87 @@ +from random import choice + +import discord +from discord.ext import commands + +from bot.bot import Bot + +HTTP_DOG_URL = "https://httpstatusdogs.com/img/{code}.jpg" +HTTP_CAT_URL = "https://http.cat/{code}.jpg" +STATUS_TEMPLATE = "**Status: {code}**" +ERR_404 = "Unable to find status floof for {code}." +ERR_UNKNOWN = "Error attempting to retrieve status floof for {code}." +ERROR_LENGTH_EMBED = discord.Embed( + title="Input status code does not exist", + description="The range of valid status codes is 100 to 599", +) + + +class HTTPStatusCodes(commands.Cog): + """ + Fetch an image depicting HTTP status codes as a dog or a cat. + + If neither animal is selected a cat or dog is chosen randomly for the given status code. + """ + + def __init__(self, bot: Bot): + self.bot = bot + + @commands.group( + name="http_status", + aliases=("status", "httpstatus"), + invoke_without_command=True, + ) + async def http_status_group(self, ctx: commands.Context, code: int) -> None: + """Choose a cat or dog randomly for the given status code.""" + subcmd = choice((self.http_cat, self.http_dog)) + await subcmd(ctx, code) + + @http_status_group.command(name="cat") + async def http_cat(self, ctx: commands.Context, code: int) -> None: + """Send a cat version of the requested HTTP status code.""" + if code in range(100, 600): + await self.build_embed(url=HTTP_CAT_URL.format(code=code), ctx=ctx, code=code) + return + await ctx.send(embed=ERROR_LENGTH_EMBED) + + @http_status_group.command(name="dog") + async def http_dog(self, ctx: commands.Context, code: int) -> None: + """Send a dog version of the requested HTTP status code.""" + if code in range(100, 600): + await self.build_embed(url=HTTP_DOG_URL.format(code=code), ctx=ctx, code=code) + return + await ctx.send(embed=ERROR_LENGTH_EMBED) + + async def build_embed(self, url: str, ctx: commands.Context, code: int) -> None: + """Attempt to build and dispatch embed. Append error message instead if something goes wrong.""" + async with self.bot.http_session.get(url, allow_redirects=False) as response: + if response.status in range(200, 300): + await ctx.send( + embed=discord.Embed( + title=STATUS_TEMPLATE.format(code=code) + ).set_image(url=url) + ) + elif response.status in (302, 404): # dog URL returns 302 instead of 404 + if "dog" in url: + await ctx.send( + embed=discord.Embed( + title=ERR_404.format(code=code) + ).set_image(url="https://httpstatusdogs.com/img/404.jpg") + ) + return + await ctx.send( + embed=discord.Embed( + title=ERR_404.format(code=code) + ).set_image(url="https://http.cat/404.jpg") + ) + else: + await ctx.send( + embed=discord.Embed( + title=STATUS_TEMPLATE.format(code=code) + ).set_footer(text=ERR_UNKNOWN.format(code=code)) + ) + + +def setup(bot: Bot) -> None: + """Load the HTTPStatusCodes cog.""" + bot.add_cog(HTTPStatusCodes(bot)) diff --git a/bot/exts/fun/tic_tac_toe.py b/bot/exts/fun/tic_tac_toe.py new file mode 100644 index 00000000..5c4f8051 --- /dev/null +++ b/bot/exts/fun/tic_tac_toe.py @@ -0,0 +1,335 @@ +import asyncio +import random +from typing import Callable, Optional, Union + +import discord +from discord.ext.commands import Cog, Context, check, group, guild_only + +from bot.bot import Bot +from bot.constants import Emojis +from bot.utils.pagination import LinePaginator + +CONFIRMATION_MESSAGE = ( + "{opponent}, {requester} wants to play Tic-Tac-Toe against you." + f"\nReact to this message with {Emojis.confirmation} to accept or with {Emojis.decline} to decline." +) + + +def check_win(board: dict[int, str]) -> bool: + """Check from board, is any player won game.""" + return any( + ( + # Horizontal + board[1] == board[2] == board[3], + board[4] == board[5] == board[6], + board[7] == board[8] == board[9], + # Vertical + board[1] == board[4] == board[7], + board[2] == board[5] == board[8], + board[3] == board[6] == board[9], + # Diagonal + board[1] == board[5] == board[9], + board[3] == board[5] == board[7], + ) + ) + + +class Player: + """Class that contains information about player and functions that interact with player.""" + + def __init__(self, user: discord.User, ctx: Context, symbol: str): + self.user = user + self.ctx = ctx + self.symbol = symbol + + async def get_move(self, board: dict[int, str], msg: discord.Message) -> tuple[bool, Optional[int]]: + """ + Get move from user. + + Return is timeout reached and position of field what user will fill when timeout don't reach. + """ + def check_for_move(r: discord.Reaction, u: discord.User) -> bool: + """Check does user who reacted is user who we want, message is board and emoji is in board values.""" + return ( + u.id == self.user.id + and msg.id == r.message.id + and r.emoji in board.values() + and r.emoji in Emojis.number_emojis.values() + ) + + try: + react, _ = await self.ctx.bot.wait_for("reaction_add", timeout=30.0, check=check_for_move) + except asyncio.TimeoutError: + return True, None + else: + return False, list(Emojis.number_emojis.keys())[list(Emojis.number_emojis.values()).index(react.emoji)] + + def __str__(self) -> str: + """Return mention of user.""" + return self.user.mention + + +class AI: + """Tic Tac Toe AI class for against computer gaming.""" + + def __init__(self, symbol: str): + self.symbol = symbol + + async def get_move(self, board: dict[int, str], _: discord.Message) -> tuple[bool, int]: + """Get move from AI. AI use Minimax strategy.""" + possible_moves = [i for i, emoji in board.items() if emoji in list(Emojis.number_emojis.values())] + + for symbol in (Emojis.o_square, Emojis.x_square): + for move in possible_moves: + board_copy = board.copy() + board_copy[move] = symbol + if check_win(board_copy): + return False, move + + open_corners = [i for i in possible_moves if i in (1, 3, 7, 9)] + if len(open_corners) > 0: + return False, random.choice(open_corners) + + if 5 in possible_moves: + return False, 5 + + open_edges = [i for i in possible_moves if i in (2, 4, 6, 8)] + return False, random.choice(open_edges) + + def __str__(self) -> str: + """Return `AI` as user name.""" + return "AI" + + +class Game: + """Class that contains information and functions about Tic Tac Toe game.""" + + def __init__(self, players: list[Union[Player, AI]], ctx: Context): + self.players = players + self.ctx = ctx + self.board = { + 1: Emojis.number_emojis[1], + 2: Emojis.number_emojis[2], + 3: Emojis.number_emojis[3], + 4: Emojis.number_emojis[4], + 5: Emojis.number_emojis[5], + 6: Emojis.number_emojis[6], + 7: Emojis.number_emojis[7], + 8: Emojis.number_emojis[8], + 9: Emojis.number_emojis[9] + } + + self.current = self.players[0] + self.next = self.players[1] + + self.winner: Optional[Union[Player, AI]] = None + self.loser: Optional[Union[Player, AI]] = None + self.over = False + self.canceled = False + self.draw = False + + async def get_confirmation(self) -> tuple[bool, Optional[str]]: + """ + Ask does user want to play TicTacToe against requester. First player is always requester. + + This return tuple that have: + - first element boolean (is game accepted?) + - (optional, only when first element is False, otherwise None) reason for declining. + """ + confirm_message = await self.ctx.send( + CONFIRMATION_MESSAGE.format( + opponent=self.players[1].user.mention, + requester=self.players[0].user.mention + ) + ) + await confirm_message.add_reaction(Emojis.confirmation) + await confirm_message.add_reaction(Emojis.decline) + + def confirm_check(reaction: discord.Reaction, user: discord.User) -> bool: + """Check is user who reacted from who this was requested, message is confirmation and emoji is valid.""" + return ( + reaction.emoji in (Emojis.confirmation, Emojis.decline) + and reaction.message.id == confirm_message.id + and user == self.players[1].user + ) + + try: + reaction, user = await self.ctx.bot.wait_for( + "reaction_add", + timeout=60.0, + check=confirm_check + ) + except asyncio.TimeoutError: + self.over = True + self.canceled = True + await confirm_message.delete() + return False, "Running out of time... Cancelled game." + + await confirm_message.delete() + if reaction.emoji == Emojis.confirmation: + return True, None + else: + self.over = True + self.canceled = True + return False, "User declined" + + async def add_reactions(self, msg: discord.Message) -> None: + """Add number emojis to message.""" + for nr in Emojis.number_emojis.values(): + await msg.add_reaction(nr) + + def format_board(self) -> str: + """Get formatted tic-tac-toe board for message.""" + board = list(self.board.values()) + return "\n".join( + (f"{board[line]} {board[line + 1]} {board[line + 2]}" for line in range(0, len(board), 3)) + ) + + async def play(self) -> None: + """Start and handle game.""" + await self.ctx.send("It's time for the game! Let's begin.") + board = await self.ctx.send( + embed=discord.Embed(description=self.format_board()) + ) + await self.add_reactions(board) + + for _ in range(9): + if isinstance(self.current, Player): + announce = await self.ctx.send( + f"{self.current.user.mention}, it's your turn! " + "React with an emoji to take your go." + ) + timeout, pos = await self.current.get_move(self.board, board) + if isinstance(self.current, Player): + await announce.delete() + if timeout: + await self.ctx.send(f"{self.current.user.mention} ran out of time. Canceling game.") + self.over = True + self.canceled = True + return + self.board[pos] = self.current.symbol + await board.edit( + embed=discord.Embed(description=self.format_board()) + ) + await board.clear_reaction(Emojis.number_emojis[pos]) + if check_win(self.board): + self.winner = self.current + self.loser = self.next + await self.ctx.send( + f":tada: {self.current} won this game! :tada:" + ) + await board.clear_reactions() + break + self.current, self.next = self.next, self.current + if not self.winner: + self.draw = True + await self.ctx.send("It's a DRAW!") + self.over = True + + +def is_channel_free() -> Callable: + """Check is channel where command will be invoked free.""" + async def predicate(ctx: Context) -> bool: + return all(game.channel != ctx.channel for game in ctx.cog.games if not game.over) + return check(predicate) + + +def is_requester_free() -> Callable: + """Check is requester not already in any game.""" + async def predicate(ctx: Context) -> bool: + return all( + ctx.author not in (player.user for player in game.players) for game in ctx.cog.games if not game.over + ) + return check(predicate) + + +class TicTacToe(Cog): + """TicTacToe cog contains tic-tac-toe game commands.""" + + def __init__(self): + self.games: list[Game] = [] + + @guild_only() + @is_channel_free() + @is_requester_free() + @group(name="tictactoe", aliases=("ttt", "tic"), invoke_without_command=True) + async def tic_tac_toe(self, ctx: Context, opponent: Optional[discord.User]) -> None: + """Tic Tac Toe game. Play against friends or AI. Use reactions to add your mark to field.""" + if opponent == ctx.author: + await ctx.send("You can't play against yourself.") + return + if opponent is not None and not all( + opponent not in (player.user for player in g.players) for g in ctx.cog.games if not g.over + ): + await ctx.send("Opponent is already in game.") + return + if opponent is None: + game = Game( + [Player(ctx.author, ctx, Emojis.x_square), AI(Emojis.o_square)], + ctx + ) + else: + game = Game( + [Player(ctx.author, ctx, Emojis.x_square), Player(opponent, ctx, Emojis.o_square)], + ctx + ) + self.games.append(game) + if opponent is not None: + if opponent.bot: # check whether the opponent is a bot or not + await ctx.send("You can't play Tic-Tac-Toe with bots!") + return + + confirmed, msg = await game.get_confirmation() + + if not confirmed: + if msg: + await ctx.send(msg) + return + await game.play() + + @tic_tac_toe.group(name="history", aliases=("log",), invoke_without_command=True) + async def tic_tac_toe_logs(self, ctx: Context) -> None: + """Show most recent tic-tac-toe games.""" + if len(self.games) < 1: + await ctx.send("No recent games.") + return + log_games = [] + for i, game in enumerate(self.games): + if game.over and not game.canceled: + if game.draw: + log_games.append( + f"**#{i+1}**: {game.players[0]} vs {game.players[1]} (draw)" + ) + else: + log_games.append( + f"**#{i+1}**: {game.winner} :trophy: vs {game.loser}" + ) + await LinePaginator.paginate( + log_games, + ctx, + discord.Embed(title="Most recent Tic Tac Toe games") + ) + + @tic_tac_toe_logs.command(name="show", aliases=("s",)) + async def show_tic_tac_toe_board(self, ctx: Context, game_id: int) -> None: + """View game board by ID (ID is possible to get by `.tictactoe history`).""" + if len(self.games) < game_id: + await ctx.send("Game don't exist.") + return + game = self.games[game_id - 1] + + if game.draw: + description = f"{game.players[0]} vs {game.players[1]} (draw)\n\n{game.format_board()}" + else: + description = f"{game.winner} :trophy: vs {game.loser}\n\n{game.format_board()}" + + embed = discord.Embed( + title=f"Match #{game_id} Game Board", + description=description, + ) + await ctx.send(embed=embed) + + +def setup(bot: Bot) -> None: + """Load the TicTacToe cog.""" + bot.add_cog(TicTacToe()) diff --git a/bot/exts/fun/trivia_quiz.py b/bot/exts/fun/trivia_quiz.py new file mode 100644 index 00000000..cf9e6cd3 --- /dev/null +++ b/bot/exts/fun/trivia_quiz.py @@ -0,0 +1,593 @@ +import asyncio +import json +import logging +import operator +import random +from dataclasses import dataclass +from pathlib import Path +from typing import Callable, Optional + +import discord +from discord.ext import commands +from rapidfuzz import fuzz + +from bot.bot import Bot +from bot.constants import Colours, NEGATIVE_REPLIES, Roles + +logger = logging.getLogger(__name__) + +DEFAULT_QUESTION_LIMIT = 6 +STANDARD_VARIATION_TOLERANCE = 88 +DYNAMICALLY_GEN_VARIATION_TOLERANCE = 97 + +WRONG_ANS_RESPONSE = [ + "No one answered correctly!", + "Better luck next time...", +] + +N_PREFIX_STARTS_AT = 5 +N_PREFIXES = [ + "penta", "hexa", "hepta", "octa", "nona", + "deca", "hendeca", "dodeca", "trideca", "tetradeca", +] + +PLANETS = [ + ("1st", "Mercury"), + ("2nd", "Venus"), + ("3rd", "Earth"), + ("4th", "Mars"), + ("5th", "Jupiter"), + ("6th", "Saturn"), + ("7th", "Uranus"), + ("8th", "Neptune"), +] + +TAXONOMIC_HIERARCHY = [ + "species", "genus", "family", "order", + "class", "phylum", "kingdom", "domain", +] + +UNITS_TO_BASE_UNITS = { + "hertz": ("(unit of frequency)", "s^-1"), + "newton": ("(unit of force)", "m*kg*s^-2"), + "pascal": ("(unit of pressure & stress)", "m^-1*kg*s^-2"), + "joule": ("(unit of energy & quantity of heat)", "m^2*kg*s^-2"), + "watt": ("(unit of power)", "m^2*kg*s^-3"), + "coulomb": ("(unit of electric charge & quantity of electricity)", "s*A"), + "volt": ("(unit of voltage & electromotive force)", "m^2*kg*s^-3*A^-1"), + "farad": ("(unit of capacitance)", "m^-2*kg^-1*s^4*A^2"), + "ohm": ("(unit of electric resistance)", "m^2*kg*s^-3*A^-2"), + "weber": ("(unit of magnetic flux)", "m^2*kg*s^-2*A^-1"), + "tesla": ("(unit of magnetic flux density)", "kg*s^-2*A^-1"), +} + + +@dataclass(frozen=True) +class QuizEntry: + """Dataclass for a quiz entry (a question and a string containing answers separated by commas).""" + + question: str + answer: str + + +def linear_system(q_format: str, a_format: str) -> QuizEntry: + """Generate a system of linear equations with two unknowns.""" + x, y = random.randint(2, 5), random.randint(2, 5) + answer = a_format.format(x, y) + + coeffs = random.sample(range(1, 6), 4) + + question = q_format.format( + coeffs[0], + coeffs[1], + coeffs[0] * x + coeffs[1] * y, + coeffs[2], + coeffs[3], + coeffs[2] * x + coeffs[3] * y, + ) + + return QuizEntry(question, answer) + + +def mod_arith(q_format: str, a_format: str) -> QuizEntry: + """Generate a basic modular arithmetic question.""" + quotient, m, b = random.randint(30, 40), random.randint(10, 20), random.randint(200, 350) + ans = random.randint(0, 9) # max remainder is 9, since the minimum modulus is 10 + a = quotient * m + ans - b + + question = q_format.format(a, b, m) + answer = a_format.format(ans) + + return QuizEntry(question, answer) + + +def ngonal_prism(q_format: str, a_format: str) -> QuizEntry: + """Generate a question regarding vertices on n-gonal prisms.""" + n = random.randint(0, len(N_PREFIXES) - 1) + + question = q_format.format(N_PREFIXES[n]) + answer = a_format.format((n + N_PREFIX_STARTS_AT) * 2) + + return QuizEntry(question, answer) + + +def imag_sqrt(q_format: str, a_format: str) -> QuizEntry: + """Generate a negative square root question.""" + ans_coeff = random.randint(3, 10) + + question = q_format.format(ans_coeff ** 2) + answer = a_format.format(ans_coeff) + + return QuizEntry(question, answer) + + +def binary_calc(q_format: str, a_format: str) -> QuizEntry: + """Generate a binary calculation question.""" + a = random.randint(15, 20) + b = random.randint(10, a) + oper = random.choice( + ( + ("+", operator.add), + ("-", operator.sub), + ("*", operator.mul), + ) + ) + + # if the operator is multiplication, lower the values of the two operands to make it easier + if oper[0] == "*": + a -= 5 + b -= 5 + + question = q_format.format(a, oper[0], b) + answer = a_format.format(oper[1](a, b)) + + return QuizEntry(question, answer) + + +def solar_system(q_format: str, a_format: str) -> QuizEntry: + """Generate a question on the planets of the Solar System.""" + planet = random.choice(PLANETS) + + question = q_format.format(planet[0]) + answer = a_format.format(planet[1]) + + return QuizEntry(question, answer) + + +def taxonomic_rank(q_format: str, a_format: str) -> QuizEntry: + """Generate a question on taxonomic classification.""" + level = random.randint(0, len(TAXONOMIC_HIERARCHY) - 2) + + question = q_format.format(TAXONOMIC_HIERARCHY[level]) + answer = a_format.format(TAXONOMIC_HIERARCHY[level + 1]) + + return QuizEntry(question, answer) + + +def base_units_convert(q_format: str, a_format: str) -> QuizEntry: + """Generate a SI base units conversion question.""" + unit = random.choice(list(UNITS_TO_BASE_UNITS)) + + question = q_format.format( + unit + " " + UNITS_TO_BASE_UNITS[unit][0] + ) + answer = a_format.format( + UNITS_TO_BASE_UNITS[unit][1] + ) + + return QuizEntry(question, answer) + + +DYNAMIC_QUESTIONS_FORMAT_FUNCS = { + 201: linear_system, + 202: mod_arith, + 203: ngonal_prism, + 204: imag_sqrt, + 205: binary_calc, + 301: solar_system, + 302: taxonomic_rank, + 303: base_units_convert, +} + + +class TriviaQuiz(commands.Cog): + """A cog for all quiz commands.""" + + def __init__(self, bot: Bot): + self.bot = bot + + self.game_status = {} # A variable to store the game status: either running or not running. + self.game_owners = {} # A variable to store the person's ID who started the quiz game in a channel. + + self.questions = self.load_questions() + self.question_limit = 0 + + self.player_scores = {} # A variable to store all player's scores for a bot session. + self.game_player_scores = {} # A variable to store temporary game player's scores. + + self.categories = { + "general": "Test your general knowledge.", + "retro": "Questions related to retro gaming.", + "math": "General questions about mathematics ranging from grade 8 to grade 12.", + "science": "Put your understanding of science to the test!", + "cs": "A large variety of computer science questions.", + "python": "Trivia on our amazing language, Python!", + } + + @staticmethod + def load_questions() -> dict: + """Load the questions from the JSON file.""" + p = Path("bot", "resources", "fun", "trivia_quiz.json") + + return json.loads(p.read_text(encoding="utf-8")) + + @commands.group(name="quiz", aliases=["trivia"], invoke_without_command=True) + async def quiz_game(self, ctx: commands.Context, category: Optional[str], questions: Optional[int]) -> None: + """ + Start a quiz! + + Questions for the quiz can be selected from the following categories: + - general: Test your general knowledge. + - retro: Questions related to retro gaming. + - math: General questions about mathematics ranging from grade 8 to grade 12. + - science: Put your understanding of science to the test! + - cs: A large variety of computer science questions. + - python: Trivia on our amazing language, Python! + + (More to come!) + """ + if ctx.channel.id not in self.game_status: + self.game_status[ctx.channel.id] = False + + if ctx.channel.id not in self.game_player_scores: + self.game_player_scores[ctx.channel.id] = {} + + # Stop game if running. + if self.game_status[ctx.channel.id]: + await ctx.send( + "Game is already running... " + f"do `{self.bot.command_prefix}quiz stop`" + ) + return + + # Send embed showing available categories if inputted category is invalid. + if category is None: + category = random.choice(list(self.categories)) + + category = category.lower() + if category not in self.categories: + embed = self.category_embed() + await ctx.send(embed=embed) + return + + topic = self.questions[category] + topic_length = len(topic) + + if questions is None: + self.question_limit = DEFAULT_QUESTION_LIMIT + else: + if questions > topic_length: + await ctx.send( + embed=self.make_error_embed( + f"This category only has {topic_length} questions. " + "Please input a lower value!" + ) + ) + return + + elif questions < 1: + await ctx.send( + embed=self.make_error_embed( + "You must choose to complete at least one question. " + f"(or enter nothing for the default value of {DEFAULT_QUESTION_LIMIT + 1} questions)" + ) + ) + return + + else: + self.question_limit = questions - 1 + + # Start game if not running. + if not self.game_status[ctx.channel.id]: + self.game_owners[ctx.channel.id] = ctx.author + self.game_status[ctx.channel.id] = True + start_embed = self.make_start_embed(category) + + await ctx.send(embed=start_embed) # send an embed with the rules + await asyncio.sleep(5) + + done_question = [] + hint_no = 0 + answers = None + + while self.game_status[ctx.channel.id]: + # Exit quiz if number of questions for a round are already sent. + if len(done_question) > self.question_limit and hint_no == 0: + await ctx.send("The round has ended.") + await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id]) + + self.game_status[ctx.channel.id] = False + del self.game_owners[ctx.channel.id] + self.game_player_scores[ctx.channel.id] = {} + + break + + # If no hint has been sent or any time alert. Basically if hint_no = 0 means it is a new question. + if hint_no == 0: + # Select a random question which has not been used yet. + while True: + question_dict = random.choice(topic) + if question_dict["id"] not in done_question: + done_question.append(question_dict["id"]) + break + + if "dynamic_id" not in question_dict: + question = question_dict["question"] + answers = question_dict["answer"].split(", ") + + var_tol = STANDARD_VARIATION_TOLERANCE + else: + format_func = DYNAMIC_QUESTIONS_FORMAT_FUNCS[question_dict["dynamic_id"]] + + quiz_entry = format_func( + question_dict["question"], + question_dict["answer"], + ) + + question, answers = quiz_entry.question, quiz_entry.answer + answers = [answers] + + var_tol = DYNAMICALLY_GEN_VARIATION_TOLERANCE + + embed = discord.Embed( + colour=Colours.gold, + title=f"Question #{len(done_question)}", + description=question, + ) + + if img_url := question_dict.get("img_url"): + embed.set_image(url=img_url) + + await ctx.send(embed=embed) + + def check_func(variation_tolerance: int) -> Callable[[discord.Message], bool]: + def contains_correct_answer(m: discord.Message) -> bool: + return m.channel == ctx.channel and any( + fuzz.ratio(answer.lower(), m.content.lower()) > variation_tolerance + for answer in answers + ) + + return contains_correct_answer + + try: + msg = await self.bot.wait_for("message", check=check_func(var_tol), timeout=10) + except asyncio.TimeoutError: + # In case of TimeoutError and the game has been stopped, then do nothing. + if not self.game_status[ctx.channel.id]: + break + + if hint_no < 2: + hint_no += 1 + + if "hints" in question_dict: + hints = question_dict["hints"] + + await ctx.send(f"**Hint #{hint_no}\n**{hints[hint_no - 1]}") + else: + await ctx.send(f"{30 - hint_no * 10}s left!") + + # Once hint or time alerts has been sent 2 times, the hint_no value will be 3 + # If hint_no > 2, then it means that all hints/time alerts have been sent. + # Also means that the answer is not yet given and the bot sends the answer and the next question. + else: + if self.game_status[ctx.channel.id] is False: + break + + response = random.choice(WRONG_ANS_RESPONSE) + await ctx.send(response) + + await self.send_answer( + ctx.channel, + answers, + False, + question_dict, + self.question_limit - len(done_question) + 1, + ) + await asyncio.sleep(1) + + hint_no = 0 # Reset the hint counter so that on the next round, it's in the initial state + + await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id]) + await asyncio.sleep(2) + else: + if self.game_status[ctx.channel.id] is False: + break + + points = 100 - 25 * hint_no + if msg.author in self.game_player_scores[ctx.channel.id]: + self.game_player_scores[ctx.channel.id][msg.author] += points + else: + self.game_player_scores[ctx.channel.id][msg.author] = points + + # Also updating the overall scoreboard. + if msg.author in self.player_scores: + self.player_scores[msg.author] += points + else: + self.player_scores[msg.author] = points + + hint_no = 0 + + await ctx.send(f"{msg.author.mention} got the correct answer :tada: {points} points!") + + await self.send_answer( + ctx.channel, + answers, + True, + question_dict, + self.question_limit - len(done_question) + 1, + ) + await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id]) + + await asyncio.sleep(2) + + def make_start_embed(self, category: str) -> discord.Embed: + """Generate a starting/introduction embed for the quiz.""" + start_embed = discord.Embed( + colour=Colours.blue, + title="A quiz game is starting!", + description=( + f"This game consists of {self.question_limit + 1} questions.\n\n" + "**Rules: **\n" + "1. Only enclose your answer in backticks when the question tells you to.\n" + "2. If the question specifies an answer format, follow it or else it won't be accepted.\n" + "3. You have 30s per question. Points for each question reduces by 25 after 10s or after a hint.\n" + "4. No cheating and have fun!\n\n" + f"**Category**: {category}" + ), + ) + + return start_embed + + @staticmethod + def make_error_embed(desc: str) -> discord.Embed: + """Generate an error embed with the given description.""" + error_embed = discord.Embed( + colour=Colours.soft_red, + title=random.choice(NEGATIVE_REPLIES), + description=desc, + ) + + return error_embed + + @quiz_game.command(name="stop") + async def stop_quiz(self, ctx: commands.Context) -> None: + """ + Stop a quiz game if its running in the channel. + + Note: Only mods or the owner of the quiz can stop it. + """ + try: + if self.game_status[ctx.channel.id]: + # Check if the author is the game starter or a moderator. + if ctx.author == self.game_owners[ctx.channel.id] or any( + Roles.moderator == role.id for role in ctx.author.roles + ): + self.game_status[ctx.channel.id] = False + del self.game_owners[ctx.channel.id] + self.game_player_scores[ctx.channel.id] = {} + + await ctx.send("Quiz stopped.") + await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id]) + + else: + await ctx.send(f"{ctx.author.mention}, you are not authorised to stop this game :ghost:!") + else: + await ctx.send("No quiz running.") + except KeyError: + await ctx.send("No quiz running.") + + @quiz_game.command(name="leaderboard") + async def leaderboard(self, ctx: commands.Context) -> None: + """View everyone's score for this bot session.""" + await self.send_score(ctx.channel, self.player_scores) + + @staticmethod + async def send_score(channel: discord.TextChannel, player_data: dict) -> None: + """Send the current scores of players in the game channel.""" + if len(player_data) == 0: + await channel.send("No one has made it onto the leaderboard yet.") + return + + embed = discord.Embed( + colour=Colours.blue, + title="Score Board", + description="", + ) + + sorted_dict = sorted(player_data.items(), key=operator.itemgetter(1), reverse=True) + for item in sorted_dict: + embed.description += f"{item[0]}: {item[1]}\n" + + await channel.send(embed=embed) + + @staticmethod + async def declare_winner(channel: discord.TextChannel, player_data: dict) -> None: + """Announce the winner of the quiz in the game channel.""" + if player_data: + highest_points = max(list(player_data.values())) + no_of_winners = list(player_data.values()).count(highest_points) + + # Check if more than 1 player has highest points. + if no_of_winners > 1: + winners = [] + points_copy = list(player_data.values()).copy() + + for _ in range(no_of_winners): + index = points_copy.index(highest_points) + winners.append(list(player_data.keys())[index]) + points_copy[index] = 0 + + winners_mention = " ".join(winner.mention for winner in winners) + else: + author_index = list(player_data.values()).index(highest_points) + winner = list(player_data.keys())[author_index] + winners_mention = winner.mention + + await channel.send( + f"Congratulations {winners_mention} :tada: " + f"You have won this quiz game with a grand total of {highest_points} points!" + ) + + def category_embed(self) -> discord.Embed: + """Build an embed showing all available trivia categories.""" + embed = discord.Embed( + colour=Colours.blue, + title="The available question categories are:", + description="", + ) + + embed.set_footer(text="If a category is not chosen, a random one will be selected.") + + for cat, description in self.categories.items(): + embed.description += ( + f"**- {cat.capitalize()}**\n" + f"{description.capitalize()}\n" + ) + + return embed + + @staticmethod + async def send_answer( + channel: discord.TextChannel, + answers: list[str], + answer_is_correct: bool, + question_dict: dict, + q_left: int, + ) -> None: + """Send the correct answer of a question to the game channel.""" + info = question_dict.get("info") + + plurality = " is" if len(answers) == 1 else "s are" + + embed = discord.Embed( + color=Colours.bright_green, + title=( + ("You got it! " if answer_is_correct else "") + + f"The correct answer{plurality} **`{', '.join(answers)}`**\n" + ), + description="", + ) + + if info is not None: + embed.description += f"**Information**\n{info}\n\n" + + embed.description += ( + ("Let's move to the next question." if q_left > 0 else "") + + f"\nRemaining questions: {q_left}" + ) + await channel.send(embed=embed) + + +def setup(bot: Bot) -> None: + """Load the TriviaQuiz cog.""" + bot.add_cog(TriviaQuiz(bot)) diff --git a/bot/exts/fun/wonder_twins.py b/bot/exts/fun/wonder_twins.py new file mode 100644 index 00000000..79d6b6d9 --- /dev/null +++ b/bot/exts/fun/wonder_twins.py @@ -0,0 +1,49 @@ +import random +from pathlib import Path + +import yaml +from discord.ext.commands import Cog, Context, command + +from bot.bot import Bot + + +class WonderTwins(Cog): + """Cog for a Wonder Twins inspired command.""" + + def __init__(self): + with open(Path.cwd() / "bot" / "resources" / "fun" / "wonder_twins.yaml", "r", encoding="utf-8") as f: + info = yaml.load(f, Loader=yaml.FullLoader) + self.water_types = info["water_types"] + self.objects = info["objects"] + self.adjectives = info["adjectives"] + + @staticmethod + def append_onto(phrase: str, insert_word: str) -> str: + """Appends one word onto the end of another phrase in order to format with the proper determiner.""" + if insert_word.endswith("s"): + phrase = phrase.split() + del phrase[0] + phrase = " ".join(phrase) + + insert_word = insert_word.split()[-1] + return " ".join([phrase, insert_word]) + + def format_phrase(self) -> str: + """Creates a transformation phrase from available words.""" + adjective = random.choice((None, random.choice(self.adjectives))) + object_name = random.choice(self.objects) + water_type = random.choice(self.water_types) + + if adjective: + object_name = self.append_onto(adjective, object_name) + return f"{object_name} of {water_type}" + + @command(name="formof", aliases=("wondertwins", "wondertwin", "fo")) + async def form_of(self, ctx: Context) -> None: + """Command to send a Wonder Twins inspired phrase to the user invoking the command.""" + await ctx.send(f"Form of {self.format_phrase()}!") + + +def setup(bot: Bot) -> None: + """Load the WonderTwins cog.""" + bot.add_cog(WonderTwins()) diff --git a/bot/exts/fun/xkcd.py b/bot/exts/fun/xkcd.py new file mode 100644 index 00000000..b56c53d9 --- /dev/null +++ b/bot/exts/fun/xkcd.py @@ -0,0 +1,91 @@ +import logging +import re +from random import randint +from typing import Optional, Union + +from discord import Embed +from discord.ext import tasks +from discord.ext.commands import Cog, Context, command + +from bot.bot import Bot +from bot.constants import Colours + +log = logging.getLogger(__name__) + +COMIC_FORMAT = re.compile(r"latest|[0-9]+") +BASE_URL = "https://xkcd.com" + + +class XKCD(Cog): + """Retrieving XKCD comics.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.latest_comic_info: dict[str, Union[str, int]] = {} + self.get_latest_comic_info.start() + + def cog_unload(self) -> None: + """Cancels refreshing of the task for refreshing the most recent comic info.""" + self.get_latest_comic_info.cancel() + + @tasks.loop(minutes=30) + async def get_latest_comic_info(self) -> None: + """Refreshes latest comic's information ever 30 minutes. Also used for finding a random comic.""" + async with self.bot.http_session.get(f"{BASE_URL}/info.0.json") as resp: + if resp.status == 200: + self.latest_comic_info = await resp.json() + else: + log.debug(f"Failed to get latest XKCD comic information. Status code {resp.status}") + + @command(name="xkcd") + async def fetch_xkcd_comics(self, ctx: Context, comic: Optional[str]) -> None: + """ + Getting an xkcd comic's information along with the image. + + To get a random comic, don't type any number as an argument. To get the latest, type 'latest'. + """ + embed = Embed(title=f"XKCD comic '{comic}'") + + embed.colour = Colours.soft_red + + if comic and (comic := re.match(COMIC_FORMAT, comic)) is None: + embed.description = "Comic parameter should either be an integer or 'latest'." + await ctx.send(embed=embed) + return + + comic = randint(1, self.latest_comic_info["num"]) if comic is None else comic.group(0) + + if comic == "latest": + info = self.latest_comic_info + else: + async with self.bot.http_session.get(f"{BASE_URL}/{comic}/info.0.json") as resp: + if resp.status == 200: + info = await resp.json() + else: + embed.title = f"XKCD comic #{comic}" + embed.description = f"{resp.status}: Could not retrieve xkcd comic #{comic}." + log.debug(f"Retrieving xkcd comic #{comic} failed with status code {resp.status}.") + await ctx.send(embed=embed) + return + + embed.title = f"XKCD comic #{info['num']}" + embed.description = info["alt"] + embed.url = f"{BASE_URL}/{info['num']}" + + if info["img"][-3:] in ("jpg", "png", "gif"): + embed.set_image(url=info["img"]) + date = f"{info['year']}/{info['month']}/{info['day']}" + embed.set_footer(text=f"{date} - #{info['num']}, \'{info['safe_title']}\'") + embed.colour = Colours.soft_green + else: + embed.description = ( + "The selected comic is interactive, and cannot be displayed within an embed.\n" + f"Comic can be viewed [here](https://xkcd.com/{info['num']})." + ) + + await ctx.send(embed=embed) + + +def setup(bot: Bot) -> None: + """Load the XKCD cog.""" + bot.add_cog(XKCD(bot)) diff --git a/bot/resources/evergreen/LuckiestGuy-Regular.ttf b/bot/resources/evergreen/LuckiestGuy-Regular.ttf deleted file mode 100644 index 8c79c875..00000000 Binary files a/bot/resources/evergreen/LuckiestGuy-Regular.ttf and /dev/null differ diff --git a/bot/resources/evergreen/all_cards.png b/bot/resources/evergreen/all_cards.png deleted file mode 100644 index 10ed2eb8..00000000 Binary files a/bot/resources/evergreen/all_cards.png and /dev/null differ diff --git a/bot/resources/evergreen/caesar_info.json b/bot/resources/evergreen/caesar_info.json deleted file mode 100644 index 8229c4f3..00000000 --- a/bot/resources/evergreen/caesar_info.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Caesar Cipher", - "description": "**Information**\nThe Caesar Cipher, named after the Roman General Julius Caesar, is one of the simplest and most widely known encryption techniques. It is a type of substitution cipher in which each letter in the plaintext is replaced by a letter given a specific position offset in the alphabet, with the letters wrapping around both sides.\n\n**Examples**\n1) `Hello World` <=> `Khoor Zruog` where letters are shifted forwards by `3`.\n2) `Julius Caesar` <=> `Yjaxjh Rpthpg` where letters are shifted backwards by `11`." -} diff --git a/bot/resources/evergreen/ducks_help_ex.png b/bot/resources/evergreen/ducks_help_ex.png deleted file mode 100644 index 01d9c243..00000000 Binary files a/bot/resources/evergreen/ducks_help_ex.png and /dev/null differ diff --git a/bot/resources/evergreen/game_recs/chrono_trigger.json b/bot/resources/evergreen/game_recs/chrono_trigger.json deleted file mode 100644 index 9720b977..00000000 --- a/bot/resources/evergreen/game_recs/chrono_trigger.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Chrono Trigger", - "description": "One of the best games of all time. A brilliant story involving time-travel with loveable characters. It has a brilliant score by Yasonuri Mitsuda and artwork by Akira Toriyama. With over 20 endings and New Game+, there is a huge amount of replay value here.", - "link": "https://rawg.io/games/chrono-trigger-1995", - "image": "https://vignette.wikia.nocookie.net/chrono/images/2/24/Chrono_Trigger_cover.jpg", - "author": "352635617709916161" -} diff --git a/bot/resources/evergreen/game_recs/digimon_world.json b/bot/resources/evergreen/game_recs/digimon_world.json deleted file mode 100644 index c1cb4f37..00000000 --- a/bot/resources/evergreen/game_recs/digimon_world.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Digimon World", - "description": "A great mix of town-building and pet-raising set in the Digimon universe. With plenty of Digimon to raise and recruit to the village, this charming game will keep you occupied for a long time.", - "image": "https://www.mobygames.com/images/covers/l/437308-digimon-world-playstation-front-cover.jpg", - "link": "https://rawg.io/games/digimon-world", - "author": "352635617709916161" -} diff --git a/bot/resources/evergreen/game_recs/doom_2.json b/bot/resources/evergreen/game_recs/doom_2.json deleted file mode 100644 index b60cc05f..00000000 --- a/bot/resources/evergreen/game_recs/doom_2.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Doom II", - "description": "Doom 2 was one of the first FPS games that I truly enjoyed. It offered awesome weapons, terrifying demons to kill, and a great atmosphere to do it in.", - "image": "https://upload.wikimedia.org/wikipedia/en/thumb/2/29/Doom_II_-_Hell_on_Earth_Coverart.png/220px-Doom_II_-_Hell_on_Earth_Coverart.png", - "link": "https://rawg.io/games/doom-ii", - "author": "352635617709916161" -} diff --git a/bot/resources/evergreen/game_recs/skyrim.json b/bot/resources/evergreen/game_recs/skyrim.json deleted file mode 100644 index ad86db31..00000000 --- a/bot/resources/evergreen/game_recs/skyrim.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "Elder Scrolls V: Skyrim", - "description": "The latest mainline Elder Scrolls game offered a fantastic role-playing experience with untethered freedom and a great story. Offering vast mod support, the game has endless customization and replay value.", - "image": "https://upload.wikimedia.org/wikipedia/en/1/15/The_Elder_Scrolls_V_Skyrim_cover.png", - "link": "https://rawg.io/games/the-elder-scrolls-v-skyrim", - "author": "352635617709916161" -} diff --git a/bot/resources/evergreen/html_colours.json b/bot/resources/evergreen/html_colours.json deleted file mode 100644 index 086083d6..00000000 --- a/bot/resources/evergreen/html_colours.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "aliceblue": "0xf0f8ff", - "antiquewhite": "0xfaebd7", - "aqua": "0x00ffff", - "aquamarine": "0x7fffd4", - "azure": "0xf0ffff", - "beige": "0xf5f5dc", - "bisque": "0xffe4c4", - "black": "0x000000", - "blanchedalmond": "0xffebcd", - "blue": "0x0000ff", - "blueviolet": "0x8a2be2", - "brown": "0xa52a2a", - "burlywood": "0xdeb887", - "cadetblue": "0x5f9ea0", - "chartreuse": "0x7fff00", - "chocolate": "0xd2691e", - "coral": "0xff7f50", - "cornflowerblue": "0x6495ed", - "cornsilk": "0xfff8dc", - "crimson": "0xdc143c", - "cyan": "0x00ffff", - "darkblue": "0x00008b", - "darkcyan": "0x008b8b", - "darkgoldenrod": "0xb8860b", - "darkgray": "0xa9a9a9", - "darkgreen": "0x006400", - "darkgrey": "0xa9a9a9", - "darkkhaki": "0xbdb76b", - "darkmagenta": "0x8b008b", - "darkolivegreen": "0x556b2f", - "darkorange": "0xff8c00", - "darkorchid": "0x9932cc", - "darkred": "0x8b0000", - "darksalmon": "0xe9967a", - "darkseagreen": "0x8fbc8f", - "darkslateblue": "0x483d8b", - "darkslategray": "0x2f4f4f", - "darkslategrey": "0x2f4f4f", - "darkturquoise": "0x00ced1", - "darkviolet": "0x9400d3", - "deeppink": "0xff1493", - "deepskyblue": "0x00bfff", - "dimgray": "0x696969", - "dimgrey": "0x696969", - "dodgerblue": "0x1e90ff", - "firebrick": "0xb22222", - "floralwhite": "0xfffaf0", - "forestgreen": "0x228b22", - "fuchsia": "0xff00ff", - "gainsboro": "0xdcdcdc", - "ghostwhite": "0xf8f8ff", - "goldenrod": "0xdaa520", - "gold": "0xffd700", - "gray": "0x808080", - "green": "0x008000", - "greenyellow": "0xadff2f", - "grey": "0x808080", - "honeydew": "0xf0fff0", - "hotpink": "0xff69b4", - "indianred": "0xcd5c5c", - "indigo": "0x4b0082", - "ivory": "0xfffff0", - "khaki": "0xf0e68c", - "lavenderblush": "0xfff0f5", - "lavender": "0xe6e6fa", - "lawngreen": "0x7cfc00", - "lemonchiffon": "0xfffacd", - "lightblue": "0xadd8e6", - "lightcoral": "0xf08080", - "lightcyan": "0xe0ffff", - "lightgoldenrodyellow": "0xfafad2", - "lightgray": "0xd3d3d3", - "lightgreen": "0x90ee90", - "lightgrey": "0xd3d3d3", - "lightpink": "0xffb6c1", - "lightsalmon": "0xffa07a", - "lightseagreen": "0x20b2aa", - "lightskyblue": "0x87cefa", - "lightslategray": "0x778899", - "lightslategrey": "0x778899", - "lightsteelblue": "0xb0c4de", - "lightyellow": "0xffffe0", - "lime": "0x00ff00", - "limegreen": "0x32cd32", - "linen": "0xfaf0e6", - "magenta": "0xff00ff", - "maroon": "0x800000", - "mediumaquamarine": "0x66cdaa", - "mediumblue": "0x0000cd", - "mediumorchid": "0xba55d3", - "mediumpurple": "0x9370db", - "mediumseagreen": "0x3cb371", - "mediumslateblue": "0x7b68ee", - "mediumspringgreen": "0x00fa9a", - "mediumturquoise": "0x48d1cc", - "mediumvioletred": "0xc71585", - "midnightblue": "0x191970", - "mintcream": "0xf5fffa", - "mistyrose": "0xffe4e1", - "moccasin": "0xffe4b5", - "navajowhite": "0xffdead", - "navy": "0x000080", - "oldlace": "0xfdf5e6", - "olive": "0x808000", - "olivedrab": "0x6b8e23", - "orange": "0xffa500", - "orangered": "0xff4500", - "orchid": "0xda70d6", - "palegoldenrod": "0xeee8aa", - "palegreen": "0x98fb98", - "paleturquoise": "0xafeeee", - "palevioletred": "0xdb7093", - "papayawhip": "0xffefd5", - "peachpuff": "0xffdab9", - "peru": "0xcd853f", - "pink": "0xffc0cb", - "plum": "0xdda0dd", - "powderblue": "0xb0e0e6", - "purple": "0x800080", - "rebeccapurple": "0x663399", - "red": "0xff0000", - "rosybrown": "0xbc8f8f", - "royalblue": "0x4169e1", - "saddlebrown": "0x8b4513", - "salmon": "0xfa8072", - "sandybrown": "0xf4a460", - "seagreen": "0x2e8b57", - "seashell": "0xfff5ee", - "sienna": "0xa0522d", - "silver": "0xc0c0c0", - "skyblue": "0x87ceeb", - "slateblue": "0x6a5acd", - "slategray": "0x708090", - "slategrey": "0x708090", - "snow": "0xfffafa", - "springgreen": "0x00ff7f", - "steelblue": "0x4682b4", - "tan": "0xd2b48c", - "teal": "0x008080", - "thistle": "0xd8bfd8", - "tomato": "0xff6347", - "turquoise": "0x40e0d0", - "violet": "0xee82ee", - "wheat": "0xf5deb3", - "white": "0xffffff", - "whitesmoke": "0xf5f5f5", - "yellow": "0xffff00", - "yellowgreen": "0x9acd32" -} diff --git a/bot/resources/evergreen/magic8ball.json b/bot/resources/evergreen/magic8ball.json deleted file mode 100644 index f5f1df62..00000000 --- a/bot/resources/evergreen/magic8ball.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - "It is certain", - "It is decidedly so", - "Without a doubt", - "Yes definitely", - "You may rely on it", - "As I see it, yes", - "Most likely", - "Outlook good", - "Yes", - "Signs point to yes", - "Reply hazy try again", - "Ask again later", - "Better not tell you now", - "Cannot predict now", - "Concentrate and ask again", - "Don't count on it", - "My reply is no", - "My sources say no", - "Outlook not so good", - "Very doubtful" -] diff --git a/bot/resources/evergreen/speedrun_links.json b/bot/resources/evergreen/speedrun_links.json deleted file mode 100644 index acb5746a..00000000 --- a/bot/resources/evergreen/speedrun_links.json +++ /dev/null @@ -1,18 +0,0 @@ - [ - "https://www.youtube.com/watch?v=jNE28SDXdyQ", - "https://www.youtube.com/watch?v=iI8Giq7zQDk", - "https://www.youtube.com/watch?v=VqNnkqQgFbc", - "https://www.youtube.com/watch?v=Gum4GI2Jr0s", - "https://www.youtube.com/watch?v=5YHjHzHJKkU", - "https://www.youtube.com/watch?v=X0pJSTy4tJI", - "https://www.youtube.com/watch?v=aVFq0H6D6_M", - "https://www.youtube.com/watch?v=1O6LuJbEbSI", - "https://www.youtube.com/watch?v=Bgh30BiWG58", - "https://www.youtube.com/watch?v=wwvgAAvhxM8", - "https://www.youtube.com/watch?v=0TWQr0_fi80", - "https://www.youtube.com/watch?v=hatqZby-0to", - "https://www.youtube.com/watch?v=tmnMq2Hw72w", - "https://www.youtube.com/watch?v=UTkyeTCAucA", - "https://www.youtube.com/watch?v=67kQ3l-1qMs", - "https://www.youtube.com/watch?v=14wqBA5Q1yc" -] diff --git a/bot/resources/evergreen/trivia_quiz.json b/bot/resources/evergreen/trivia_quiz.json deleted file mode 100644 index 8008838c..00000000 --- a/bot/resources/evergreen/trivia_quiz.json +++ /dev/null @@ -1,912 +0,0 @@ -{ - "retro": [ - { - "id": 1, - "hints": [ - "It is not a mainline Mario Game, although the plumber is present.", - "It is not a mainline Zelda Game, although Link is present." - ], - "question": "What is the best selling game on the Nintendo GameCube?", - "answer": "Super Smash Bros" - }, - { - "id": 2, - "hints": [ - "It was released before the 90's.", - "It was released after 1980." - ], - "question": "What year was Tetris released?", - "answer": "1984" - }, - { - "id": 3, - "hints": [ - "The occupation was in construction", - "He appeared as this kind of worker in 1981's Donkey Kong" - ], - "question": "What was Mario's original occupation?", - "answer": "Carpenter" - }, - { - "id": 4, - "hints": [ - "It was revealed in the Nintendo Character Guide in 1993.", - "His last name has to do with eating Mario's enemies." - ], - "question": "What is Yoshi's (from Mario Bros.) full name?", - "answer": "Yoshisaur Munchakoopas" - }, - { - "id": 5, - "hints": [ - "The game was released in 1990.", - "It was released on the SNES." - ], - "question": "What was the first game Yoshi appeared in?", - "answer": "Super Mario World" - }, - { - "id": 6, - "hints": [ - "They were used alternatively to playing cards.", - "They generally have handdrawn nature images on them." - ], - "question": "What did Nintendo make before video games and toys?", - "answer": "Hanafuda, Hanafuda cards" - }, - { - "id": 7, - "hints": [ - "Before being Nintendo's main competitor in home gaming, they were successful in arcades.", - "Their first console was called the Master System." - ], - "question": "Who was Nintendo's biggest competitor in 1990?", - "answer": "Sega" - } - ], - "general": [ - { - "id": 100, - "question": "Name \"the land of a thousand lakes\"", - "answer": "Finland", - "info": "Finland is a country in Northern Europe. Sweden borders it to the northwest, Estonia to the south, Russia to the east, and Norway to the north. Finland is part of the European Union with its capital city being Helsinki. With a population of 5.5 million people, it has over 187,000 lakes. The thousands of lakes in Finland are the reason why the country's nickname is \"the land of a thousand lakes.\"" - }, - { - "id": 101, - "question": "Who was the winner of FIFA 2018?", - "answer": "France", - "info": "France 4 - 2 Croatia" - }, - { - "id": 102, - "question": "What is the largest ocean in the world?", - "answer": "Pacific", - "info": "The Pacific Ocean is the largest and deepest of the world ocean basins. Covering approximately 63 million square miles and containing more than half of the free water on Earth, the Pacific is by far the largest of the world's ocean basins." - }, - { - "id": 103, - "question": "Who gifted the Statue Of Liberty?", - "answer": "France", - "info": "The Statue of Liberty was a gift from the French people commemorating the alliance of France and the United States during the American Revolution. Yet, it represented much more to those individuals who proposed the gift." - }, - { - "id": 104, - "question": "Which country is known as the \"Land Of The Rising Sun\"?", - "answer": "Japan", - "info": "The title stems from the Japanese names for Japan, Nippon/Nihon, both literally translating to \"the suns origin\"." - }, - { - "id": 105, - "question": "What's known as the \"Playground of Europe\"?", - "answer": "Switzerland", - "info": "It comes from the title of a book written in 1870 by Leslie Stephen (father of Virginia Woolf) detailing his exploits of mountain climbing (not skiing) of which sport he was one of the pioneers and trekking or walking." - }, - { - "id": 106, - "question": "Which country is known as the \"Land of Thunderbolt\"?", - "answer": "Bhutan", - "info": "Bhutan is known as the \"Land of Thunder Dragon\" or \"Land of Thunderbolt\" due to the violent and large thunderstorms that whip down through the valleys from the Himalayas. The dragon reference was due to people thinking the sparkling light of thunderbolts was the red fire of a dragon." - }, - { - "id": 107, - "question": "Which country is the largest producer of tea in the world?", - "answer": "China", - "info": "Tea is mainly grown in Asia, Africa, South America, and around the Black and Caspian Seas. The four biggest tea-producing countries today are China, India, Sri Lanka and Kenya. Together they represent 75% of world production." - }, - { - "id": 108, - "question": "Which country is the largest producer of coffee?", - "answer": "Brazil", - "info": "Brazil is the world's largest coffee producer. In 2016, Brazil produced a staggering 2,595,000 metric tons of coffee beans. It is not a new development, as Brazil has been the highest global producer of coffee beans for over 150 years." - }, - { - "id": 109, - "question": "Which country is Mount Etna, one of the most active volcanoes in the world, located?", - "answer": "Italy", - "info": "Mount Etna is the highest volcano in Europe. Towering above the city of Catania on the island of Sicily, it has been growing for about 500,000 years and is in the midst of a series of eruptions that began in 2001." - }, - { - "id": 110, - "question": "Which country is called \"Battleground of Europe?\"", - "answer": "Belgium", - "info": "Belgium has been the \"Battleground of Europe\" since the Roman Empire as it had no natural protection from its larger neighbouring countries. The battles of Oudenaarde, Ramillies, Waterloo, Ypres and Bastogne were all fought on Belgian soil." - }, - { - "id": 111, - "question": "Which is the largest tropical rain forest in the world?", - "answer": "Amazon", - "info": "The Amazon is regarded as vital in the fight against global warming due to its ability to absorb carbon from the air. It's often referred to as the \"lungs of the Earth,\" as more than 20 per cent of the world's oxygen is produced there." - }, - { - "id": 112, - "question": "Which is the largest island in the world?", - "answer": "Greenland", - "info": "Commonly thought to be Australia, but as it's actually a continental landmass, it doesn't get to make it in the list." - }, - { - "id": 113, - "question": "What's the name of the tallest waterfall in the world.", - "answer": "Angel Falls", - "info": "Angel Falls (Salto \u00c1ngel) in Venezuela is the highest waterfall in the world. The falls are 3230 feet in height, with an uninterrupted drop of 2647 feet. Angel Falls is located on a tributary of the Rio Caroni." - }, - { - "id": 114, - "question": "What country is called \"Land of White Elephants\"?", - "answer": "Thailand", - "info": "White elephants were regarded to be holy creatures in ancient Thailand and some other countries. Today, white elephants are still used as a symbol of divine and royal power in the country. Ownership of a white elephant symbolizes wealth, success, royalty, political power, wisdom, and prosperity." - }, - { - "id": 115, - "question": "Which city is in two continents?", - "answer": "Istanbul", - "info": "Istanbul embraces two continents, one arm reaching out to Asia, the other to Europe." - }, - { - "id": 116, - "question": "The Valley Of The Kings is located in which country?", - "answer": "Egypt", - "info": "The Valley of the Kings, also known as the Valley of the Gates of the Kings, is a valley in Egypt where, for a period of nearly 500 years from the 16th to 11th century BC, rock cut tombs were excavated for the pharaohs and powerful nobles of the New Kingdom (the Eighteenth to the Twentieth Dynasties of Ancient Egypt)." - }, - { - "id": 117, - "question": "Diamonds are always nice in Minecraft, but can you name the \"Diamond Capital in the World\"?", - "answer": "Antwerp", - "info": "Antwerp, Belgium is where 60-80% of the world's diamonds are cut and traded, and is known as the \"Diamond Capital of the World.\"" - }, - { - "id": 118, - "question": "Where is the \"International Court Of Justice\" located at?", - "answer": "The Hague", - "info": "" - }, - { - "id": 119, - "question": "In which country is Bali located in?", - "answer": "Indonesia", - "info": "" - }, - { - "id": 120, - "question": "What country is the world's largest coral reef system, the \"Great Barrier Reef\", located in?", - "answer": "Australia", - "info": "The Great Barrier Reef is the world's largest coral reef system composed of over 2,900 individual reefs and 900 islands stretching for over 2,300 kilometres (1,400 mi) over an area of approximately 344,400 square kilometres (133,000 sq mi). The reef is located in the Coral Sea, off the coast of Queensland, Australia." - }, - { - "id": 121, - "question": "When did the First World War start?", - "answer": "1914", - "info": "The first world war began in August 1914. It was directly triggered by the assassination of the Austrian archduke, Franz Ferdinand and his wife, on 28th June 1914 by Bosnian revolutionary, Gavrilo Princip. This event was, however, simply the trigger that set off declarations of war." - }, - { - "id": 122, - "question": "Which is the largest hot desert in the world?", - "answer": "Sahara", - "info": "The Sahara Desert covers 3.6 million square miles. It is almost the same size as the United States or China. There are sand dunes in the Sahara as tall as 590 feet." - }, - { - "id": 123, - "question": "Who lived at 221B, Baker Street, London?", - "answer": "Sherlock Holmes", - "info": "" - }, - { - "id": 124, - "question": "When did the Second World War end?", - "answer": "1945", - "info": "World War 2 ended with the unconditional surrender of the Axis powers. On 8 May 1945, the Allies accepted Germany's surrender, about a week after Adolf Hitler had committed suicide. VE Day \u2013 Victory in Europe celebrates the end of the Second World War on 8 May 1945." - }, - { - "id": 125, - "question": "What is the name of the largest dam in the world?", - "answer": "Three Gorges Dam", - "info": "At 1.4 miles wide (2.3 kilometers) and 630 feet (192 meters) high, Three Gorges Dam is the largest hydroelectric dam in the world, according to International Water Power & Dam Construction magazine. Three Gorges impounds the Yangtze River about 1,000 miles (1,610 km) west of Shanghai." - }, - { - "id": 126, - "question": "Which is the smallest planet in the Solar System?", - "answer": "Mercury", - "info": "Mercury is the smallest planet in our solar system. It's just a little bigger than Earth's moon. It is the closest planet to the sun, but it's actually not the hottest. Venus is hotter." - }, - { - "id": 127, - "question": "What is the smallest country?", - "answer": "Vatican City", - "info": "With an area of 0.17 square miles (0.44 km2) and a population right around 1,000, Vatican City is the smallest country in the world, both in terms of size and population." - }, - { - "id": 128, - "question": "What's the name of the largest bird?", - "answer": "Ostrich", - "info": "The largest living bird, a member of the Struthioniformes, is the ostrich (Struthio camelus), from the plains of Africa and Arabia. A large male ostrich can reach a height of 2.8 metres (9.2 feet) and weigh over 156 kilograms (344 pounds)." - }, - { - "id": 129, - "question": "What does the acronym GPRS stand for?", - "answer": "General Packet Radio Service", - "info": "General Packet Radio Service (GPRS) is a packet-based mobile data service on the global system for mobile communications (GSM) of 3G and 2G cellular communication systems. It is a non-voice, high-speed and useful packet-switching technology intended for GSM networks." - }, - { - "id": 130, - "question": "In what country is the Ebro river located?", - "answer": "Spain", - "info": "The Ebro river is located in Spain. It is 930 kilometers long and it's the second longest river that ends on the Mediterranean Sea." - }, - { - "id": 131, - "question": "What year was the IBM PC model 5150 introduced into the market?", - "answer": "1981", - "info": "The IBM PC was introduced into the market in 1981. It used the Intel 8088, with a clock speed of 4.77 MHz, along with the MDA and CGA as a video card." - }, - { - "id": 132, - "question": "What's the world's largest urban area?", - "answer": "Tokyo", - "info": "Tokyo is the most populated city in the world, with a population of 37 million people. It is located in Japan." - }, - { - "id": 133, - "question": "How many planets are there in the Solar system?", - "answer": "8", - "info": "In the Solar system, there are 8 planets: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus and Neptune. Pluto isn't considered a planet in the Solar System anymore." - }, - { - "id": 134, - "question": "What is the capital of Iraq?", - "answer": "Baghdad", - "info": "Baghdad is the capital of Iraq. It has a population of 7 million people." - }, - { - "id": 135, - "question": "The United Nations headquarters is located at which city?", - "answer": "New York", - "info": "The United Nations is headquartered in New York City in a complex designed by a board of architects led by Wallace Harrison and built by the architectural firm Harrison & Abramovitz. The complex has served as the official headquarters of the United Nations since its completion in 1951." - }, - { - "id": 136, - "question": "At what year did Christopher Columbus discover America?", - "answer": "1492", - "info": "The explorer Christopher Columbus made four trips across the Atlantic Ocean from Spain: in 1492, 1493, 1498 and 1502. He was determined to find a direct water route west from Europe to Asia, but he never did. Instead, he stumbled upon the Americas" - } - ], - "math": [ - { - "id": 201, - "question": "What is the highest power of a biquadratic polynomial?", - "answer": "4, four" - }, - { - "id": 202, - "question": "What is the formula for surface area of a sphere?", - "answer": "4pir^2, 4πr^2" - }, - { - "id": 203, - "question": "Which theorem states that hypotenuse^2 = base^2 + height^2?", - "answer": "Pythagorean's, Pythagorean's theorem" - }, - { - "id": 204, - "question": "Which trigonometric function is defined as hypotenuse/opposite?", - "answer": "cosecant, cosec, csc" - }, - { - "id": 205, - "question": "Does the harmonic series converge or diverge?", - "answer": "diverge" - }, - { - "id": 206, - "question": "How many quadrants are there in a cartesian plane?", - "answer": "4, four" - }, - { - "id": 207, - "question": "What is the (0,0) coordinate in a cartesian plane termed as?", - "answer": "origin" - }, - { - "id": 208, - "question": "What's the following formula that finds the area of a triangle called?", - "img_url": "https://wikimedia.org/api/rest_v1/media/math/render/png/d22b8566e8187542966e8d166e72e93746a1a6fc", - "answer": "Heron's formula, Heron" - }, - { - "id": 209, - "dynamic_id": 201, - "question": "Solve the following system of linear equations (format your answer like this & ):\n{}x + {}y = {},\n{}x + {}y = {}", - "answer": "{} & {}" - }, - { - "id": 210, - "dynamic_id": 202, - "question": "What's {} + {} mod {} congruent to?", - "answer": "{}" - }, - { - "id": 211, - "question": "What is the bottom number on a fraction called?", - "answer": "denominator" - }, - { - "id": 212, - "dynamic_id": 203, - "question": "How many vertices are on a {}gonal prism?", - "answer": "{}" - }, - { - "id": 213, - "question": "What is the term used to describe two triangles that have equal corresponding sides and angle measures?", - "answer": "congruent" - }, - { - "id": 214, - "question": "⅓πr^2h is the volume of which 3 dimensional figure?", - "answer": "cone" - }, - { - "id": 215, - "dynamic_id": 204, - "question": "Find the square root of -{}.", - "answer": "{}i" - }, - { - "id": 216, - "question": "In set builder notation, what does {p/q | q ≠ 0, p & q ∈ Z} represent?", - "answer": "Rationals, Rational Numbers" - }, - { - "id": 217, - "question": "What is the natural log of -1 (use i for imaginary number)?", - "answer": "pi*i, pii, πi" - }, - { - "id": 218, - "question": "When is the *inaugural* World Maths Day (format your answer in MM/DD)?", - "answer": "03/13" - }, - { - "id": 219, - "question": "As the Fibonacci sequence extends to infinity, what's the ratio of each number `n` and its preceding number `n-1` approaching?", - "answer": "Golden Ratio" - }, - { - "id": 220, - "question": "0, 1, 1, 2, 3, 5, 8, 13, 21, 34 are numbers of which sequence?", - "answer": "Fibonacci" - }, - { - "id": 221, - "question": "Prime numbers only have __ factors.", - "answer": "2, two" - }, - { - "id": 222, - "question": "In probability, the \\_\\_\\_\\_\\_\\_ \\_\\_\\_\\_\\_ of an experiment or random trial is the set of all possible outcomes of it.", - "answer": "sample space" - }, - { - "id": 223, - "question": "In statistics, what does this formula represent?", - "img_url": "https://www.statisticshowto.com/wp-content/uploads/2013/11/sample-standard-deviation.jpg", - "answer": "sample standard deviation, standard deviation of a sample" - }, - { - "id": 224, - "question": "\"Hexakosioihexekontahexaphobia\" is the fear of which number?", - "answer": "666" - }, - { - "id": 225, - "question": "A matrix multiplied by its inverse matrix equals...", - "answer": "the identity matrix, identity matrix" - }, - { - "id": 226, - "dynamic_id": 205, - "question": "BASE TWO QUESTION: Calculate {:b} {} {:b}", - "answer": "{:b}" - }, - { - "id": 227, - "question": "What is the only number in the entire number system which can be spelled with the same number of letters as itself?", - "answer": "4, four" - - }, - { - "id": 228, - "question": "1/100th of a second is also termed as what?", - "answer": "a jiffy, jiffy, centisecond" - }, - { - "id": 229, - "question": "What is this triangle called?", - "img_url": "https://cdn.askpython.com/wp-content/uploads/2020/07/Pascals-triangle.png", - "answer": "Pascal's triangle, Pascal" - }, - { - "id": 230, - "question": "6a^2 is the surface area of which 3 dimensional figure?", - "answer": "cube" - } - ], - "science": [ - { - "id": 301, - "question": "The three main components of a normal atom are: protons, neutrons, and...", - "answer": "electrons" - }, - { - "id": 302, - "question": "As of 2021, how many elements are there in the Periodic Table?", - "answer": "118" - }, - { - "id": 303, - "question": "What is the universal force discovered by Newton that causes objects with mass to attract each other called?", - "answer": "gravity" - }, - { - "id": 304, - "question": "What do you call an organism composed of only one cell?", - "answer": "unicellular, single-celled" - }, - { - "id": 305, - "question": "The Heisenberg's Uncertainty Principle states that the position and \\_\\_\\_\\_\\_\\_\\_\\_ of a quantum object can't be both exactly measured at the same time.", - "answer": "velocity, momentum" - }, - { - "id": 306, - "question": "A \\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_ reaction is the one wherein an atom or a set of atoms is/are replaced by another atom or a set of atoms", - "answer": "displacement, exchange" - }, - { - "id": 307, - "question": "What is the process by which green plants and certain other organisms transform light energy into chemical energy?", - "answer": "photosynthesis" - }, - { - "id": 308, - "dynamic_id": 301, - "question": "What is the {} planet of our Solar System?", - "answer": "{}" - }, - { - "id": 309, - "dynamic_id": 302, - "question": "In the biological taxonomic hierarchy, what is placed directly above {}?", - "answer": "{}" - }, - { - "id": 310, - "dynamic_id": 303, - "question": "How does one describe the unit {} in SI base units?\n**IMPORTANT:** enclose answer in backticks, use \\* for multiplication, ^ for exponentiation, and place your base units in this order: m - kg - s - A", - "img_url": "https://i.imgur.com/NRzU6tf.png", - "answer": "`{}`" - }, - { - "id": 311, - "question": "How does one call the direct phase transition from gas to solid?", - "answer": "deposition" - }, - { - "id": 312, - "question": "What is the intermolecular force caused by temporary and induced dipoles?", - "answer": "LDF, London dispersion, London dispersion force" - }, - { - "id": 313, - "question": "What is the force that causes objects to float in fluids called?", - "answer": "buoyancy" - }, - { - "id": 314, - "question": "About how many neurons are in the human brain?\n(A. 1 billion, B. 10 billion, C. 100 billion, D. 300 billion)", - "answer": "C, 100 billion, 100 bil" - }, - { - "id": 315, - "question": "What is the name of our galaxy group in which the Milky Way resides?", - "answer": "Local Group" - }, - { - "id": 316, - "question": "Which cell organelle is nicknamed \"the powerhouse of the cell\"?", - "answer": "mitochondria" - }, - { - "id": 317, - "question": "Which vascular tissue transports water and minerals from the roots to the rest of a plant?", - "answer": "the xylem, xylem" - }, - { - "id": 318, - "question": "Who discovered the theories of relativity?", - "answer": "Albert Einstein, Einstein" - }, - { - "id": 319, - "question": "In particle physics, the hypothetical isolated elementary particle with only one magnetic pole is termed as...", - "answer": "magnetic monopole" - }, - { - "id": 320, - "question": "How does one describe a chemical reaction wherein heat is released?", - "answer": "exothermic" - }, - { - "id": 321, - "question": "What range of frequency are the average human ears capable of hearing?\n(A. 10Hz-10kHz, B. 20Hz-20kHz, C. 20Hz-2000Hz, D. 10kHz-20kHz)", - "answer": "B, 20Hz-20kHz" - }, - { - "id": 322, - "question": "What is the process used to separate substances with different polarity in a mixture, using a stationary and mobile phase?", - "answer": "chromatography" - }, - { - "id": 323, - "question": "Which law states that the current through a conductor between two points is directly proportional to the voltage across the two points?", - "answer": "Ohm's law" - }, - { - "id": 324, - "question": "The type of rock that is formed by the accumulation or deposition of mineral or organic particles at the Earth's surface, followed by cementation, is called...", - "answer": "sedimentary, sedimentary rock" - }, - { - "id": 325, - "question": "Is the Richter scale (common earthquake scale) linear or logarithmic?", - "answer": "logarithmic" - }, - { - "id": 326, - "question": "What type of image is formed by a convex mirror?", - "answer": "virtual image, virtual" - }, - { - "id": 327, - "question": "How does one call the branch of physics that deals with the study of mechanical waves in gases, liquids, and solids including topics such as vibration, sound, ultrasound and infrasound", - "answer": "acoustics" - }, - { - "id": 328, - "question": "Which law states that the global entropy in a closed system can only increase?", - "answer": "second law, second law of thermodynamics" - }, - { - "id": 329, - "question": "Which particle is emitted during the beta decay of a radioactive element?", - "answer": "an electron, the electron, electron" - }, - { - "id": 330, - "question": "When DNA is unzipped, two strands are formed. What are they called (separate both answers by the word \"and\")?", - "answer": "leading and lagging, leading strand and lagging strand" - } - ], - "cs": [ - { - "id": 401, - "question": "What does HTML stand for?", - "answer": "HyperText Markup Language" - }, - { - "id": 402, - "question": "What does ASCII stand for?", - "answer": "American Standard Code for Information Interchange" - }, - { - "id": 403, - "question": "What does SASS stand for?", - "answer": "Syntactically Awesome Stylesheets, Syntactically Awesome Style Sheets" - }, - { - "id": 404, - "question": "In neural networks, \\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_ is an algorithm for supervised learning using gradient descent.", - "answer": "backpropagation" - }, - { - "id": 405, - "question": "What is computing capable of performing exaFLOPS called?", - "answer": "exascale computing, exascale" - }, - { - "id": 406, - "question": "In quantum computing, what is the full name of \"qubit\"?", - "answer": "quantum binary digit" - }, - { - "id": 407, - "question": "Given that January 1, 1970 is the starting epoch of time_t in c time, and that time_t is stored as a signed 32-bit integer, when will unix time roll over (year)?", - "answer": "2038" - }, - { - "id": 408, - "question": "What are the components of digital devices that make up logic gates called?", - "answer": "transistors" - }, - { - "id": 409, - "question": "How many possible public IPv6 addresses are there (answer in 2^n)?", - "answer": "2^128" - }, - { - "id": 410, - "question": "A hypothetical point in time at which technological growth becomes uncontrollable and irreversible, resulting in unforeseeable changes to human civilization is termed as...?", - "answer": "technological singularity, singularity" - }, - { - "id": 411, - "question": "In cryptography, the practice of establishing a shared secret between two parties using public keys and private keys is called...?", - "answer": "key exchange" - }, - { - "id": 412, - "question": "How many bits are in a TCP checksum header?", - "answer": "16, sixteen" - }, - { - "id": 413, - "question": "What is the most popular protocol (as of 2021) that handles communication between email servers?", - "answer": "SMTP, Simple Mail Transfer Protocol" - }, - { - "id": 414, - "question": "Which port does SMTP use to communicate between email servers? (assuming its plaintext)", - "answer": "25" - }, - { - "id": 415, - "question": "Which DNS record contains mail servers of a given domain?", - "answer": "MX, mail exchange" - }, - { - "id": 416, - "question": "Which newline sequence does HTTP use?", - "answer": "carriage return line feed, CRLF, \\r\\n" - }, - { - "id": 417, - "question": "What does one call the optimization technique used in CPU design that attempts to guess the outcome of a conditional operation and prepare for the most likely result?", - "answer": "branch prediction" - }, - { - "id": 418, - "question": "Name a universal logic gate.", - "answer": "NAND, NOR" - }, - { - "id": 419, - "question": "What is the mathematical formalism which functional programming was built on?", - "answer": "lambda calculus" - }, - { - "id": 420, - "question": "Why is a DDoS attack different from a DoS attack?\n(A. because the victim's server was indefinitely disrupted from the amount of traffic, B. because it also attacks the victim's confidentiality, C. because the attack had political purposes behind it, D. because the traffic flooding the victim originated from many different sources)", - "answer": "D" - }, - { - "id": 421, - "question": "What is a HTTP/1.1 feature that was superseded by HTTP/2 multiplexing and is unsupported in most browsers nowadays?", - "answer": "pipelining" - }, - { - "id": 422, - "question": "Which of these languages is the oldest?\n(Tcl, Smalltalk 80, Haskell, Standard ML, Java)", - "answer": "Smalltalk 80" - }, - { - "id": 423, - "question": "What is the name for unicode codepoints that do not fit into 16 bits?", - "answer": "surrogates" - }, - { - "id": 424, - "question": "Under what locale does making a string lowercase behave differently?", - "answer": "Turkish" - }, - { - "id": 425, - "question": "What does the \"a\" represent in a HSLA color value?", - "answer": "transparency, translucency, alpha value, alpha channel, alpha" - }, - { - "id": 426, - "question": "What is the section of a GIF that is limited to 256 colors called?", - "answer": "image block" - }, - { - "id": 427, - "question": "What is an interpreter capable of interpreting itself called?", - "answer": "metainterpreter" - }, - { - "id": 428, - "question": "Due to what data storage medium did old programming languages, such as cobol, ignore all characters past the 72nd column?", - "answer": "punch cards" - }, - { - "id": 429, - "question": "Which of these sorting algorithms is not stable?\n(Counting sort, quick sort, insertion sort, tim sort, bubble sort)", - "answer": "quick, quick sort" - }, - { - "id": 430, - "question": "Which of these languages is the youngest?\n(Lisp, Python, Java, Haskell, Prolog, Ruby, Perl)", - "answer": "Java" - } - ], - "python": [ - { - "id": 501, - "question": "Is everything an instance of the `object` class (y/n)?", - "answer": "y, yes" - }, - { - "id": 502, - "question": "Name the only non-dunder method of the builtin slice object.", - "answer": "indices" - }, - { - "id": 503, - "question": "What exception, other than `StopIteration`, can you raise from a `__getitem__` dunder to indicate to an iterator that it should stop?", - "answer": "IndexError" - }, - { - "id": 504, - "question": "What type does the `&` operator return when given 2 `dict_keys` objects?", - "answer": "set" - }, - { - "id": 505, - "question": "Can you pickle a running `list_iterator` (y/n)?", - "answer": "y, yes" - }, - { - "id": 506, - "question": "What attribute of a closure contains the value closed over?", - "answer": "cell_contents" - }, - { - "id": 507, - "question": "What name does a lambda function have?", - "answer": "" - }, - { - "id": 508, - "question": "Which file contains all special site builtins, such as help or credits?", - "answer": "_sitebuiltins" - }, - { - "id": 509, - "question": "Which module when imported opens up a web browser tab that points to the classic 353 XKCD comic mentioning Python?", - "answer": "antigravity" - }, - { - "id": 510, - "question": "Which attribute is the documentation string of a function/method/class stored in (answer should be enclosed in backticks!)?", - "answer": "`__doc__`" - }, - { - "id": 511, - "question": "What is the official name of this operator `:=`, introduced in 3.8?", - "answer": "assignment-expression operator" - }, - { - "id": 512, - "question": "When was Python first released?", - "answer": "1991" - }, - { - "id": 513, - "question": "Where does the name Python come from?", - "answer": "Monty Python, Monty Python's Flying Circus" - }, - { - "id": 514, - "question": "How is infinity represented in Python?", - "answer": "float(\"infinity\"), float('infinity'), float(\"inf\"), float('inf')" - }, - { - "id": 515, - "question": "Which of these characters is valid python outside of string literals in some context?\n(`@`, `$`, `?`)", - "answer": "@" - }, - { - "id": 516, - "question": "Which standard library module is designed for making simple parsers for languages like shell, as well as safe quoting of strings for use in a shell?", - "answer": "shlex" - }, - { - "id": 517, - "question": "Which one of these protocols/abstract base classes does the builtin `range` object NOT implement?\n(`Sequence`, `Iterable`, `Generator`)", - "answer": "Generator" - }, - { - "id": 518, - "question": "What decorator is used to allow a protocol to be checked at runtime?", - "answer": "runtime_checkable, typing.runtime_checkable" - }, - { - "id": 519, - "question": "Does `numbers.Rational` include the builtin object float (y/n)", - "answer": "n, no" - }, - { - "id": 520, - "question": "What is a package that doesn't have a `__init__` file called?", - "answer":"namespace package" - }, - { - "id": 521, - "question": "What file extension is used by the site module to determine what to do at every start?", - "answer": ".pth" - }, - { - "id": 522, - "question": "What is the garbage collection strategy used by cpython to collect everything but reference cycles?", - "answer": "reference counting, refcounting" - }, - { - "id": 523, - "question": "What dunder method is used by the tuple constructor to optimize converting an iterator to a tuple (answer should be enclosed in backticks!)?", - "answer": "`__length_hint__`" - }, - { - "id": 524, - "question": "Which protocol is used to pass self to methods when accessed on classes?", - "answer": "Descriptor" - }, - { - "id": 525, - "question": "Which year was Python 3 released?", - "answer": "2008" - }, - { - "id": 526, - "question": "Which of these is not a generator method?\n(`next`, `send`, `throw`, `close`)", - "answer": "next" - }, - { - "id": 527, - "question": "Is the `__aiter__` method async (y/n)?", - "answer": "n, no" - }, - { - "id": 528, - "question": "How does one call a class who defines the behavior of their instance classes?", - "answer": "a metaclass, metaclass" - }, - { - "id": 529, - "question": "Which of these is a subclass of `Exception`?\n(`NotImplemented`, `asyncio.CancelledError`, `StopIteration`)", - "answer": "StopIteration" - }, - { - "id": 530, - "question": "What type is the attribute of a frame object that contains the current local variables?", - "answer": "dict" - } - ] -} diff --git a/bot/resources/evergreen/wonder_twins.yaml b/bot/resources/evergreen/wonder_twins.yaml deleted file mode 100644 index 05e8d749..00000000 --- a/bot/resources/evergreen/wonder_twins.yaml +++ /dev/null @@ -1,99 +0,0 @@ -water_types: - - ice - - water - - steam - - snow - -objects: - - a bucket - - a spear - - a wall - - a lake - - a ladder - - a boat - - a vial - - a ski slope - - a hand - - a ramp - - clippers - - a bridge - - a dam - - a glacier - - a crowbar - - stilts - - a pole - - a hook - - a wave - - a cage - - a basket - - bolt cutters - - a trapeze - - a puddle - - a toboggan - - a gale - - a cloud - - a unicycle - - a spout - - a sheet - - a gelatin dessert - - a saw - - a geyser - - a jet - - a ball - - handcuffs - - a door - - a row - - a gondola - - a sled - - a rocket - - a swing - - a blizzard - - a saddle - - cubes - - a horse - - a knight - - a rocket pack - - a slick - - a drill - - a shield - - a crane - - a reflector - - a bowling ball - - a turret - - a catapault - - a blanket - - balls - - a faucet - - shears - - a thunder cloud - - a net - - a yoyo - - a block - - a straight-jacket - - a slingshot - - a jack - - a car - - a club - - a vault - - a storm - - a wrench - - an anchor - - a beast - -adjectives: - - a large - - a giant - - a massive - - a small - - a tiny - - a super cool - - a frozen - - a minuscule - - a minute - - a microscopic - - a very small - - a little - - a huge - - an enourmous - - a gigantic - - a great diff --git a/bot/resources/evergreen/xkcd_colours.json b/bot/resources/evergreen/xkcd_colours.json deleted file mode 100644 index 3feeb639..00000000 --- a/bot/resources/evergreen/xkcd_colours.json +++ /dev/null @@ -1,951 +0,0 @@ -{ - "cloudy blue": "0xacc2d9", - "dark pastel green": "0x56ae57", - "dust": "0xb2996e", - "electric lime": "0xa8ff04", - "fresh green": "0x69d84f", - "light eggplant": "0x894585", - "nasty green": "0x70b23f", - "really light blue": "0xd4ffff", - "tea": "0x65ab7c", - "warm purple": "0x952e8f", - "yellowish tan": "0xfcfc81", - "cement": "0xa5a391", - "dark grass green": "0x388004", - "dusty teal": "0x4c9085", - "grey teal": "0x5e9b8a", - "macaroni and cheese": "0xefb435", - "pinkish tan": "0xd99b82", - "spruce": "0x0a5f38", - "strong blue": "0x0c06f7", - "toxic green": "0x61de2a", - "windows blue": "0x3778bf", - "blue blue": "0x2242c7", - "blue with a hint of purple": "0x533cc6", - "booger": "0x9bb53c", - "bright sea green": "0x05ffa6", - "dark green blue": "0x1f6357", - "deep turquoise": "0x017374", - "green teal": "0x0cb577", - "strong pink": "0xff0789", - "bland": "0xafa88b", - "deep aqua": "0x08787f", - "lavender pink": "0xdd85d7", - "light moss green": "0xa6c875", - "light seafoam green": "0xa7ffb5", - "olive yellow": "0xc2b709", - "pig pink": "0xe78ea5", - "deep lilac": "0x966ebd", - "desert": "0xccad60", - "dusty lavender": "0xac86a8", - "purpley grey": "0x947e94", - "purply": "0x983fb2", - "candy pink": "0xff63e9", - "light pastel green": "0xb2fba5", - "boring green": "0x63b365", - "kiwi green": "0x8ee53f", - "light grey green": "0xb7e1a1", - "orange pink": "0xff6f52", - "tea green": "0xbdf8a3", - "very light brown": "0xd3b683", - "egg shell": "0xfffcc4", - "eggplant purple": "0x430541", - "powder pink": "0xffb2d0", - "reddish grey": "0x997570", - "baby shit brown": "0xad900d", - "liliac": "0xc48efd", - "stormy blue": "0x507b9c", - "ugly brown": "0x7d7103", - "custard": "0xfffd78", - "darkish pink": "0xda467d", - "deep brown": "0x410200", - "greenish beige": "0xc9d179", - "manilla": "0xfffa86", - "off blue": "0x5684ae", - "battleship grey": "0x6b7c85", - "browny green": "0x6f6c0a", - "bruise": "0x7e4071", - "kelley green": "0x009337", - "sickly yellow": "0xd0e429", - "sunny yellow": "0xfff917", - "azul": "0x1d5dec", - "darkgreen": "0x054907", - "green/yellow": "0xb5ce08", - "lichen": "0x8fb67b", - "light light green": "0xc8ffb0", - "pale gold": "0xfdde6c", - "sun yellow": "0xffdf22", - "tan green": "0xa9be70", - "burple": "0x6832e3", - "butterscotch": "0xfdb147", - "toupe": "0xc7ac7d", - "dark cream": "0xfff39a", - "indian red": "0x850e04", - "light lavendar": "0xefc0fe", - "poison green": "0x40fd14", - "baby puke green": "0xb6c406", - "bright yellow green": "0x9dff00", - "charcoal grey": "0x3c4142", - "squash": "0xf2ab15", - "cinnamon": "0xac4f06", - "light pea green": "0xc4fe82", - "radioactive green": "0x2cfa1f", - "raw sienna": "0x9a6200", - "baby purple": "0xca9bf7", - "cocoa": "0x875f42", - "light royal blue": "0x3a2efe", - "orangeish": "0xfd8d49", - "rust brown": "0x8b3103", - "sand brown": "0xcba560", - "swamp": "0x698339", - "tealish green": "0x0cdc73", - "burnt siena": "0xb75203", - "camo": "0x7f8f4e", - "dusk blue": "0x26538d", - "fern": "0x63a950", - "old rose": "0xc87f89", - "pale light green": "0xb1fc99", - "peachy pink": "0xff9a8a", - "rosy pink": "0xf6688e", - "light bluish green": "0x76fda8", - "light bright green": "0x53fe5c", - "light neon green": "0x4efd54", - "light seafoam": "0xa0febf", - "tiffany blue": "0x7bf2da", - "washed out green": "0xbcf5a6", - "browny orange": "0xca6b02", - "nice blue": "0x107ab0", - "sapphire": "0x2138ab", - "greyish teal": "0x719f91", - "orangey yellow": "0xfdb915", - "parchment": "0xfefcaf", - "straw": "0xfcf679", - "very dark brown": "0x1d0200", - "terracota": "0xcb6843", - "ugly blue": "0x31668a", - "clear blue": "0x247afd", - "creme": "0xffffb6", - "foam green": "0x90fda9", - "grey/green": "0x86a17d", - "light gold": "0xfddc5c", - "seafoam blue": "0x78d1b6", - "topaz": "0x13bbaf", - "violet pink": "0xfb5ffc", - "wintergreen": "0x20f986", - "yellow tan": "0xffe36e", - "dark fuchsia": "0x9d0759", - "indigo blue": "0x3a18b1", - "light yellowish green": "0xc2ff89", - "pale magenta": "0xd767ad", - "rich purple": "0x720058", - "sunflower yellow": "0xffda03", - "green/blue": "0x01c08d", - "leather": "0xac7434", - "racing green": "0x014600", - "vivid purple": "0x9900fa", - "dark royal blue": "0x02066f", - "hazel": "0x8e7618", - "muted pink": "0xd1768f", - "booger green": "0x96b403", - "canary": "0xfdff63", - "cool grey": "0x95a3a6", - "dark taupe": "0x7f684e", - "darkish purple": "0x751973", - "true green": "0x089404", - "coral pink": "0xff6163", - "dark sage": "0x598556", - "dark slate blue": "0x214761", - "flat blue": "0x3c73a8", - "mushroom": "0xba9e88", - "rich blue": "0x021bf9", - "dirty purple": "0x734a65", - "greenblue": "0x23c48b", - "icky green": "0x8fae22", - "light khaki": "0xe6f2a2", - "warm blue": "0x4b57db", - "dark hot pink": "0xd90166", - "deep sea blue": "0x015482", - "carmine": "0x9d0216", - "dark yellow green": "0x728f02", - "pale peach": "0xffe5ad", - "plum purple": "0x4e0550", - "golden rod": "0xf9bc08", - "neon red": "0xff073a", - "old pink": "0xc77986", - "very pale blue": "0xd6fffe", - "blood orange": "0xfe4b03", - "grapefruit": "0xfd5956", - "sand yellow": "0xfce166", - "clay brown": "0xb2713d", - "dark blue grey": "0x1f3b4d", - "flat green": "0x699d4c", - "light green blue": "0x56fca2", - "warm pink": "0xfb5581", - "dodger blue": "0x3e82fc", - "gross green": "0xa0bf16", - "ice": "0xd6fffa", - "metallic blue": "0x4f738e", - "pale salmon": "0xffb19a", - "sap green": "0x5c8b15", - "algae": "0x54ac68", - "bluey grey": "0x89a0b0", - "greeny grey": "0x7ea07a", - "highlighter green": "0x1bfc06", - "light light blue": "0xcafffb", - "light mint": "0xb6ffbb", - "raw umber": "0xa75e09", - "vivid blue": "0x152eff", - "deep lavender": "0x8d5eb7", - "dull teal": "0x5f9e8f", - "light greenish blue": "0x63f7b4", - "mud green": "0x606602", - "pinky": "0xfc86aa", - "red wine": "0x8c0034", - "shit green": "0x758000", - "tan brown": "0xab7e4c", - "darkblue": "0x030764", - "rosa": "0xfe86a4", - "lipstick": "0xd5174e", - "pale mauve": "0xfed0fc", - "claret": "0x680018", - "dandelion": "0xfedf08", - "orangered": "0xfe420f", - "poop green": "0x6f7c00", - "ruby": "0xca0147", - "dark": "0x1b2431", - "greenish turquoise": "0x00fbb0", - "pastel red": "0xdb5856", - "piss yellow": "0xddd618", - "bright cyan": "0x41fdfe", - "dark coral": "0xcf524e", - "algae green": "0x21c36f", - "darkish red": "0xa90308", - "reddy brown": "0x6e1005", - "blush pink": "0xfe828c", - "camouflage green": "0x4b6113", - "lawn green": "0x4da409", - "putty": "0xbeae8a", - "vibrant blue": "0x0339f8", - "dark sand": "0xa88f59", - "purple/blue": "0x5d21d0", - "saffron": "0xfeb209", - "twilight": "0x4e518b", - "warm brown": "0x964e02", - "bluegrey": "0x85a3b2", - "bubble gum pink": "0xff69af", - "duck egg blue": "0xc3fbf4", - "greenish cyan": "0x2afeb7", - "petrol": "0x005f6a", - "royal": "0x0c1793", - "butter": "0xffff81", - "dusty orange": "0xf0833a", - "off yellow": "0xf1f33f", - "pale olive green": "0xb1d27b", - "orangish": "0xfc824a", - "leaf": "0x71aa34", - "light blue grey": "0xb7c9e2", - "dried blood": "0x4b0101", - "lightish purple": "0xa552e6", - "rusty red": "0xaf2f0d", - "lavender blue": "0x8b88f8", - "light grass green": "0x9af764", - "light mint green": "0xa6fbb2", - "sunflower": "0xffc512", - "velvet": "0x750851", - "brick orange": "0xc14a09", - "lightish red": "0xfe2f4a", - "pure blue": "0x0203e2", - "twilight blue": "0x0a437a", - "violet red": "0xa50055", - "yellowy brown": "0xae8b0c", - "carnation": "0xfd798f", - "muddy yellow": "0xbfac05", - "dark seafoam green": "0x3eaf76", - "deep rose": "0xc74767", - "dusty red": "0xb9484e", - "grey/blue": "0x647d8e", - "lemon lime": "0xbffe28", - "purple/pink": "0xd725de", - "brown yellow": "0xb29705", - "purple brown": "0x673a3f", - "wisteria": "0xa87dc2", - "banana yellow": "0xfafe4b", - "lipstick red": "0xc0022f", - "water blue": "0x0e87cc", - "brown grey": "0x8d8468", - "vibrant purple": "0xad03de", - "baby green": "0x8cff9e", - "barf green": "0x94ac02", - "eggshell blue": "0xc4fff7", - "sandy yellow": "0xfdee73", - "cool green": "0x33b864", - "pale": "0xfff9d0", - "blue/grey": "0x758da3", - "hot magenta": "0xf504c9", - "greyblue": "0x77a1b5", - "purpley": "0x8756e4", - "baby shit green": "0x889717", - "brownish pink": "0xc27e79", - "dark aquamarine": "0x017371", - "diarrhea": "0x9f8303", - "light mustard": "0xf7d560", - "pale sky blue": "0xbdf6fe", - "turtle green": "0x75b84f", - "bright olive": "0x9cbb04", - "dark grey blue": "0x29465b", - "greeny brown": "0x696006", - "lemon green": "0xadf802", - "light periwinkle": "0xc1c6fc", - "seaweed green": "0x35ad6b", - "sunshine yellow": "0xfffd37", - "ugly purple": "0xa442a0", - "medium pink": "0xf36196", - "puke brown": "0x947706", - "very light pink": "0xfff4f2", - "viridian": "0x1e9167", - "bile": "0xb5c306", - "faded yellow": "0xfeff7f", - "very pale green": "0xcffdbc", - "vibrant green": "0x0add08", - "bright lime": "0x87fd05", - "spearmint": "0x1ef876", - "light aquamarine": "0x7bfdc7", - "light sage": "0xbcecac", - "yellowgreen": "0xbbf90f", - "baby poo": "0xab9004", - "dark seafoam": "0x1fb57a", - "deep teal": "0x00555a", - "heather": "0xa484ac", - "rust orange": "0xc45508", - "dirty blue": "0x3f829d", - "fern green": "0x548d44", - "bright lilac": "0xc95efb", - "weird green": "0x3ae57f", - "peacock blue": "0x016795", - "avocado green": "0x87a922", - "faded orange": "0xf0944d", - "grape purple": "0x5d1451", - "hot green": "0x25ff29", - "lime yellow": "0xd0fe1d", - "mango": "0xffa62b", - "shamrock": "0x01b44c", - "bubblegum": "0xff6cb5", - "purplish brown": "0x6b4247", - "vomit yellow": "0xc7c10c", - "pale cyan": "0xb7fffa", - "key lime": "0xaeff6e", - "tomato red": "0xec2d01", - "lightgreen": "0x76ff7b", - "merlot": "0x730039", - "night blue": "0x040348", - "purpleish pink": "0xdf4ec8", - "apple": "0x6ecb3c", - "baby poop green": "0x8f9805", - "green apple": "0x5edc1f", - "heliotrope": "0xd94ff5", - "yellow/green": "0xc8fd3d", - "almost black": "0x070d0d", - "cool blue": "0x4984b8", - "leafy green": "0x51b73b", - "mustard brown": "0xac7e04", - "dusk": "0x4e5481", - "dull brown": "0x876e4b", - "frog green": "0x58bc08", - "vivid green": "0x2fef10", - "bright light green": "0x2dfe54", - "fluro green": "0x0aff02", - "kiwi": "0x9cef43", - "seaweed": "0x18d17b", - "navy green": "0x35530a", - "ultramarine blue": "0x1805db", - "iris": "0x6258c4", - "pastel orange": "0xff964f", - "yellowish orange": "0xffab0f", - "perrywinkle": "0x8f8ce7", - "tealish": "0x24bca8", - "dark plum": "0x3f012c", - "pear": "0xcbf85f", - "pinkish orange": "0xff724c", - "midnight purple": "0x280137", - "light urple": "0xb36ff6", - "dark mint": "0x48c072", - "greenish tan": "0xbccb7a", - "light burgundy": "0xa8415b", - "turquoise blue": "0x06b1c4", - "ugly pink": "0xcd7584", - "sandy": "0xf1da7a", - "electric pink": "0xff0490", - "muted purple": "0x805b87", - "mid green": "0x50a747", - "greyish": "0xa8a495", - "neon yellow": "0xcfff04", - "banana": "0xffff7e", - "carnation pink": "0xff7fa7", - "tomato": "0xef4026", - "sea": "0x3c9992", - "muddy brown": "0x886806", - "turquoise green": "0x04f489", - "buff": "0xfef69e", - "fawn": "0xcfaf7b", - "muted blue": "0x3b719f", - "pale rose": "0xfdc1c5", - "dark mint green": "0x20c073", - "amethyst": "0x9b5fc0", - "blue/green": "0x0f9b8e", - "chestnut": "0x742802", - "sick green": "0x9db92c", - "pea": "0xa4bf20", - "rusty orange": "0xcd5909", - "stone": "0xada587", - "rose red": "0xbe013c", - "pale aqua": "0xb8ffeb", - "deep orange": "0xdc4d01", - "earth": "0xa2653e", - "mossy green": "0x638b27", - "grassy green": "0x419c03", - "pale lime green": "0xb1ff65", - "light grey blue": "0x9dbcd4", - "pale grey": "0xfdfdfe", - "asparagus": "0x77ab56", - "blueberry": "0x464196", - "purple red": "0x990147", - "pale lime": "0xbefd73", - "greenish teal": "0x32bf84", - "caramel": "0xaf6f09", - "deep magenta": "0xa0025c", - "light peach": "0xffd8b1", - "milk chocolate": "0x7f4e1e", - "ocher": "0xbf9b0c", - "off green": "0x6ba353", - "purply pink": "0xf075e6", - "lightblue": "0x7bc8f6", - "dusky blue": "0x475f94", - "golden": "0xf5bf03", - "light beige": "0xfffeb6", - "butter yellow": "0xfffd74", - "dusky purple": "0x895b7b", - "french blue": "0x436bad", - "ugly yellow": "0xd0c101", - "greeny yellow": "0xc6f808", - "orangish red": "0xf43605", - "shamrock green": "0x02c14d", - "orangish brown": "0xb25f03", - "tree green": "0x2a7e19", - "deep violet": "0x490648", - "gunmetal": "0x536267", - "blue/purple": "0x5a06ef", - "cherry": "0xcf0234", - "sandy brown": "0xc4a661", - "warm grey": "0x978a84", - "dark indigo": "0x1f0954", - "midnight": "0x03012d", - "bluey green": "0x2bb179", - "grey pink": "0xc3909b", - "soft purple": "0xa66fb5", - "blood": "0x770001", - "brown red": "0x922b05", - "medium grey": "0x7d7f7c", - "berry": "0x990f4b", - "poo": "0x8f7303", - "purpley pink": "0xc83cb9", - "light salmon": "0xfea993", - "snot": "0xacbb0d", - "easter purple": "0xc071fe", - "light yellow green": "0xccfd7f", - "dark navy blue": "0x00022e", - "drab": "0x828344", - "light rose": "0xffc5cb", - "rouge": "0xab1239", - "purplish red": "0xb0054b", - "slime green": "0x99cc04", - "baby poop": "0x937c00", - "irish green": "0x019529", - "pink/purple": "0xef1de7", - "dark navy": "0x000435", - "greeny blue": "0x42b395", - "light plum": "0x9d5783", - "pinkish grey": "0xc8aca9", - "dirty orange": "0xc87606", - "rust red": "0xaa2704", - "pale lilac": "0xe4cbff", - "orangey red": "0xfa4224", - "primary blue": "0x0804f9", - "kermit green": "0x5cb200", - "brownish purple": "0x76424e", - "murky green": "0x6c7a0e", - "wheat": "0xfbdd7e", - "very dark purple": "0x2a0134", - "bottle green": "0x044a05", - "watermelon": "0xfd4659", - "deep sky blue": "0x0d75f8", - "fire engine red": "0xfe0002", - "yellow ochre": "0xcb9d06", - "pumpkin orange": "0xfb7d07", - "pale olive": "0xb9cc81", - "light lilac": "0xedc8ff", - "lightish green": "0x61e160", - "carolina blue": "0x8ab8fe", - "mulberry": "0x920a4e", - "shocking pink": "0xfe02a2", - "auburn": "0x9a3001", - "bright lime green": "0x65fe08", - "celadon": "0xbefdb7", - "pinkish brown": "0xb17261", - "poo brown": "0x885f01", - "bright sky blue": "0x02ccfe", - "celery": "0xc1fd95", - "dirt brown": "0x836539", - "strawberry": "0xfb2943", - "dark lime": "0x84b701", - "copper": "0xb66325", - "medium brown": "0x7f5112", - "muted green": "0x5fa052", - "robin's egg": "0x6dedfd", - "bright aqua": "0x0bf9ea", - "bright lavender": "0xc760ff", - "ivory": "0xffffcb", - "very light purple": "0xf6cefc", - "light navy": "0x155084", - "pink red": "0xf5054f", - "olive brown": "0x645403", - "poop brown": "0x7a5901", - "mustard green": "0xa8b504", - "ocean green": "0x3d9973", - "very dark blue": "0x000133", - "dusty green": "0x76a973", - "light navy blue": "0x2e5a88", - "minty green": "0x0bf77d", - "adobe": "0xbd6c48", - "barney": "0xac1db8", - "jade green": "0x2baf6a", - "bright light blue": "0x26f7fd", - "light lime": "0xaefd6c", - "dark khaki": "0x9b8f55", - "orange yellow": "0xffad01", - "ocre": "0xc69c04", - "maize": "0xf4d054", - "faded pink": "0xde9dac", - "british racing green": "0x05480d", - "sandstone": "0xc9ae74", - "mud brown": "0x60460f", - "light sea green": "0x98f6b0", - "robin egg blue": "0x8af1fe", - "aqua marine": "0x2ee8bb", - "dark sea green": "0x11875d", - "soft pink": "0xfdb0c0", - "orangey brown": "0xb16002", - "cherry red": "0xf7022a", - "burnt yellow": "0xd5ab09", - "brownish grey": "0x86775f", - "camel": "0xc69f59", - "purplish grey": "0x7a687f", - "marine": "0x042e60", - "greyish pink": "0xc88d94", - "pale turquoise": "0xa5fbd5", - "pastel yellow": "0xfffe71", - "bluey purple": "0x6241c7", - "canary yellow": "0xfffe40", - "faded red": "0xd3494e", - "sepia": "0x985e2b", - "coffee": "0xa6814c", - "bright magenta": "0xff08e8", - "mocha": "0x9d7651", - "ecru": "0xfeffca", - "purpleish": "0x98568d", - "cranberry": "0x9e003a", - "darkish green": "0x287c37", - "brown orange": "0xb96902", - "dusky rose": "0xba6873", - "melon": "0xff7855", - "sickly green": "0x94b21c", - "silver": "0xc5c9c7", - "purply blue": "0x661aee", - "purpleish blue": "0x6140ef", - "hospital green": "0x9be5aa", - "shit brown": "0x7b5804", - "mid blue": "0x276ab3", - "amber": "0xfeb308", - "easter green": "0x8cfd7e", - "soft blue": "0x6488ea", - "cerulean blue": "0x056eee", - "golden brown": "0xb27a01", - "bright turquoise": "0x0ffef9", - "red pink": "0xfa2a55", - "red purple": "0x820747", - "greyish brown": "0x7a6a4f", - "vermillion": "0xf4320c", - "russet": "0xa13905", - "steel grey": "0x6f828a", - "lighter purple": "0xa55af4", - "bright violet": "0xad0afd", - "prussian blue": "0x004577", - "slate green": "0x658d6d", - "dirty pink": "0xca7b80", - "dark blue green": "0x005249", - "pine": "0x2b5d34", - "yellowy green": "0xbff128", - "dark gold": "0xb59410", - "bluish": "0x2976bb", - "darkish blue": "0x014182", - "dull red": "0xbb3f3f", - "pinky red": "0xfc2647", - "bronze": "0xa87900", - "pale teal": "0x82cbb2", - "military green": "0x667c3e", - "barbie pink": "0xfe46a5", - "bubblegum pink": "0xfe83cc", - "pea soup green": "0x94a617", - "dark mustard": "0xa88905", - "shit": "0x7f5f00", - "medium purple": "0x9e43a2", - "very dark green": "0x062e03", - "dirt": "0x8a6e45", - "dusky pink": "0xcc7a8b", - "red violet": "0x9e0168", - "lemon yellow": "0xfdff38", - "pistachio": "0xc0fa8b", - "dull yellow": "0xeedc5b", - "dark lime green": "0x7ebd01", - "denim blue": "0x3b5b92", - "teal blue": "0x01889f", - "lightish blue": "0x3d7afd", - "purpley blue": "0x5f34e7", - "light indigo": "0x6d5acf", - "swamp green": "0x748500", - "brown green": "0x706c11", - "dark maroon": "0x3c0008", - "hot purple": "0xcb00f5", - "dark forest green": "0x002d04", - "faded blue": "0x658cbb", - "drab green": "0x749551", - "light lime green": "0xb9ff66", - "snot green": "0x9dc100", - "yellowish": "0xfaee66", - "light blue green": "0x7efbb3", - "bordeaux": "0x7b002c", - "light mauve": "0xc292a1", - "ocean": "0x017b92", - "marigold": "0xfcc006", - "muddy green": "0x657432", - "dull orange": "0xd8863b", - "steel": "0x738595", - "electric purple": "0xaa23ff", - "fluorescent green": "0x08ff08", - "yellowish brown": "0x9b7a01", - "blush": "0xf29e8e", - "soft green": "0x6fc276", - "bright orange": "0xff5b00", - "lemon": "0xfdff52", - "purple grey": "0x866f85", - "acid green": "0x8ffe09", - "pale lavender": "0xeecffe", - "violet blue": "0x510ac9", - "light forest green": "0x4f9153", - "burnt red": "0x9f2305", - "khaki green": "0x728639", - "cerise": "0xde0c62", - "faded purple": "0x916e99", - "apricot": "0xffb16d", - "dark olive green": "0x3c4d03", - "grey brown": "0x7f7053", - "green grey": "0x77926f", - "true blue": "0x010fcc", - "pale violet": "0xceaefa", - "periwinkle blue": "0x8f99fb", - "light sky blue": "0xc6fcff", - "blurple": "0x5539cc", - "green brown": "0x544e03", - "bluegreen": "0x017a79", - "bright teal": "0x01f9c6", - "brownish yellow": "0xc9b003", - "pea soup": "0x929901", - "forest": "0x0b5509", - "barney purple": "0xa00498", - "ultramarine": "0x2000b1", - "purplish": "0x94568c", - "puke yellow": "0xc2be0e", - "bluish grey": "0x748b97", - "dark periwinkle": "0x665fd1", - "dark lilac": "0x9c6da5", - "reddish": "0xc44240", - "light maroon": "0xa24857", - "dusty purple": "0x825f87", - "terra cotta": "0xc9643b", - "avocado": "0x90b134", - "marine blue": "0x01386a", - "teal green": "0x25a36f", - "slate grey": "0x59656d", - "lighter green": "0x75fd63", - "electric green": "0x21fc0d", - "dusty blue": "0x5a86ad", - "golden yellow": "0xfec615", - "bright yellow": "0xfffd01", - "light lavender": "0xdfc5fe", - "umber": "0xb26400", - "poop": "0x7f5e00", - "dark peach": "0xde7e5d", - "jungle green": "0x048243", - "eggshell": "0xffffd4", - "denim": "0x3b638c", - "yellow brown": "0xb79400", - "dull purple": "0x84597e", - "chocolate brown": "0x411900", - "wine red": "0x7b0323", - "neon blue": "0x04d9ff", - "dirty green": "0x667e2c", - "light tan": "0xfbeeac", - "ice blue": "0xd7fffe", - "cadet blue": "0x4e7496", - "dark mauve": "0x874c62", - "very light blue": "0xd5ffff", - "grey purple": "0x826d8c", - "pastel pink": "0xffbacd", - "very light green": "0xd1ffbd", - "dark sky blue": "0x448ee4", - "evergreen": "0x05472a", - "dull pink": "0xd5869d", - "aubergine": "0x3d0734", - "mahogany": "0x4a0100", - "reddish orange": "0xf8481c", - "deep green": "0x02590f", - "vomit green": "0x89a203", - "purple pink": "0xe03fd8", - "dusty pink": "0xd58a94", - "faded green": "0x7bb274", - "camo green": "0x526525", - "pinky purple": "0xc94cbe", - "pink purple": "0xdb4bda", - "brownish red": "0x9e3623", - "dark rose": "0xb5485d", - "mud": "0x735c12", - "brownish": "0x9c6d57", - "emerald green": "0x028f1e", - "pale brown": "0xb1916e", - "dull blue": "0x49759c", - "burnt umber": "0xa0450e", - "medium green": "0x39ad48", - "clay": "0xb66a50", - "light aqua": "0x8cffdb", - "light olive green": "0xa4be5c", - "brownish orange": "0xcb7723", - "dark aqua": "0x05696b", - "purplish pink": "0xce5dae", - "dark salmon": "0xc85a53", - "greenish grey": "0x96ae8d", - "jade": "0x1fa774", - "ugly green": "0x7a9703", - "dark beige": "0xac9362", - "emerald": "0x01a049", - "pale red": "0xd9544d", - "light magenta": "0xfa5ff7", - "sky": "0x82cafc", - "light cyan": "0xacfffc", - "yellow orange": "0xfcb001", - "reddish purple": "0x910951", - "reddish pink": "0xfe2c54", - "orchid": "0xc875c4", - "dirty yellow": "0xcdc50a", - "orange red": "0xfd411e", - "deep red": "0x9a0200", - "orange brown": "0xbe6400", - "cobalt blue": "0x030aa7", - "neon pink": "0xfe019a", - "rose pink": "0xf7879a", - "greyish purple": "0x887191", - "raspberry": "0xb00149", - "aqua green": "0x12e193", - "salmon pink": "0xfe7b7c", - "tangerine": "0xff9408", - "brownish green": "0x6a6e09", - "red brown": "0x8b2e16", - "greenish brown": "0x696112", - "pumpkin": "0xe17701", - "pine green": "0x0a481e", - "charcoal": "0x343837", - "baby pink": "0xffb7ce", - "cornflower": "0x6a79f7", - "blue violet": "0x5d06e9", - "chocolate": "0x3d1c02", - "greyish green": "0x82a67d", - "scarlet": "0xbe0119", - "green yellow": "0xc9ff27", - "dark olive": "0x373e02", - "sienna": "0xa9561e", - "pastel purple": "0xcaa0ff", - "terracotta": "0xca6641", - "aqua blue": "0x02d8e9", - "sage green": "0x88b378", - "blood red": "0x980002", - "deep pink": "0xcb0162", - "grass": "0x5cac2d", - "moss": "0x769958", - "pastel blue": "0xa2bffe", - "bluish green": "0x10a674", - "green blue": "0x06b48b", - "dark tan": "0xaf884a", - "greenish blue": "0x0b8b87", - "pale orange": "0xffa756", - "vomit": "0xa2a415", - "forrest green": "0x154406", - "dark lavender": "0x856798", - "dark violet": "0x34013f", - "purple blue": "0x632de9", - "dark cyan": "0x0a888a", - "olive drab": "0x6f7632", - "pinkish": "0xd46a7e", - "cobalt": "0x1e488f", - "neon purple": "0xbc13fe", - "light turquoise": "0x7ef4cc", - "apple green": "0x76cd26", - "dull green": "0x74a662", - "wine": "0x80013f", - "powder blue": "0xb1d1fc", - "off white": "0xffffe4", - "electric blue": "0x0652ff", - "dark turquoise": "0x045c5a", - "blue purple": "0x5729ce", - "azure": "0x069af3", - "bright red": "0xff000d", - "pinkish red": "0xf10c45", - "cornflower blue": "0x5170d7", - "light olive": "0xacbf69", - "grape": "0x6c3461", - "greyish blue": "0x5e819d", - "purplish blue": "0x601ef9", - "yellowish green": "0xb0dd16", - "greenish yellow": "0xcdfd02", - "medium blue": "0x2c6fbb", - "dusty rose": "0xc0737a", - "light violet": "0xd6b4fc", - "midnight blue": "0x020035", - "bluish purple": "0x703be7", - "red orange": "0xfd3c06", - "dark magenta": "0x960056", - "greenish": "0x40a368", - "ocean blue": "0x03719c", - "coral": "0xfc5a50", - "cream": "0xffffc2", - "reddish brown": "0x7f2b0a", - "burnt sienna": "0xb04e0f", - "brick": "0xa03623", - "sage": "0x87ae73", - "grey green": "0x789b73", - "white": "0xffffff", - "robin's egg blue": "0x98eff9", - "moss green": "0x658b38", - "steel blue": "0x5a7d9a", - "eggplant": "0x380835", - "light yellow": "0xfffe7a", - "leaf green": "0x5ca904", - "light grey": "0xd8dcd6", - "puke": "0xa5a502", - "pinkish purple": "0xd648d7", - "sea blue": "0x047495", - "pale purple": "0xb790d4", - "slate blue": "0x5b7c99", - "blue grey": "0x607c8e", - "hunter green": "0x0b4008", - "fuchsia": "0xed0dd9", - "crimson": "0x8c000f", - "pale yellow": "0xffff84", - "ochre": "0xbf9005", - "mustard yellow": "0xd2bd0a", - "light red": "0xff474c", - "cerulean": "0x0485d1", - "pale pink": "0xffcfdc", - "deep blue": "0x040273", - "rust": "0xa83c09", - "light teal": "0x90e4c1", - "slate": "0x516572", - "goldenrod": "0xfac205", - "dark yellow": "0xd5b60a", - "dark grey": "0x363737", - "army green": "0x4b5d16", - "grey blue": "0x6b8ba4", - "seafoam": "0x80f9ad", - "puce": "0xa57e52", - "spring green": "0xa9f971", - "dark orange": "0xc65102", - "sand": "0xe2ca76", - "pastel green": "0xb0ff9d", - "mint": "0x9ffeb0", - "light orange": "0xfdaa48", - "bright pink": "0xfe01b1", - "chartreuse": "0xc1f80a", - "deep purple": "0x36013f", - "dark brown": "0x341c02", - "taupe": "0xb9a281", - "pea green": "0x8eab12", - "puke green": "0x9aae07", - "kelly green": "0x02ab2e", - "seafoam green": "0x7af9ab", - "blue green": "0x137e6d", - "khaki": "0xaaa662", - "burgundy": "0x610023", - "dark teal": "0x014d4e", - "brick red": "0x8f1402", - "royal purple": "0x4b006e", - "plum": "0x580f41", - "mint green": "0x8fff9f", - "gold": "0xdbb40c", - "baby blue": "0xa2cffe", - "yellow green": "0xc0fb2d", - "bright purple": "0xbe03fd", - "dark red": "0x840000", - "pale blue": "0xd0fefe", - "grass green": "0x3f9b0b", - "navy": "0x01153e", - "aquamarine": "0x04d8b2", - "burnt orange": "0xc04e01", - "neon green": "0x0cff0c", - "bright blue": "0x0165fc", - "rose": "0xcf6275", - "light pink": "0xffd1df", - "mustard": "0xceb301", - "indigo": "0x380282", - "lime": "0xaaff32", - "sea green": "0x53fca1", - "periwinkle": "0x8e82fe", - "dark pink": "0xcb416b", - "olive green": "0x677a04", - "peach": "0xffb07c", - "pale green": "0xc7fdb5", - "light brown": "0xad8150", - "hot pink": "0xff028d", - "black": "0x000000", - "lilac": "0xcea2fd", - "navy blue": "0x001146", - "royal blue": "0x0504aa", - "beige": "0xe6daa6", - "salmon": "0xff796c", - "olive": "0x6e750e", - "maroon": "0x650021", - "bright green": "0x01ff07", - "dark purple": "0x35063e", - "mauve": "0xae7181", - "forest green": "0x06470c", - "aqua": "0x13eac9", - "cyan": "0x00ffff", - "tan": "0xd1b26f", - "dark blue": "0x00035b", - "lavender": "0xc79fef", - "turquoise": "0x06c2ac", - "dark green": "0x033500", - "violet": "0x9a0eea", - "light purple": "0xbf77f6", - "lime green": "0x89fe05", - "grey": "0x929591", - "sky blue": "0x75bbfd", - "yellow": "0xffff14", - "magenta": "0xc20078", - "light green": "0x96f97b", - "orange": "0xf97306", - "teal": "0x029386", - "light blue": "0x95d0fc", - "red": "0xe50000", - "brown": "0x653700", - "pink": "0xff81c0", - "blue": "0x0343df", - "green": "0x15b01a", - "purple": "0x7e1e9c" -} diff --git a/bot/resources/fun/LuckiestGuy-Regular.ttf b/bot/resources/fun/LuckiestGuy-Regular.ttf new file mode 100644 index 00000000..8c79c875 Binary files /dev/null and b/bot/resources/fun/LuckiestGuy-Regular.ttf differ diff --git a/bot/resources/fun/all_cards.png b/bot/resources/fun/all_cards.png new file mode 100644 index 00000000..10ed2eb8 Binary files /dev/null and b/bot/resources/fun/all_cards.png differ diff --git a/bot/resources/fun/caesar_info.json b/bot/resources/fun/caesar_info.json new file mode 100644 index 00000000..8229c4f3 --- /dev/null +++ b/bot/resources/fun/caesar_info.json @@ -0,0 +1,4 @@ +{ + "title": "Caesar Cipher", + "description": "**Information**\nThe Caesar Cipher, named after the Roman General Julius Caesar, is one of the simplest and most widely known encryption techniques. It is a type of substitution cipher in which each letter in the plaintext is replaced by a letter given a specific position offset in the alphabet, with the letters wrapping around both sides.\n\n**Examples**\n1) `Hello World` <=> `Khoor Zruog` where letters are shifted forwards by `3`.\n2) `Julius Caesar` <=> `Yjaxjh Rpthpg` where letters are shifted backwards by `11`." +} diff --git a/bot/resources/fun/ducks_help_ex.png b/bot/resources/fun/ducks_help_ex.png new file mode 100644 index 00000000..01d9c243 Binary files /dev/null and b/bot/resources/fun/ducks_help_ex.png differ diff --git a/bot/resources/fun/game_recs/chrono_trigger.json b/bot/resources/fun/game_recs/chrono_trigger.json new file mode 100644 index 00000000..9720b977 --- /dev/null +++ b/bot/resources/fun/game_recs/chrono_trigger.json @@ -0,0 +1,7 @@ +{ + "title": "Chrono Trigger", + "description": "One of the best games of all time. A brilliant story involving time-travel with loveable characters. It has a brilliant score by Yasonuri Mitsuda and artwork by Akira Toriyama. With over 20 endings and New Game+, there is a huge amount of replay value here.", + "link": "https://rawg.io/games/chrono-trigger-1995", + "image": "https://vignette.wikia.nocookie.net/chrono/images/2/24/Chrono_Trigger_cover.jpg", + "author": "352635617709916161" +} diff --git a/bot/resources/fun/game_recs/digimon_world.json b/bot/resources/fun/game_recs/digimon_world.json new file mode 100644 index 00000000..c1cb4f37 --- /dev/null +++ b/bot/resources/fun/game_recs/digimon_world.json @@ -0,0 +1,7 @@ +{ + "title": "Digimon World", + "description": "A great mix of town-building and pet-raising set in the Digimon universe. With plenty of Digimon to raise and recruit to the village, this charming game will keep you occupied for a long time.", + "image": "https://www.mobygames.com/images/covers/l/437308-digimon-world-playstation-front-cover.jpg", + "link": "https://rawg.io/games/digimon-world", + "author": "352635617709916161" +} diff --git a/bot/resources/fun/game_recs/doom_2.json b/bot/resources/fun/game_recs/doom_2.json new file mode 100644 index 00000000..b60cc05f --- /dev/null +++ b/bot/resources/fun/game_recs/doom_2.json @@ -0,0 +1,7 @@ +{ + "title": "Doom II", + "description": "Doom 2 was one of the first FPS games that I truly enjoyed. It offered awesome weapons, terrifying demons to kill, and a great atmosphere to do it in.", + "image": "https://upload.wikimedia.org/wikipedia/en/thumb/2/29/Doom_II_-_Hell_on_Earth_Coverart.png/220px-Doom_II_-_Hell_on_Earth_Coverart.png", + "link": "https://rawg.io/games/doom-ii", + "author": "352635617709916161" +} diff --git a/bot/resources/fun/game_recs/skyrim.json b/bot/resources/fun/game_recs/skyrim.json new file mode 100644 index 00000000..ad86db31 --- /dev/null +++ b/bot/resources/fun/game_recs/skyrim.json @@ -0,0 +1,7 @@ +{ + "title": "Elder Scrolls V: Skyrim", + "description": "The latest mainline Elder Scrolls game offered a fantastic role-playing experience with untethered freedom and a great story. Offering vast mod support, the game has endless customization and replay value.", + "image": "https://upload.wikimedia.org/wikipedia/en/1/15/The_Elder_Scrolls_V_Skyrim_cover.png", + "link": "https://rawg.io/games/the-elder-scrolls-v-skyrim", + "author": "352635617709916161" +} diff --git a/bot/resources/fun/html_colours.json b/bot/resources/fun/html_colours.json new file mode 100644 index 00000000..086083d6 --- /dev/null +++ b/bot/resources/fun/html_colours.json @@ -0,0 +1,150 @@ +{ + "aliceblue": "0xf0f8ff", + "antiquewhite": "0xfaebd7", + "aqua": "0x00ffff", + "aquamarine": "0x7fffd4", + "azure": "0xf0ffff", + "beige": "0xf5f5dc", + "bisque": "0xffe4c4", + "black": "0x000000", + "blanchedalmond": "0xffebcd", + "blue": "0x0000ff", + "blueviolet": "0x8a2be2", + "brown": "0xa52a2a", + "burlywood": "0xdeb887", + "cadetblue": "0x5f9ea0", + "chartreuse": "0x7fff00", + "chocolate": "0xd2691e", + "coral": "0xff7f50", + "cornflowerblue": "0x6495ed", + "cornsilk": "0xfff8dc", + "crimson": "0xdc143c", + "cyan": "0x00ffff", + "darkblue": "0x00008b", + "darkcyan": "0x008b8b", + "darkgoldenrod": "0xb8860b", + "darkgray": "0xa9a9a9", + "darkgreen": "0x006400", + "darkgrey": "0xa9a9a9", + "darkkhaki": "0xbdb76b", + "darkmagenta": "0x8b008b", + "darkolivegreen": "0x556b2f", + "darkorange": "0xff8c00", + "darkorchid": "0x9932cc", + "darkred": "0x8b0000", + "darksalmon": "0xe9967a", + "darkseagreen": "0x8fbc8f", + "darkslateblue": "0x483d8b", + "darkslategray": "0x2f4f4f", + "darkslategrey": "0x2f4f4f", + "darkturquoise": "0x00ced1", + "darkviolet": "0x9400d3", + "deeppink": "0xff1493", + "deepskyblue": "0x00bfff", + "dimgray": "0x696969", + "dimgrey": "0x696969", + "dodgerblue": "0x1e90ff", + "firebrick": "0xb22222", + "floralwhite": "0xfffaf0", + "forestgreen": "0x228b22", + "fuchsia": "0xff00ff", + "gainsboro": "0xdcdcdc", + "ghostwhite": "0xf8f8ff", + "goldenrod": "0xdaa520", + "gold": "0xffd700", + "gray": "0x808080", + "green": "0x008000", + "greenyellow": "0xadff2f", + "grey": "0x808080", + "honeydew": "0xf0fff0", + "hotpink": "0xff69b4", + "indianred": "0xcd5c5c", + "indigo": "0x4b0082", + "ivory": "0xfffff0", + "khaki": "0xf0e68c", + "lavenderblush": "0xfff0f5", + "lavender": "0xe6e6fa", + "lawngreen": "0x7cfc00", + "lemonchiffon": "0xfffacd", + "lightblue": "0xadd8e6", + "lightcoral": "0xf08080", + "lightcyan": "0xe0ffff", + "lightgoldenrodyellow": "0xfafad2", + "lightgray": "0xd3d3d3", + "lightgreen": "0x90ee90", + "lightgrey": "0xd3d3d3", + "lightpink": "0xffb6c1", + "lightsalmon": "0xffa07a", + "lightseagreen": "0x20b2aa", + "lightskyblue": "0x87cefa", + "lightslategray": "0x778899", + "lightslategrey": "0x778899", + "lightsteelblue": "0xb0c4de", + "lightyellow": "0xffffe0", + "lime": "0x00ff00", + "limegreen": "0x32cd32", + "linen": "0xfaf0e6", + "magenta": "0xff00ff", + "maroon": "0x800000", + "mediumaquamarine": "0x66cdaa", + "mediumblue": "0x0000cd", + "mediumorchid": "0xba55d3", + "mediumpurple": "0x9370db", + "mediumseagreen": "0x3cb371", + "mediumslateblue": "0x7b68ee", + "mediumspringgreen": "0x00fa9a", + "mediumturquoise": "0x48d1cc", + "mediumvioletred": "0xc71585", + "midnightblue": "0x191970", + "mintcream": "0xf5fffa", + "mistyrose": "0xffe4e1", + "moccasin": "0xffe4b5", + "navajowhite": "0xffdead", + "navy": "0x000080", + "oldlace": "0xfdf5e6", + "olive": "0x808000", + "olivedrab": "0x6b8e23", + "orange": "0xffa500", + "orangered": "0xff4500", + "orchid": "0xda70d6", + "palegoldenrod": "0xeee8aa", + "palegreen": "0x98fb98", + "paleturquoise": "0xafeeee", + "palevioletred": "0xdb7093", + "papayawhip": "0xffefd5", + "peachpuff": "0xffdab9", + "peru": "0xcd853f", + "pink": "0xffc0cb", + "plum": "0xdda0dd", + "powderblue": "0xb0e0e6", + "purple": "0x800080", + "rebeccapurple": "0x663399", + "red": "0xff0000", + "rosybrown": "0xbc8f8f", + "royalblue": "0x4169e1", + "saddlebrown": "0x8b4513", + "salmon": "0xfa8072", + "sandybrown": "0xf4a460", + "seagreen": "0x2e8b57", + "seashell": "0xfff5ee", + "sienna": "0xa0522d", + "silver": "0xc0c0c0", + "skyblue": "0x87ceeb", + "slateblue": "0x6a5acd", + "slategray": "0x708090", + "slategrey": "0x708090", + "snow": "0xfffafa", + "springgreen": "0x00ff7f", + "steelblue": "0x4682b4", + "tan": "0xd2b48c", + "teal": "0x008080", + "thistle": "0xd8bfd8", + "tomato": "0xff6347", + "turquoise": "0x40e0d0", + "violet": "0xee82ee", + "wheat": "0xf5deb3", + "white": "0xffffff", + "whitesmoke": "0xf5f5f5", + "yellow": "0xffff00", + "yellowgreen": "0x9acd32" +} diff --git a/bot/resources/fun/magic8ball.json b/bot/resources/fun/magic8ball.json new file mode 100644 index 00000000..f5f1df62 --- /dev/null +++ b/bot/resources/fun/magic8ball.json @@ -0,0 +1,22 @@ +[ + "It is certain", + "It is decidedly so", + "Without a doubt", + "Yes definitely", + "You may rely on it", + "As I see it, yes", + "Most likely", + "Outlook good", + "Yes", + "Signs point to yes", + "Reply hazy try again", + "Ask again later", + "Better not tell you now", + "Cannot predict now", + "Concentrate and ask again", + "Don't count on it", + "My reply is no", + "My sources say no", + "Outlook not so good", + "Very doubtful" +] diff --git a/bot/resources/fun/speedrun_links.json b/bot/resources/fun/speedrun_links.json new file mode 100644 index 00000000..acb5746a --- /dev/null +++ b/bot/resources/fun/speedrun_links.json @@ -0,0 +1,18 @@ + [ + "https://www.youtube.com/watch?v=jNE28SDXdyQ", + "https://www.youtube.com/watch?v=iI8Giq7zQDk", + "https://www.youtube.com/watch?v=VqNnkqQgFbc", + "https://www.youtube.com/watch?v=Gum4GI2Jr0s", + "https://www.youtube.com/watch?v=5YHjHzHJKkU", + "https://www.youtube.com/watch?v=X0pJSTy4tJI", + "https://www.youtube.com/watch?v=aVFq0H6D6_M", + "https://www.youtube.com/watch?v=1O6LuJbEbSI", + "https://www.youtube.com/watch?v=Bgh30BiWG58", + "https://www.youtube.com/watch?v=wwvgAAvhxM8", + "https://www.youtube.com/watch?v=0TWQr0_fi80", + "https://www.youtube.com/watch?v=hatqZby-0to", + "https://www.youtube.com/watch?v=tmnMq2Hw72w", + "https://www.youtube.com/watch?v=UTkyeTCAucA", + "https://www.youtube.com/watch?v=67kQ3l-1qMs", + "https://www.youtube.com/watch?v=14wqBA5Q1yc" +] diff --git a/bot/resources/fun/trivia_quiz.json b/bot/resources/fun/trivia_quiz.json new file mode 100644 index 00000000..8008838c --- /dev/null +++ b/bot/resources/fun/trivia_quiz.json @@ -0,0 +1,912 @@ +{ + "retro": [ + { + "id": 1, + "hints": [ + "It is not a mainline Mario Game, although the plumber is present.", + "It is not a mainline Zelda Game, although Link is present." + ], + "question": "What is the best selling game on the Nintendo GameCube?", + "answer": "Super Smash Bros" + }, + { + "id": 2, + "hints": [ + "It was released before the 90's.", + "It was released after 1980." + ], + "question": "What year was Tetris released?", + "answer": "1984" + }, + { + "id": 3, + "hints": [ + "The occupation was in construction", + "He appeared as this kind of worker in 1981's Donkey Kong" + ], + "question": "What was Mario's original occupation?", + "answer": "Carpenter" + }, + { + "id": 4, + "hints": [ + "It was revealed in the Nintendo Character Guide in 1993.", + "His last name has to do with eating Mario's enemies." + ], + "question": "What is Yoshi's (from Mario Bros.) full name?", + "answer": "Yoshisaur Munchakoopas" + }, + { + "id": 5, + "hints": [ + "The game was released in 1990.", + "It was released on the SNES." + ], + "question": "What was the first game Yoshi appeared in?", + "answer": "Super Mario World" + }, + { + "id": 6, + "hints": [ + "They were used alternatively to playing cards.", + "They generally have handdrawn nature images on them." + ], + "question": "What did Nintendo make before video games and toys?", + "answer": "Hanafuda, Hanafuda cards" + }, + { + "id": 7, + "hints": [ + "Before being Nintendo's main competitor in home gaming, they were successful in arcades.", + "Their first console was called the Master System." + ], + "question": "Who was Nintendo's biggest competitor in 1990?", + "answer": "Sega" + } + ], + "general": [ + { + "id": 100, + "question": "Name \"the land of a thousand lakes\"", + "answer": "Finland", + "info": "Finland is a country in Northern Europe. Sweden borders it to the northwest, Estonia to the south, Russia to the east, and Norway to the north. Finland is part of the European Union with its capital city being Helsinki. With a population of 5.5 million people, it has over 187,000 lakes. The thousands of lakes in Finland are the reason why the country's nickname is \"the land of a thousand lakes.\"" + }, + { + "id": 101, + "question": "Who was the winner of FIFA 2018?", + "answer": "France", + "info": "France 4 - 2 Croatia" + }, + { + "id": 102, + "question": "What is the largest ocean in the world?", + "answer": "Pacific", + "info": "The Pacific Ocean is the largest and deepest of the world ocean basins. Covering approximately 63 million square miles and containing more than half of the free water on Earth, the Pacific is by far the largest of the world's ocean basins." + }, + { + "id": 103, + "question": "Who gifted the Statue Of Liberty?", + "answer": "France", + "info": "The Statue of Liberty was a gift from the French people commemorating the alliance of France and the United States during the American Revolution. Yet, it represented much more to those individuals who proposed the gift." + }, + { + "id": 104, + "question": "Which country is known as the \"Land Of The Rising Sun\"?", + "answer": "Japan", + "info": "The title stems from the Japanese names for Japan, Nippon/Nihon, both literally translating to \"the suns origin\"." + }, + { + "id": 105, + "question": "What's known as the \"Playground of Europe\"?", + "answer": "Switzerland", + "info": "It comes from the title of a book written in 1870 by Leslie Stephen (father of Virginia Woolf) detailing his exploits of mountain climbing (not skiing) of which sport he was one of the pioneers and trekking or walking." + }, + { + "id": 106, + "question": "Which country is known as the \"Land of Thunderbolt\"?", + "answer": "Bhutan", + "info": "Bhutan is known as the \"Land of Thunder Dragon\" or \"Land of Thunderbolt\" due to the violent and large thunderstorms that whip down through the valleys from the Himalayas. The dragon reference was due to people thinking the sparkling light of thunderbolts was the red fire of a dragon." + }, + { + "id": 107, + "question": "Which country is the largest producer of tea in the world?", + "answer": "China", + "info": "Tea is mainly grown in Asia, Africa, South America, and around the Black and Caspian Seas. The four biggest tea-producing countries today are China, India, Sri Lanka and Kenya. Together they represent 75% of world production." + }, + { + "id": 108, + "question": "Which country is the largest producer of coffee?", + "answer": "Brazil", + "info": "Brazil is the world's largest coffee producer. In 2016, Brazil produced a staggering 2,595,000 metric tons of coffee beans. It is not a new development, as Brazil has been the highest global producer of coffee beans for over 150 years." + }, + { + "id": 109, + "question": "Which country is Mount Etna, one of the most active volcanoes in the world, located?", + "answer": "Italy", + "info": "Mount Etna is the highest volcano in Europe. Towering above the city of Catania on the island of Sicily, it has been growing for about 500,000 years and is in the midst of a series of eruptions that began in 2001." + }, + { + "id": 110, + "question": "Which country is called \"Battleground of Europe?\"", + "answer": "Belgium", + "info": "Belgium has been the \"Battleground of Europe\" since the Roman Empire as it had no natural protection from its larger neighbouring countries. The battles of Oudenaarde, Ramillies, Waterloo, Ypres and Bastogne were all fought on Belgian soil." + }, + { + "id": 111, + "question": "Which is the largest tropical rain forest in the world?", + "answer": "Amazon", + "info": "The Amazon is regarded as vital in the fight against global warming due to its ability to absorb carbon from the air. It's often referred to as the \"lungs of the Earth,\" as more than 20 per cent of the world's oxygen is produced there." + }, + { + "id": 112, + "question": "Which is the largest island in the world?", + "answer": "Greenland", + "info": "Commonly thought to be Australia, but as it's actually a continental landmass, it doesn't get to make it in the list." + }, + { + "id": 113, + "question": "What's the name of the tallest waterfall in the world.", + "answer": "Angel Falls", + "info": "Angel Falls (Salto \u00c1ngel) in Venezuela is the highest waterfall in the world. The falls are 3230 feet in height, with an uninterrupted drop of 2647 feet. Angel Falls is located on a tributary of the Rio Caroni." + }, + { + "id": 114, + "question": "What country is called \"Land of White Elephants\"?", + "answer": "Thailand", + "info": "White elephants were regarded to be holy creatures in ancient Thailand and some other countries. Today, white elephants are still used as a symbol of divine and royal power in the country. Ownership of a white elephant symbolizes wealth, success, royalty, political power, wisdom, and prosperity." + }, + { + "id": 115, + "question": "Which city is in two continents?", + "answer": "Istanbul", + "info": "Istanbul embraces two continents, one arm reaching out to Asia, the other to Europe." + }, + { + "id": 116, + "question": "The Valley Of The Kings is located in which country?", + "answer": "Egypt", + "info": "The Valley of the Kings, also known as the Valley of the Gates of the Kings, is a valley in Egypt where, for a period of nearly 500 years from the 16th to 11th century BC, rock cut tombs were excavated for the pharaohs and powerful nobles of the New Kingdom (the Eighteenth to the Twentieth Dynasties of Ancient Egypt)." + }, + { + "id": 117, + "question": "Diamonds are always nice in Minecraft, but can you name the \"Diamond Capital in the World\"?", + "answer": "Antwerp", + "info": "Antwerp, Belgium is where 60-80% of the world's diamonds are cut and traded, and is known as the \"Diamond Capital of the World.\"" + }, + { + "id": 118, + "question": "Where is the \"International Court Of Justice\" located at?", + "answer": "The Hague", + "info": "" + }, + { + "id": 119, + "question": "In which country is Bali located in?", + "answer": "Indonesia", + "info": "" + }, + { + "id": 120, + "question": "What country is the world's largest coral reef system, the \"Great Barrier Reef\", located in?", + "answer": "Australia", + "info": "The Great Barrier Reef is the world's largest coral reef system composed of over 2,900 individual reefs and 900 islands stretching for over 2,300 kilometres (1,400 mi) over an area of approximately 344,400 square kilometres (133,000 sq mi). The reef is located in the Coral Sea, off the coast of Queensland, Australia." + }, + { + "id": 121, + "question": "When did the First World War start?", + "answer": "1914", + "info": "The first world war began in August 1914. It was directly triggered by the assassination of the Austrian archduke, Franz Ferdinand and his wife, on 28th June 1914 by Bosnian revolutionary, Gavrilo Princip. This event was, however, simply the trigger that set off declarations of war." + }, + { + "id": 122, + "question": "Which is the largest hot desert in the world?", + "answer": "Sahara", + "info": "The Sahara Desert covers 3.6 million square miles. It is almost the same size as the United States or China. There are sand dunes in the Sahara as tall as 590 feet." + }, + { + "id": 123, + "question": "Who lived at 221B, Baker Street, London?", + "answer": "Sherlock Holmes", + "info": "" + }, + { + "id": 124, + "question": "When did the Second World War end?", + "answer": "1945", + "info": "World War 2 ended with the unconditional surrender of the Axis powers. On 8 May 1945, the Allies accepted Germany's surrender, about a week after Adolf Hitler had committed suicide. VE Day \u2013 Victory in Europe celebrates the end of the Second World War on 8 May 1945." + }, + { + "id": 125, + "question": "What is the name of the largest dam in the world?", + "answer": "Three Gorges Dam", + "info": "At 1.4 miles wide (2.3 kilometers) and 630 feet (192 meters) high, Three Gorges Dam is the largest hydroelectric dam in the world, according to International Water Power & Dam Construction magazine. Three Gorges impounds the Yangtze River about 1,000 miles (1,610 km) west of Shanghai." + }, + { + "id": 126, + "question": "Which is the smallest planet in the Solar System?", + "answer": "Mercury", + "info": "Mercury is the smallest planet in our solar system. It's just a little bigger than Earth's moon. It is the closest planet to the sun, but it's actually not the hottest. Venus is hotter." + }, + { + "id": 127, + "question": "What is the smallest country?", + "answer": "Vatican City", + "info": "With an area of 0.17 square miles (0.44 km2) and a population right around 1,000, Vatican City is the smallest country in the world, both in terms of size and population." + }, + { + "id": 128, + "question": "What's the name of the largest bird?", + "answer": "Ostrich", + "info": "The largest living bird, a member of the Struthioniformes, is the ostrich (Struthio camelus), from the plains of Africa and Arabia. A large male ostrich can reach a height of 2.8 metres (9.2 feet) and weigh over 156 kilograms (344 pounds)." + }, + { + "id": 129, + "question": "What does the acronym GPRS stand for?", + "answer": "General Packet Radio Service", + "info": "General Packet Radio Service (GPRS) is a packet-based mobile data service on the global system for mobile communications (GSM) of 3G and 2G cellular communication systems. It is a non-voice, high-speed and useful packet-switching technology intended for GSM networks." + }, + { + "id": 130, + "question": "In what country is the Ebro river located?", + "answer": "Spain", + "info": "The Ebro river is located in Spain. It is 930 kilometers long and it's the second longest river that ends on the Mediterranean Sea." + }, + { + "id": 131, + "question": "What year was the IBM PC model 5150 introduced into the market?", + "answer": "1981", + "info": "The IBM PC was introduced into the market in 1981. It used the Intel 8088, with a clock speed of 4.77 MHz, along with the MDA and CGA as a video card." + }, + { + "id": 132, + "question": "What's the world's largest urban area?", + "answer": "Tokyo", + "info": "Tokyo is the most populated city in the world, with a population of 37 million people. It is located in Japan." + }, + { + "id": 133, + "question": "How many planets are there in the Solar system?", + "answer": "8", + "info": "In the Solar system, there are 8 planets: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus and Neptune. Pluto isn't considered a planet in the Solar System anymore." + }, + { + "id": 134, + "question": "What is the capital of Iraq?", + "answer": "Baghdad", + "info": "Baghdad is the capital of Iraq. It has a population of 7 million people." + }, + { + "id": 135, + "question": "The United Nations headquarters is located at which city?", + "answer": "New York", + "info": "The United Nations is headquartered in New York City in a complex designed by a board of architects led by Wallace Harrison and built by the architectural firm Harrison & Abramovitz. The complex has served as the official headquarters of the United Nations since its completion in 1951." + }, + { + "id": 136, + "question": "At what year did Christopher Columbus discover America?", + "answer": "1492", + "info": "The explorer Christopher Columbus made four trips across the Atlantic Ocean from Spain: in 1492, 1493, 1498 and 1502. He was determined to find a direct water route west from Europe to Asia, but he never did. Instead, he stumbled upon the Americas" + } + ], + "math": [ + { + "id": 201, + "question": "What is the highest power of a biquadratic polynomial?", + "answer": "4, four" + }, + { + "id": 202, + "question": "What is the formula for surface area of a sphere?", + "answer": "4pir^2, 4πr^2" + }, + { + "id": 203, + "question": "Which theorem states that hypotenuse^2 = base^2 + height^2?", + "answer": "Pythagorean's, Pythagorean's theorem" + }, + { + "id": 204, + "question": "Which trigonometric function is defined as hypotenuse/opposite?", + "answer": "cosecant, cosec, csc" + }, + { + "id": 205, + "question": "Does the harmonic series converge or diverge?", + "answer": "diverge" + }, + { + "id": 206, + "question": "How many quadrants are there in a cartesian plane?", + "answer": "4, four" + }, + { + "id": 207, + "question": "What is the (0,0) coordinate in a cartesian plane termed as?", + "answer": "origin" + }, + { + "id": 208, + "question": "What's the following formula that finds the area of a triangle called?", + "img_url": "https://wikimedia.org/api/rest_v1/media/math/render/png/d22b8566e8187542966e8d166e72e93746a1a6fc", + "answer": "Heron's formula, Heron" + }, + { + "id": 209, + "dynamic_id": 201, + "question": "Solve the following system of linear equations (format your answer like this & ):\n{}x + {}y = {},\n{}x + {}y = {}", + "answer": "{} & {}" + }, + { + "id": 210, + "dynamic_id": 202, + "question": "What's {} + {} mod {} congruent to?", + "answer": "{}" + }, + { + "id": 211, + "question": "What is the bottom number on a fraction called?", + "answer": "denominator" + }, + { + "id": 212, + "dynamic_id": 203, + "question": "How many vertices are on a {}gonal prism?", + "answer": "{}" + }, + { + "id": 213, + "question": "What is the term used to describe two triangles that have equal corresponding sides and angle measures?", + "answer": "congruent" + }, + { + "id": 214, + "question": "⅓πr^2h is the volume of which 3 dimensional figure?", + "answer": "cone" + }, + { + "id": 215, + "dynamic_id": 204, + "question": "Find the square root of -{}.", + "answer": "{}i" + }, + { + "id": 216, + "question": "In set builder notation, what does {p/q | q ≠ 0, p & q ∈ Z} represent?", + "answer": "Rationals, Rational Numbers" + }, + { + "id": 217, + "question": "What is the natural log of -1 (use i for imaginary number)?", + "answer": "pi*i, pii, πi" + }, + { + "id": 218, + "question": "When is the *inaugural* World Maths Day (format your answer in MM/DD)?", + "answer": "03/13" + }, + { + "id": 219, + "question": "As the Fibonacci sequence extends to infinity, what's the ratio of each number `n` and its preceding number `n-1` approaching?", + "answer": "Golden Ratio" + }, + { + "id": 220, + "question": "0, 1, 1, 2, 3, 5, 8, 13, 21, 34 are numbers of which sequence?", + "answer": "Fibonacci" + }, + { + "id": 221, + "question": "Prime numbers only have __ factors.", + "answer": "2, two" + }, + { + "id": 222, + "question": "In probability, the \\_\\_\\_\\_\\_\\_ \\_\\_\\_\\_\\_ of an experiment or random trial is the set of all possible outcomes of it.", + "answer": "sample space" + }, + { + "id": 223, + "question": "In statistics, what does this formula represent?", + "img_url": "https://www.statisticshowto.com/wp-content/uploads/2013/11/sample-standard-deviation.jpg", + "answer": "sample standard deviation, standard deviation of a sample" + }, + { + "id": 224, + "question": "\"Hexakosioihexekontahexaphobia\" is the fear of which number?", + "answer": "666" + }, + { + "id": 225, + "question": "A matrix multiplied by its inverse matrix equals...", + "answer": "the identity matrix, identity matrix" + }, + { + "id": 226, + "dynamic_id": 205, + "question": "BASE TWO QUESTION: Calculate {:b} {} {:b}", + "answer": "{:b}" + }, + { + "id": 227, + "question": "What is the only number in the entire number system which can be spelled with the same number of letters as itself?", + "answer": "4, four" + + }, + { + "id": 228, + "question": "1/100th of a second is also termed as what?", + "answer": "a jiffy, jiffy, centisecond" + }, + { + "id": 229, + "question": "What is this triangle called?", + "img_url": "https://cdn.askpython.com/wp-content/uploads/2020/07/Pascals-triangle.png", + "answer": "Pascal's triangle, Pascal" + }, + { + "id": 230, + "question": "6a^2 is the surface area of which 3 dimensional figure?", + "answer": "cube" + } + ], + "science": [ + { + "id": 301, + "question": "The three main components of a normal atom are: protons, neutrons, and...", + "answer": "electrons" + }, + { + "id": 302, + "question": "As of 2021, how many elements are there in the Periodic Table?", + "answer": "118" + }, + { + "id": 303, + "question": "What is the universal force discovered by Newton that causes objects with mass to attract each other called?", + "answer": "gravity" + }, + { + "id": 304, + "question": "What do you call an organism composed of only one cell?", + "answer": "unicellular, single-celled" + }, + { + "id": 305, + "question": "The Heisenberg's Uncertainty Principle states that the position and \\_\\_\\_\\_\\_\\_\\_\\_ of a quantum object can't be both exactly measured at the same time.", + "answer": "velocity, momentum" + }, + { + "id": 306, + "question": "A \\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_ reaction is the one wherein an atom or a set of atoms is/are replaced by another atom or a set of atoms", + "answer": "displacement, exchange" + }, + { + "id": 307, + "question": "What is the process by which green plants and certain other organisms transform light energy into chemical energy?", + "answer": "photosynthesis" + }, + { + "id": 308, + "dynamic_id": 301, + "question": "What is the {} planet of our Solar System?", + "answer": "{}" + }, + { + "id": 309, + "dynamic_id": 302, + "question": "In the biological taxonomic hierarchy, what is placed directly above {}?", + "answer": "{}" + }, + { + "id": 310, + "dynamic_id": 303, + "question": "How does one describe the unit {} in SI base units?\n**IMPORTANT:** enclose answer in backticks, use \\* for multiplication, ^ for exponentiation, and place your base units in this order: m - kg - s - A", + "img_url": "https://i.imgur.com/NRzU6tf.png", + "answer": "`{}`" + }, + { + "id": 311, + "question": "How does one call the direct phase transition from gas to solid?", + "answer": "deposition" + }, + { + "id": 312, + "question": "What is the intermolecular force caused by temporary and induced dipoles?", + "answer": "LDF, London dispersion, London dispersion force" + }, + { + "id": 313, + "question": "What is the force that causes objects to float in fluids called?", + "answer": "buoyancy" + }, + { + "id": 314, + "question": "About how many neurons are in the human brain?\n(A. 1 billion, B. 10 billion, C. 100 billion, D. 300 billion)", + "answer": "C, 100 billion, 100 bil" + }, + { + "id": 315, + "question": "What is the name of our galaxy group in which the Milky Way resides?", + "answer": "Local Group" + }, + { + "id": 316, + "question": "Which cell organelle is nicknamed \"the powerhouse of the cell\"?", + "answer": "mitochondria" + }, + { + "id": 317, + "question": "Which vascular tissue transports water and minerals from the roots to the rest of a plant?", + "answer": "the xylem, xylem" + }, + { + "id": 318, + "question": "Who discovered the theories of relativity?", + "answer": "Albert Einstein, Einstein" + }, + { + "id": 319, + "question": "In particle physics, the hypothetical isolated elementary particle with only one magnetic pole is termed as...", + "answer": "magnetic monopole" + }, + { + "id": 320, + "question": "How does one describe a chemical reaction wherein heat is released?", + "answer": "exothermic" + }, + { + "id": 321, + "question": "What range of frequency are the average human ears capable of hearing?\n(A. 10Hz-10kHz, B. 20Hz-20kHz, C. 20Hz-2000Hz, D. 10kHz-20kHz)", + "answer": "B, 20Hz-20kHz" + }, + { + "id": 322, + "question": "What is the process used to separate substances with different polarity in a mixture, using a stationary and mobile phase?", + "answer": "chromatography" + }, + { + "id": 323, + "question": "Which law states that the current through a conductor between two points is directly proportional to the voltage across the two points?", + "answer": "Ohm's law" + }, + { + "id": 324, + "question": "The type of rock that is formed by the accumulation or deposition of mineral or organic particles at the Earth's surface, followed by cementation, is called...", + "answer": "sedimentary, sedimentary rock" + }, + { + "id": 325, + "question": "Is the Richter scale (common earthquake scale) linear or logarithmic?", + "answer": "logarithmic" + }, + { + "id": 326, + "question": "What type of image is formed by a convex mirror?", + "answer": "virtual image, virtual" + }, + { + "id": 327, + "question": "How does one call the branch of physics that deals with the study of mechanical waves in gases, liquids, and solids including topics such as vibration, sound, ultrasound and infrasound", + "answer": "acoustics" + }, + { + "id": 328, + "question": "Which law states that the global entropy in a closed system can only increase?", + "answer": "second law, second law of thermodynamics" + }, + { + "id": 329, + "question": "Which particle is emitted during the beta decay of a radioactive element?", + "answer": "an electron, the electron, electron" + }, + { + "id": 330, + "question": "When DNA is unzipped, two strands are formed. What are they called (separate both answers by the word \"and\")?", + "answer": "leading and lagging, leading strand and lagging strand" + } + ], + "cs": [ + { + "id": 401, + "question": "What does HTML stand for?", + "answer": "HyperText Markup Language" + }, + { + "id": 402, + "question": "What does ASCII stand for?", + "answer": "American Standard Code for Information Interchange" + }, + { + "id": 403, + "question": "What does SASS stand for?", + "answer": "Syntactically Awesome Stylesheets, Syntactically Awesome Style Sheets" + }, + { + "id": 404, + "question": "In neural networks, \\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_ is an algorithm for supervised learning using gradient descent.", + "answer": "backpropagation" + }, + { + "id": 405, + "question": "What is computing capable of performing exaFLOPS called?", + "answer": "exascale computing, exascale" + }, + { + "id": 406, + "question": "In quantum computing, what is the full name of \"qubit\"?", + "answer": "quantum binary digit" + }, + { + "id": 407, + "question": "Given that January 1, 1970 is the starting epoch of time_t in c time, and that time_t is stored as a signed 32-bit integer, when will unix time roll over (year)?", + "answer": "2038" + }, + { + "id": 408, + "question": "What are the components of digital devices that make up logic gates called?", + "answer": "transistors" + }, + { + "id": 409, + "question": "How many possible public IPv6 addresses are there (answer in 2^n)?", + "answer": "2^128" + }, + { + "id": 410, + "question": "A hypothetical point in time at which technological growth becomes uncontrollable and irreversible, resulting in unforeseeable changes to human civilization is termed as...?", + "answer": "technological singularity, singularity" + }, + { + "id": 411, + "question": "In cryptography, the practice of establishing a shared secret between two parties using public keys and private keys is called...?", + "answer": "key exchange" + }, + { + "id": 412, + "question": "How many bits are in a TCP checksum header?", + "answer": "16, sixteen" + }, + { + "id": 413, + "question": "What is the most popular protocol (as of 2021) that handles communication between email servers?", + "answer": "SMTP, Simple Mail Transfer Protocol" + }, + { + "id": 414, + "question": "Which port does SMTP use to communicate between email servers? (assuming its plaintext)", + "answer": "25" + }, + { + "id": 415, + "question": "Which DNS record contains mail servers of a given domain?", + "answer": "MX, mail exchange" + }, + { + "id": 416, + "question": "Which newline sequence does HTTP use?", + "answer": "carriage return line feed, CRLF, \\r\\n" + }, + { + "id": 417, + "question": "What does one call the optimization technique used in CPU design that attempts to guess the outcome of a conditional operation and prepare for the most likely result?", + "answer": "branch prediction" + }, + { + "id": 418, + "question": "Name a universal logic gate.", + "answer": "NAND, NOR" + }, + { + "id": 419, + "question": "What is the mathematical formalism which functional programming was built on?", + "answer": "lambda calculus" + }, + { + "id": 420, + "question": "Why is a DDoS attack different from a DoS attack?\n(A. because the victim's server was indefinitely disrupted from the amount of traffic, B. because it also attacks the victim's confidentiality, C. because the attack had political purposes behind it, D. because the traffic flooding the victim originated from many different sources)", + "answer": "D" + }, + { + "id": 421, + "question": "What is a HTTP/1.1 feature that was superseded by HTTP/2 multiplexing and is unsupported in most browsers nowadays?", + "answer": "pipelining" + }, + { + "id": 422, + "question": "Which of these languages is the oldest?\n(Tcl, Smalltalk 80, Haskell, Standard ML, Java)", + "answer": "Smalltalk 80" + }, + { + "id": 423, + "question": "What is the name for unicode codepoints that do not fit into 16 bits?", + "answer": "surrogates" + }, + { + "id": 424, + "question": "Under what locale does making a string lowercase behave differently?", + "answer": "Turkish" + }, + { + "id": 425, + "question": "What does the \"a\" represent in a HSLA color value?", + "answer": "transparency, translucency, alpha value, alpha channel, alpha" + }, + { + "id": 426, + "question": "What is the section of a GIF that is limited to 256 colors called?", + "answer": "image block" + }, + { + "id": 427, + "question": "What is an interpreter capable of interpreting itself called?", + "answer": "metainterpreter" + }, + { + "id": 428, + "question": "Due to what data storage medium did old programming languages, such as cobol, ignore all characters past the 72nd column?", + "answer": "punch cards" + }, + { + "id": 429, + "question": "Which of these sorting algorithms is not stable?\n(Counting sort, quick sort, insertion sort, tim sort, bubble sort)", + "answer": "quick, quick sort" + }, + { + "id": 430, + "question": "Which of these languages is the youngest?\n(Lisp, Python, Java, Haskell, Prolog, Ruby, Perl)", + "answer": "Java" + } + ], + "python": [ + { + "id": 501, + "question": "Is everything an instance of the `object` class (y/n)?", + "answer": "y, yes" + }, + { + "id": 502, + "question": "Name the only non-dunder method of the builtin slice object.", + "answer": "indices" + }, + { + "id": 503, + "question": "What exception, other than `StopIteration`, can you raise from a `__getitem__` dunder to indicate to an iterator that it should stop?", + "answer": "IndexError" + }, + { + "id": 504, + "question": "What type does the `&` operator return when given 2 `dict_keys` objects?", + "answer": "set" + }, + { + "id": 505, + "question": "Can you pickle a running `list_iterator` (y/n)?", + "answer": "y, yes" + }, + { + "id": 506, + "question": "What attribute of a closure contains the value closed over?", + "answer": "cell_contents" + }, + { + "id": 507, + "question": "What name does a lambda function have?", + "answer": "" + }, + { + "id": 508, + "question": "Which file contains all special site builtins, such as help or credits?", + "answer": "_sitebuiltins" + }, + { + "id": 509, + "question": "Which module when imported opens up a web browser tab that points to the classic 353 XKCD comic mentioning Python?", + "answer": "antigravity" + }, + { + "id": 510, + "question": "Which attribute is the documentation string of a function/method/class stored in (answer should be enclosed in backticks!)?", + "answer": "`__doc__`" + }, + { + "id": 511, + "question": "What is the official name of this operator `:=`, introduced in 3.8?", + "answer": "assignment-expression operator" + }, + { + "id": 512, + "question": "When was Python first released?", + "answer": "1991" + }, + { + "id": 513, + "question": "Where does the name Python come from?", + "answer": "Monty Python, Monty Python's Flying Circus" + }, + { + "id": 514, + "question": "How is infinity represented in Python?", + "answer": "float(\"infinity\"), float('infinity'), float(\"inf\"), float('inf')" + }, + { + "id": 515, + "question": "Which of these characters is valid python outside of string literals in some context?\n(`@`, `$`, `?`)", + "answer": "@" + }, + { + "id": 516, + "question": "Which standard library module is designed for making simple parsers for languages like shell, as well as safe quoting of strings for use in a shell?", + "answer": "shlex" + }, + { + "id": 517, + "question": "Which one of these protocols/abstract base classes does the builtin `range` object NOT implement?\n(`Sequence`, `Iterable`, `Generator`)", + "answer": "Generator" + }, + { + "id": 518, + "question": "What decorator is used to allow a protocol to be checked at runtime?", + "answer": "runtime_checkable, typing.runtime_checkable" + }, + { + "id": 519, + "question": "Does `numbers.Rational` include the builtin object float (y/n)", + "answer": "n, no" + }, + { + "id": 520, + "question": "What is a package that doesn't have a `__init__` file called?", + "answer":"namespace package" + }, + { + "id": 521, + "question": "What file extension is used by the site module to determine what to do at every start?", + "answer": ".pth" + }, + { + "id": 522, + "question": "What is the garbage collection strategy used by cpython to collect everything but reference cycles?", + "answer": "reference counting, refcounting" + }, + { + "id": 523, + "question": "What dunder method is used by the tuple constructor to optimize converting an iterator to a tuple (answer should be enclosed in backticks!)?", + "answer": "`__length_hint__`" + }, + { + "id": 524, + "question": "Which protocol is used to pass self to methods when accessed on classes?", + "answer": "Descriptor" + }, + { + "id": 525, + "question": "Which year was Python 3 released?", + "answer": "2008" + }, + { + "id": 526, + "question": "Which of these is not a generator method?\n(`next`, `send`, `throw`, `close`)", + "answer": "next" + }, + { + "id": 527, + "question": "Is the `__aiter__` method async (y/n)?", + "answer": "n, no" + }, + { + "id": 528, + "question": "How does one call a class who defines the behavior of their instance classes?", + "answer": "a metaclass, metaclass" + }, + { + "id": 529, + "question": "Which of these is a subclass of `Exception`?\n(`NotImplemented`, `asyncio.CancelledError`, `StopIteration`)", + "answer": "StopIteration" + }, + { + "id": 530, + "question": "What type is the attribute of a frame object that contains the current local variables?", + "answer": "dict" + } + ] +} diff --git a/bot/resources/fun/wonder_twins.yaml b/bot/resources/fun/wonder_twins.yaml new file mode 100644 index 00000000..05e8d749 --- /dev/null +++ b/bot/resources/fun/wonder_twins.yaml @@ -0,0 +1,99 @@ +water_types: + - ice + - water + - steam + - snow + +objects: + - a bucket + - a spear + - a wall + - a lake + - a ladder + - a boat + - a vial + - a ski slope + - a hand + - a ramp + - clippers + - a bridge + - a dam + - a glacier + - a crowbar + - stilts + - a pole + - a hook + - a wave + - a cage + - a basket + - bolt cutters + - a trapeze + - a puddle + - a toboggan + - a gale + - a cloud + - a unicycle + - a spout + - a sheet + - a gelatin dessert + - a saw + - a geyser + - a jet + - a ball + - handcuffs + - a door + - a row + - a gondola + - a sled + - a rocket + - a swing + - a blizzard + - a saddle + - cubes + - a horse + - a knight + - a rocket pack + - a slick + - a drill + - a shield + - a crane + - a reflector + - a bowling ball + - a turret + - a catapault + - a blanket + - balls + - a faucet + - shears + - a thunder cloud + - a net + - a yoyo + - a block + - a straight-jacket + - a slingshot + - a jack + - a car + - a club + - a vault + - a storm + - a wrench + - an anchor + - a beast + +adjectives: + - a large + - a giant + - a massive + - a small + - a tiny + - a super cool + - a frozen + - a minuscule + - a minute + - a microscopic + - a very small + - a little + - a huge + - an enourmous + - a gigantic + - a great diff --git a/bot/resources/fun/xkcd_colours.json b/bot/resources/fun/xkcd_colours.json new file mode 100644 index 00000000..3feeb639 --- /dev/null +++ b/bot/resources/fun/xkcd_colours.json @@ -0,0 +1,951 @@ +{ + "cloudy blue": "0xacc2d9", + "dark pastel green": "0x56ae57", + "dust": "0xb2996e", + "electric lime": "0xa8ff04", + "fresh green": "0x69d84f", + "light eggplant": "0x894585", + "nasty green": "0x70b23f", + "really light blue": "0xd4ffff", + "tea": "0x65ab7c", + "warm purple": "0x952e8f", + "yellowish tan": "0xfcfc81", + "cement": "0xa5a391", + "dark grass green": "0x388004", + "dusty teal": "0x4c9085", + "grey teal": "0x5e9b8a", + "macaroni and cheese": "0xefb435", + "pinkish tan": "0xd99b82", + "spruce": "0x0a5f38", + "strong blue": "0x0c06f7", + "toxic green": "0x61de2a", + "windows blue": "0x3778bf", + "blue blue": "0x2242c7", + "blue with a hint of purple": "0x533cc6", + "booger": "0x9bb53c", + "bright sea green": "0x05ffa6", + "dark green blue": "0x1f6357", + "deep turquoise": "0x017374", + "green teal": "0x0cb577", + "strong pink": "0xff0789", + "bland": "0xafa88b", + "deep aqua": "0x08787f", + "lavender pink": "0xdd85d7", + "light moss green": "0xa6c875", + "light seafoam green": "0xa7ffb5", + "olive yellow": "0xc2b709", + "pig pink": "0xe78ea5", + "deep lilac": "0x966ebd", + "desert": "0xccad60", + "dusty lavender": "0xac86a8", + "purpley grey": "0x947e94", + "purply": "0x983fb2", + "candy pink": "0xff63e9", + "light pastel green": "0xb2fba5", + "boring green": "0x63b365", + "kiwi green": "0x8ee53f", + "light grey green": "0xb7e1a1", + "orange pink": "0xff6f52", + "tea green": "0xbdf8a3", + "very light brown": "0xd3b683", + "egg shell": "0xfffcc4", + "eggplant purple": "0x430541", + "powder pink": "0xffb2d0", + "reddish grey": "0x997570", + "baby shit brown": "0xad900d", + "liliac": "0xc48efd", + "stormy blue": "0x507b9c", + "ugly brown": "0x7d7103", + "custard": "0xfffd78", + "darkish pink": "0xda467d", + "deep brown": "0x410200", + "greenish beige": "0xc9d179", + "manilla": "0xfffa86", + "off blue": "0x5684ae", + "battleship grey": "0x6b7c85", + "browny green": "0x6f6c0a", + "bruise": "0x7e4071", + "kelley green": "0x009337", + "sickly yellow": "0xd0e429", + "sunny yellow": "0xfff917", + "azul": "0x1d5dec", + "darkgreen": "0x054907", + "green/yellow": "0xb5ce08", + "lichen": "0x8fb67b", + "light light green": "0xc8ffb0", + "pale gold": "0xfdde6c", + "sun yellow": "0xffdf22", + "tan green": "0xa9be70", + "burple": "0x6832e3", + "butterscotch": "0xfdb147", + "toupe": "0xc7ac7d", + "dark cream": "0xfff39a", + "indian red": "0x850e04", + "light lavendar": "0xefc0fe", + "poison green": "0x40fd14", + "baby puke green": "0xb6c406", + "bright yellow green": "0x9dff00", + "charcoal grey": "0x3c4142", + "squash": "0xf2ab15", + "cinnamon": "0xac4f06", + "light pea green": "0xc4fe82", + "radioactive green": "0x2cfa1f", + "raw sienna": "0x9a6200", + "baby purple": "0xca9bf7", + "cocoa": "0x875f42", + "light royal blue": "0x3a2efe", + "orangeish": "0xfd8d49", + "rust brown": "0x8b3103", + "sand brown": "0xcba560", + "swamp": "0x698339", + "tealish green": "0x0cdc73", + "burnt siena": "0xb75203", + "camo": "0x7f8f4e", + "dusk blue": "0x26538d", + "fern": "0x63a950", + "old rose": "0xc87f89", + "pale light green": "0xb1fc99", + "peachy pink": "0xff9a8a", + "rosy pink": "0xf6688e", + "light bluish green": "0x76fda8", + "light bright green": "0x53fe5c", + "light neon green": "0x4efd54", + "light seafoam": "0xa0febf", + "tiffany blue": "0x7bf2da", + "washed out green": "0xbcf5a6", + "browny orange": "0xca6b02", + "nice blue": "0x107ab0", + "sapphire": "0x2138ab", + "greyish teal": "0x719f91", + "orangey yellow": "0xfdb915", + "parchment": "0xfefcaf", + "straw": "0xfcf679", + "very dark brown": "0x1d0200", + "terracota": "0xcb6843", + "ugly blue": "0x31668a", + "clear blue": "0x247afd", + "creme": "0xffffb6", + "foam green": "0x90fda9", + "grey/green": "0x86a17d", + "light gold": "0xfddc5c", + "seafoam blue": "0x78d1b6", + "topaz": "0x13bbaf", + "violet pink": "0xfb5ffc", + "wintergreen": "0x20f986", + "yellow tan": "0xffe36e", + "dark fuchsia": "0x9d0759", + "indigo blue": "0x3a18b1", + "light yellowish green": "0xc2ff89", + "pale magenta": "0xd767ad", + "rich purple": "0x720058", + "sunflower yellow": "0xffda03", + "green/blue": "0x01c08d", + "leather": "0xac7434", + "racing green": "0x014600", + "vivid purple": "0x9900fa", + "dark royal blue": "0x02066f", + "hazel": "0x8e7618", + "muted pink": "0xd1768f", + "booger green": "0x96b403", + "canary": "0xfdff63", + "cool grey": "0x95a3a6", + "dark taupe": "0x7f684e", + "darkish purple": "0x751973", + "true green": "0x089404", + "coral pink": "0xff6163", + "dark sage": "0x598556", + "dark slate blue": "0x214761", + "flat blue": "0x3c73a8", + "mushroom": "0xba9e88", + "rich blue": "0x021bf9", + "dirty purple": "0x734a65", + "greenblue": "0x23c48b", + "icky green": "0x8fae22", + "light khaki": "0xe6f2a2", + "warm blue": "0x4b57db", + "dark hot pink": "0xd90166", + "deep sea blue": "0x015482", + "carmine": "0x9d0216", + "dark yellow green": "0x728f02", + "pale peach": "0xffe5ad", + "plum purple": "0x4e0550", + "golden rod": "0xf9bc08", + "neon red": "0xff073a", + "old pink": "0xc77986", + "very pale blue": "0xd6fffe", + "blood orange": "0xfe4b03", + "grapefruit": "0xfd5956", + "sand yellow": "0xfce166", + "clay brown": "0xb2713d", + "dark blue grey": "0x1f3b4d", + "flat green": "0x699d4c", + "light green blue": "0x56fca2", + "warm pink": "0xfb5581", + "dodger blue": "0x3e82fc", + "gross green": "0xa0bf16", + "ice": "0xd6fffa", + "metallic blue": "0x4f738e", + "pale salmon": "0xffb19a", + "sap green": "0x5c8b15", + "algae": "0x54ac68", + "bluey grey": "0x89a0b0", + "greeny grey": "0x7ea07a", + "highlighter green": "0x1bfc06", + "light light blue": "0xcafffb", + "light mint": "0xb6ffbb", + "raw umber": "0xa75e09", + "vivid blue": "0x152eff", + "deep lavender": "0x8d5eb7", + "dull teal": "0x5f9e8f", + "light greenish blue": "0x63f7b4", + "mud green": "0x606602", + "pinky": "0xfc86aa", + "red wine": "0x8c0034", + "shit green": "0x758000", + "tan brown": "0xab7e4c", + "darkblue": "0x030764", + "rosa": "0xfe86a4", + "lipstick": "0xd5174e", + "pale mauve": "0xfed0fc", + "claret": "0x680018", + "dandelion": "0xfedf08", + "orangered": "0xfe420f", + "poop green": "0x6f7c00", + "ruby": "0xca0147", + "dark": "0x1b2431", + "greenish turquoise": "0x00fbb0", + "pastel red": "0xdb5856", + "piss yellow": "0xddd618", + "bright cyan": "0x41fdfe", + "dark coral": "0xcf524e", + "algae green": "0x21c36f", + "darkish red": "0xa90308", + "reddy brown": "0x6e1005", + "blush pink": "0xfe828c", + "camouflage green": "0x4b6113", + "lawn green": "0x4da409", + "putty": "0xbeae8a", + "vibrant blue": "0x0339f8", + "dark sand": "0xa88f59", + "purple/blue": "0x5d21d0", + "saffron": "0xfeb209", + "twilight": "0x4e518b", + "warm brown": "0x964e02", + "bluegrey": "0x85a3b2", + "bubble gum pink": "0xff69af", + "duck egg blue": "0xc3fbf4", + "greenish cyan": "0x2afeb7", + "petrol": "0x005f6a", + "royal": "0x0c1793", + "butter": "0xffff81", + "dusty orange": "0xf0833a", + "off yellow": "0xf1f33f", + "pale olive green": "0xb1d27b", + "orangish": "0xfc824a", + "leaf": "0x71aa34", + "light blue grey": "0xb7c9e2", + "dried blood": "0x4b0101", + "lightish purple": "0xa552e6", + "rusty red": "0xaf2f0d", + "lavender blue": "0x8b88f8", + "light grass green": "0x9af764", + "light mint green": "0xa6fbb2", + "sunflower": "0xffc512", + "velvet": "0x750851", + "brick orange": "0xc14a09", + "lightish red": "0xfe2f4a", + "pure blue": "0x0203e2", + "twilight blue": "0x0a437a", + "violet red": "0xa50055", + "yellowy brown": "0xae8b0c", + "carnation": "0xfd798f", + "muddy yellow": "0xbfac05", + "dark seafoam green": "0x3eaf76", + "deep rose": "0xc74767", + "dusty red": "0xb9484e", + "grey/blue": "0x647d8e", + "lemon lime": "0xbffe28", + "purple/pink": "0xd725de", + "brown yellow": "0xb29705", + "purple brown": "0x673a3f", + "wisteria": "0xa87dc2", + "banana yellow": "0xfafe4b", + "lipstick red": "0xc0022f", + "water blue": "0x0e87cc", + "brown grey": "0x8d8468", + "vibrant purple": "0xad03de", + "baby green": "0x8cff9e", + "barf green": "0x94ac02", + "eggshell blue": "0xc4fff7", + "sandy yellow": "0xfdee73", + "cool green": "0x33b864", + "pale": "0xfff9d0", + "blue/grey": "0x758da3", + "hot magenta": "0xf504c9", + "greyblue": "0x77a1b5", + "purpley": "0x8756e4", + "baby shit green": "0x889717", + "brownish pink": "0xc27e79", + "dark aquamarine": "0x017371", + "diarrhea": "0x9f8303", + "light mustard": "0xf7d560", + "pale sky blue": "0xbdf6fe", + "turtle green": "0x75b84f", + "bright olive": "0x9cbb04", + "dark grey blue": "0x29465b", + "greeny brown": "0x696006", + "lemon green": "0xadf802", + "light periwinkle": "0xc1c6fc", + "seaweed green": "0x35ad6b", + "sunshine yellow": "0xfffd37", + "ugly purple": "0xa442a0", + "medium pink": "0xf36196", + "puke brown": "0x947706", + "very light pink": "0xfff4f2", + "viridian": "0x1e9167", + "bile": "0xb5c306", + "faded yellow": "0xfeff7f", + "very pale green": "0xcffdbc", + "vibrant green": "0x0add08", + "bright lime": "0x87fd05", + "spearmint": "0x1ef876", + "light aquamarine": "0x7bfdc7", + "light sage": "0xbcecac", + "yellowgreen": "0xbbf90f", + "baby poo": "0xab9004", + "dark seafoam": "0x1fb57a", + "deep teal": "0x00555a", + "heather": "0xa484ac", + "rust orange": "0xc45508", + "dirty blue": "0x3f829d", + "fern green": "0x548d44", + "bright lilac": "0xc95efb", + "weird green": "0x3ae57f", + "peacock blue": "0x016795", + "avocado green": "0x87a922", + "faded orange": "0xf0944d", + "grape purple": "0x5d1451", + "hot green": "0x25ff29", + "lime yellow": "0xd0fe1d", + "mango": "0xffa62b", + "shamrock": "0x01b44c", + "bubblegum": "0xff6cb5", + "purplish brown": "0x6b4247", + "vomit yellow": "0xc7c10c", + "pale cyan": "0xb7fffa", + "key lime": "0xaeff6e", + "tomato red": "0xec2d01", + "lightgreen": "0x76ff7b", + "merlot": "0x730039", + "night blue": "0x040348", + "purpleish pink": "0xdf4ec8", + "apple": "0x6ecb3c", + "baby poop green": "0x8f9805", + "green apple": "0x5edc1f", + "heliotrope": "0xd94ff5", + "yellow/green": "0xc8fd3d", + "almost black": "0x070d0d", + "cool blue": "0x4984b8", + "leafy green": "0x51b73b", + "mustard brown": "0xac7e04", + "dusk": "0x4e5481", + "dull brown": "0x876e4b", + "frog green": "0x58bc08", + "vivid green": "0x2fef10", + "bright light green": "0x2dfe54", + "fluro green": "0x0aff02", + "kiwi": "0x9cef43", + "seaweed": "0x18d17b", + "navy green": "0x35530a", + "ultramarine blue": "0x1805db", + "iris": "0x6258c4", + "pastel orange": "0xff964f", + "yellowish orange": "0xffab0f", + "perrywinkle": "0x8f8ce7", + "tealish": "0x24bca8", + "dark plum": "0x3f012c", + "pear": "0xcbf85f", + "pinkish orange": "0xff724c", + "midnight purple": "0x280137", + "light urple": "0xb36ff6", + "dark mint": "0x48c072", + "greenish tan": "0xbccb7a", + "light burgundy": "0xa8415b", + "turquoise blue": "0x06b1c4", + "ugly pink": "0xcd7584", + "sandy": "0xf1da7a", + "electric pink": "0xff0490", + "muted purple": "0x805b87", + "mid green": "0x50a747", + "greyish": "0xa8a495", + "neon yellow": "0xcfff04", + "banana": "0xffff7e", + "carnation pink": "0xff7fa7", + "tomato": "0xef4026", + "sea": "0x3c9992", + "muddy brown": "0x886806", + "turquoise green": "0x04f489", + "buff": "0xfef69e", + "fawn": "0xcfaf7b", + "muted blue": "0x3b719f", + "pale rose": "0xfdc1c5", + "dark mint green": "0x20c073", + "amethyst": "0x9b5fc0", + "blue/green": "0x0f9b8e", + "chestnut": "0x742802", + "sick green": "0x9db92c", + "pea": "0xa4bf20", + "rusty orange": "0xcd5909", + "stone": "0xada587", + "rose red": "0xbe013c", + "pale aqua": "0xb8ffeb", + "deep orange": "0xdc4d01", + "earth": "0xa2653e", + "mossy green": "0x638b27", + "grassy green": "0x419c03", + "pale lime green": "0xb1ff65", + "light grey blue": "0x9dbcd4", + "pale grey": "0xfdfdfe", + "asparagus": "0x77ab56", + "blueberry": "0x464196", + "purple red": "0x990147", + "pale lime": "0xbefd73", + "greenish teal": "0x32bf84", + "caramel": "0xaf6f09", + "deep magenta": "0xa0025c", + "light peach": "0xffd8b1", + "milk chocolate": "0x7f4e1e", + "ocher": "0xbf9b0c", + "off green": "0x6ba353", + "purply pink": "0xf075e6", + "lightblue": "0x7bc8f6", + "dusky blue": "0x475f94", + "golden": "0xf5bf03", + "light beige": "0xfffeb6", + "butter yellow": "0xfffd74", + "dusky purple": "0x895b7b", + "french blue": "0x436bad", + "ugly yellow": "0xd0c101", + "greeny yellow": "0xc6f808", + "orangish red": "0xf43605", + "shamrock green": "0x02c14d", + "orangish brown": "0xb25f03", + "tree green": "0x2a7e19", + "deep violet": "0x490648", + "gunmetal": "0x536267", + "blue/purple": "0x5a06ef", + "cherry": "0xcf0234", + "sandy brown": "0xc4a661", + "warm grey": "0x978a84", + "dark indigo": "0x1f0954", + "midnight": "0x03012d", + "bluey green": "0x2bb179", + "grey pink": "0xc3909b", + "soft purple": "0xa66fb5", + "blood": "0x770001", + "brown red": "0x922b05", + "medium grey": "0x7d7f7c", + "berry": "0x990f4b", + "poo": "0x8f7303", + "purpley pink": "0xc83cb9", + "light salmon": "0xfea993", + "snot": "0xacbb0d", + "easter purple": "0xc071fe", + "light yellow green": "0xccfd7f", + "dark navy blue": "0x00022e", + "drab": "0x828344", + "light rose": "0xffc5cb", + "rouge": "0xab1239", + "purplish red": "0xb0054b", + "slime green": "0x99cc04", + "baby poop": "0x937c00", + "irish green": "0x019529", + "pink/purple": "0xef1de7", + "dark navy": "0x000435", + "greeny blue": "0x42b395", + "light plum": "0x9d5783", + "pinkish grey": "0xc8aca9", + "dirty orange": "0xc87606", + "rust red": "0xaa2704", + "pale lilac": "0xe4cbff", + "orangey red": "0xfa4224", + "primary blue": "0x0804f9", + "kermit green": "0x5cb200", + "brownish purple": "0x76424e", + "murky green": "0x6c7a0e", + "wheat": "0xfbdd7e", + "very dark purple": "0x2a0134", + "bottle green": "0x044a05", + "watermelon": "0xfd4659", + "deep sky blue": "0x0d75f8", + "fire engine red": "0xfe0002", + "yellow ochre": "0xcb9d06", + "pumpkin orange": "0xfb7d07", + "pale olive": "0xb9cc81", + "light lilac": "0xedc8ff", + "lightish green": "0x61e160", + "carolina blue": "0x8ab8fe", + "mulberry": "0x920a4e", + "shocking pink": "0xfe02a2", + "auburn": "0x9a3001", + "bright lime green": "0x65fe08", + "celadon": "0xbefdb7", + "pinkish brown": "0xb17261", + "poo brown": "0x885f01", + "bright sky blue": "0x02ccfe", + "celery": "0xc1fd95", + "dirt brown": "0x836539", + "strawberry": "0xfb2943", + "dark lime": "0x84b701", + "copper": "0xb66325", + "medium brown": "0x7f5112", + "muted green": "0x5fa052", + "robin's egg": "0x6dedfd", + "bright aqua": "0x0bf9ea", + "bright lavender": "0xc760ff", + "ivory": "0xffffcb", + "very light purple": "0xf6cefc", + "light navy": "0x155084", + "pink red": "0xf5054f", + "olive brown": "0x645403", + "poop brown": "0x7a5901", + "mustard green": "0xa8b504", + "ocean green": "0x3d9973", + "very dark blue": "0x000133", + "dusty green": "0x76a973", + "light navy blue": "0x2e5a88", + "minty green": "0x0bf77d", + "adobe": "0xbd6c48", + "barney": "0xac1db8", + "jade green": "0x2baf6a", + "bright light blue": "0x26f7fd", + "light lime": "0xaefd6c", + "dark khaki": "0x9b8f55", + "orange yellow": "0xffad01", + "ocre": "0xc69c04", + "maize": "0xf4d054", + "faded pink": "0xde9dac", + "british racing green": "0x05480d", + "sandstone": "0xc9ae74", + "mud brown": "0x60460f", + "light sea green": "0x98f6b0", + "robin egg blue": "0x8af1fe", + "aqua marine": "0x2ee8bb", + "dark sea green": "0x11875d", + "soft pink": "0xfdb0c0", + "orangey brown": "0xb16002", + "cherry red": "0xf7022a", + "burnt yellow": "0xd5ab09", + "brownish grey": "0x86775f", + "camel": "0xc69f59", + "purplish grey": "0x7a687f", + "marine": "0x042e60", + "greyish pink": "0xc88d94", + "pale turquoise": "0xa5fbd5", + "pastel yellow": "0xfffe71", + "bluey purple": "0x6241c7", + "canary yellow": "0xfffe40", + "faded red": "0xd3494e", + "sepia": "0x985e2b", + "coffee": "0xa6814c", + "bright magenta": "0xff08e8", + "mocha": "0x9d7651", + "ecru": "0xfeffca", + "purpleish": "0x98568d", + "cranberry": "0x9e003a", + "darkish green": "0x287c37", + "brown orange": "0xb96902", + "dusky rose": "0xba6873", + "melon": "0xff7855", + "sickly green": "0x94b21c", + "silver": "0xc5c9c7", + "purply blue": "0x661aee", + "purpleish blue": "0x6140ef", + "hospital green": "0x9be5aa", + "shit brown": "0x7b5804", + "mid blue": "0x276ab3", + "amber": "0xfeb308", + "easter green": "0x8cfd7e", + "soft blue": "0x6488ea", + "cerulean blue": "0x056eee", + "golden brown": "0xb27a01", + "bright turquoise": "0x0ffef9", + "red pink": "0xfa2a55", + "red purple": "0x820747", + "greyish brown": "0x7a6a4f", + "vermillion": "0xf4320c", + "russet": "0xa13905", + "steel grey": "0x6f828a", + "lighter purple": "0xa55af4", + "bright violet": "0xad0afd", + "prussian blue": "0x004577", + "slate green": "0x658d6d", + "dirty pink": "0xca7b80", + "dark blue green": "0x005249", + "pine": "0x2b5d34", + "yellowy green": "0xbff128", + "dark gold": "0xb59410", + "bluish": "0x2976bb", + "darkish blue": "0x014182", + "dull red": "0xbb3f3f", + "pinky red": "0xfc2647", + "bronze": "0xa87900", + "pale teal": "0x82cbb2", + "military green": "0x667c3e", + "barbie pink": "0xfe46a5", + "bubblegum pink": "0xfe83cc", + "pea soup green": "0x94a617", + "dark mustard": "0xa88905", + "shit": "0x7f5f00", + "medium purple": "0x9e43a2", + "very dark green": "0x062e03", + "dirt": "0x8a6e45", + "dusky pink": "0xcc7a8b", + "red violet": "0x9e0168", + "lemon yellow": "0xfdff38", + "pistachio": "0xc0fa8b", + "dull yellow": "0xeedc5b", + "dark lime green": "0x7ebd01", + "denim blue": "0x3b5b92", + "teal blue": "0x01889f", + "lightish blue": "0x3d7afd", + "purpley blue": "0x5f34e7", + "light indigo": "0x6d5acf", + "swamp green": "0x748500", + "brown green": "0x706c11", + "dark maroon": "0x3c0008", + "hot purple": "0xcb00f5", + "dark forest green": "0x002d04", + "faded blue": "0x658cbb", + "drab green": "0x749551", + "light lime green": "0xb9ff66", + "snot green": "0x9dc100", + "yellowish": "0xfaee66", + "light blue green": "0x7efbb3", + "bordeaux": "0x7b002c", + "light mauve": "0xc292a1", + "ocean": "0x017b92", + "marigold": "0xfcc006", + "muddy green": "0x657432", + "dull orange": "0xd8863b", + "steel": "0x738595", + "electric purple": "0xaa23ff", + "fluorescent green": "0x08ff08", + "yellowish brown": "0x9b7a01", + "blush": "0xf29e8e", + "soft green": "0x6fc276", + "bright orange": "0xff5b00", + "lemon": "0xfdff52", + "purple grey": "0x866f85", + "acid green": "0x8ffe09", + "pale lavender": "0xeecffe", + "violet blue": "0x510ac9", + "light forest green": "0x4f9153", + "burnt red": "0x9f2305", + "khaki green": "0x728639", + "cerise": "0xde0c62", + "faded purple": "0x916e99", + "apricot": "0xffb16d", + "dark olive green": "0x3c4d03", + "grey brown": "0x7f7053", + "green grey": "0x77926f", + "true blue": "0x010fcc", + "pale violet": "0xceaefa", + "periwinkle blue": "0x8f99fb", + "light sky blue": "0xc6fcff", + "blurple": "0x5539cc", + "green brown": "0x544e03", + "bluegreen": "0x017a79", + "bright teal": "0x01f9c6", + "brownish yellow": "0xc9b003", + "pea soup": "0x929901", + "forest": "0x0b5509", + "barney purple": "0xa00498", + "ultramarine": "0x2000b1", + "purplish": "0x94568c", + "puke yellow": "0xc2be0e", + "bluish grey": "0x748b97", + "dark periwinkle": "0x665fd1", + "dark lilac": "0x9c6da5", + "reddish": "0xc44240", + "light maroon": "0xa24857", + "dusty purple": "0x825f87", + "terra cotta": "0xc9643b", + "avocado": "0x90b134", + "marine blue": "0x01386a", + "teal green": "0x25a36f", + "slate grey": "0x59656d", + "lighter green": "0x75fd63", + "electric green": "0x21fc0d", + "dusty blue": "0x5a86ad", + "golden yellow": "0xfec615", + "bright yellow": "0xfffd01", + "light lavender": "0xdfc5fe", + "umber": "0xb26400", + "poop": "0x7f5e00", + "dark peach": "0xde7e5d", + "jungle green": "0x048243", + "eggshell": "0xffffd4", + "denim": "0x3b638c", + "yellow brown": "0xb79400", + "dull purple": "0x84597e", + "chocolate brown": "0x411900", + "wine red": "0x7b0323", + "neon blue": "0x04d9ff", + "dirty green": "0x667e2c", + "light tan": "0xfbeeac", + "ice blue": "0xd7fffe", + "cadet blue": "0x4e7496", + "dark mauve": "0x874c62", + "very light blue": "0xd5ffff", + "grey purple": "0x826d8c", + "pastel pink": "0xffbacd", + "very light green": "0xd1ffbd", + "dark sky blue": "0x448ee4", + "evergreen": "0x05472a", + "dull pink": "0xd5869d", + "aubergine": "0x3d0734", + "mahogany": "0x4a0100", + "reddish orange": "0xf8481c", + "deep green": "0x02590f", + "vomit green": "0x89a203", + "purple pink": "0xe03fd8", + "dusty pink": "0xd58a94", + "faded green": "0x7bb274", + "camo green": "0x526525", + "pinky purple": "0xc94cbe", + "pink purple": "0xdb4bda", + "brownish red": "0x9e3623", + "dark rose": "0xb5485d", + "mud": "0x735c12", + "brownish": "0x9c6d57", + "emerald green": "0x028f1e", + "pale brown": "0xb1916e", + "dull blue": "0x49759c", + "burnt umber": "0xa0450e", + "medium green": "0x39ad48", + "clay": "0xb66a50", + "light aqua": "0x8cffdb", + "light olive green": "0xa4be5c", + "brownish orange": "0xcb7723", + "dark aqua": "0x05696b", + "purplish pink": "0xce5dae", + "dark salmon": "0xc85a53", + "greenish grey": "0x96ae8d", + "jade": "0x1fa774", + "ugly green": "0x7a9703", + "dark beige": "0xac9362", + "emerald": "0x01a049", + "pale red": "0xd9544d", + "light magenta": "0xfa5ff7", + "sky": "0x82cafc", + "light cyan": "0xacfffc", + "yellow orange": "0xfcb001", + "reddish purple": "0x910951", + "reddish pink": "0xfe2c54", + "orchid": "0xc875c4", + "dirty yellow": "0xcdc50a", + "orange red": "0xfd411e", + "deep red": "0x9a0200", + "orange brown": "0xbe6400", + "cobalt blue": "0x030aa7", + "neon pink": "0xfe019a", + "rose pink": "0xf7879a", + "greyish purple": "0x887191", + "raspberry": "0xb00149", + "aqua green": "0x12e193", + "salmon pink": "0xfe7b7c", + "tangerine": "0xff9408", + "brownish green": "0x6a6e09", + "red brown": "0x8b2e16", + "greenish brown": "0x696112", + "pumpkin": "0xe17701", + "pine green": "0x0a481e", + "charcoal": "0x343837", + "baby pink": "0xffb7ce", + "cornflower": "0x6a79f7", + "blue violet": "0x5d06e9", + "chocolate": "0x3d1c02", + "greyish green": "0x82a67d", + "scarlet": "0xbe0119", + "green yellow": "0xc9ff27", + "dark olive": "0x373e02", + "sienna": "0xa9561e", + "pastel purple": "0xcaa0ff", + "terracotta": "0xca6641", + "aqua blue": "0x02d8e9", + "sage green": "0x88b378", + "blood red": "0x980002", + "deep pink": "0xcb0162", + "grass": "0x5cac2d", + "moss": "0x769958", + "pastel blue": "0xa2bffe", + "bluish green": "0x10a674", + "green blue": "0x06b48b", + "dark tan": "0xaf884a", + "greenish blue": "0x0b8b87", + "pale orange": "0xffa756", + "vomit": "0xa2a415", + "forrest green": "0x154406", + "dark lavender": "0x856798", + "dark violet": "0x34013f", + "purple blue": "0x632de9", + "dark cyan": "0x0a888a", + "olive drab": "0x6f7632", + "pinkish": "0xd46a7e", + "cobalt": "0x1e488f", + "neon purple": "0xbc13fe", + "light turquoise": "0x7ef4cc", + "apple green": "0x76cd26", + "dull green": "0x74a662", + "wine": "0x80013f", + "powder blue": "0xb1d1fc", + "off white": "0xffffe4", + "electric blue": "0x0652ff", + "dark turquoise": "0x045c5a", + "blue purple": "0x5729ce", + "azure": "0x069af3", + "bright red": "0xff000d", + "pinkish red": "0xf10c45", + "cornflower blue": "0x5170d7", + "light olive": "0xacbf69", + "grape": "0x6c3461", + "greyish blue": "0x5e819d", + "purplish blue": "0x601ef9", + "yellowish green": "0xb0dd16", + "greenish yellow": "0xcdfd02", + "medium blue": "0x2c6fbb", + "dusty rose": "0xc0737a", + "light violet": "0xd6b4fc", + "midnight blue": "0x020035", + "bluish purple": "0x703be7", + "red orange": "0xfd3c06", + "dark magenta": "0x960056", + "greenish": "0x40a368", + "ocean blue": "0x03719c", + "coral": "0xfc5a50", + "cream": "0xffffc2", + "reddish brown": "0x7f2b0a", + "burnt sienna": "0xb04e0f", + "brick": "0xa03623", + "sage": "0x87ae73", + "grey green": "0x789b73", + "white": "0xffffff", + "robin's egg blue": "0x98eff9", + "moss green": "0x658b38", + "steel blue": "0x5a7d9a", + "eggplant": "0x380835", + "light yellow": "0xfffe7a", + "leaf green": "0x5ca904", + "light grey": "0xd8dcd6", + "puke": "0xa5a502", + "pinkish purple": "0xd648d7", + "sea blue": "0x047495", + "pale purple": "0xb790d4", + "slate blue": "0x5b7c99", + "blue grey": "0x607c8e", + "hunter green": "0x0b4008", + "fuchsia": "0xed0dd9", + "crimson": "0x8c000f", + "pale yellow": "0xffff84", + "ochre": "0xbf9005", + "mustard yellow": "0xd2bd0a", + "light red": "0xff474c", + "cerulean": "0x0485d1", + "pale pink": "0xffcfdc", + "deep blue": "0x040273", + "rust": "0xa83c09", + "light teal": "0x90e4c1", + "slate": "0x516572", + "goldenrod": "0xfac205", + "dark yellow": "0xd5b60a", + "dark grey": "0x363737", + "army green": "0x4b5d16", + "grey blue": "0x6b8ba4", + "seafoam": "0x80f9ad", + "puce": "0xa57e52", + "spring green": "0xa9f971", + "dark orange": "0xc65102", + "sand": "0xe2ca76", + "pastel green": "0xb0ff9d", + "mint": "0x9ffeb0", + "light orange": "0xfdaa48", + "bright pink": "0xfe01b1", + "chartreuse": "0xc1f80a", + "deep purple": "0x36013f", + "dark brown": "0x341c02", + "taupe": "0xb9a281", + "pea green": "0x8eab12", + "puke green": "0x9aae07", + "kelly green": "0x02ab2e", + "seafoam green": "0x7af9ab", + "blue green": "0x137e6d", + "khaki": "0xaaa662", + "burgundy": "0x610023", + "dark teal": "0x014d4e", + "brick red": "0x8f1402", + "royal purple": "0x4b006e", + "plum": "0x580f41", + "mint green": "0x8fff9f", + "gold": "0xdbb40c", + "baby blue": "0xa2cffe", + "yellow green": "0xc0fb2d", + "bright purple": "0xbe03fd", + "dark red": "0x840000", + "pale blue": "0xd0fefe", + "grass green": "0x3f9b0b", + "navy": "0x01153e", + "aquamarine": "0x04d8b2", + "burnt orange": "0xc04e01", + "neon green": "0x0cff0c", + "bright blue": "0x0165fc", + "rose": "0xcf6275", + "light pink": "0xffd1df", + "mustard": "0xceb301", + "indigo": "0x380282", + "lime": "0xaaff32", + "sea green": "0x53fca1", + "periwinkle": "0x8e82fe", + "dark pink": "0xcb416b", + "olive green": "0x677a04", + "peach": "0xffb07c", + "pale green": "0xc7fdb5", + "light brown": "0xad8150", + "hot pink": "0xff028d", + "black": "0x000000", + "lilac": "0xcea2fd", + "navy blue": "0x001146", + "royal blue": "0x0504aa", + "beige": "0xe6daa6", + "salmon": "0xff796c", + "olive": "0x6e750e", + "maroon": "0x650021", + "bright green": "0x01ff07", + "dark purple": "0x35063e", + "mauve": "0xae7181", + "forest green": "0x06470c", + "aqua": "0x13eac9", + "cyan": "0x00ffff", + "tan": "0xd1b26f", + "dark blue": "0x00035b", + "lavender": "0xc79fef", + "turquoise": "0x06c2ac", + "dark green": "0x033500", + "violet": "0x9a0eea", + "light purple": "0xbf77f6", + "lime green": "0x89fe05", + "grey": "0x929591", + "sky blue": "0x75bbfd", + "yellow": "0xffff14", + "magenta": "0xc20078", + "light green": "0x96f97b", + "orange": "0xf97306", + "teal": "0x029386", + "light blue": "0x95d0fc", + "red": "0xe50000", + "brown": "0x653700", + "pink": "0xff81c0", + "blue": "0x0343df", + "green": "0x15b01a", + "purple": "0x7e1e9c" +} -- cgit v1.2.3