Category: Uncategorized

  • Is Proxima Centauri B a New Home for Us?

    Introduction

    This blog will report my research on the Habitability of our closest exoplanet – Proxima Centauri B – and its potential challenges. This will inform my next steps to explore potential devices for humans who wish to survive there.


    Our Closest Solar Neighbour

    Proxima Centauri is the nearest star to us. It is still far to travel there, but we are in a hope of advanced technology of space vehicle to make it happen.

    >> Discovery

    • Robert T. A. Innes (1917) discovered Proxima Centauri. He used photographic astrometry to estimate the distance (4 light-years) and gave the name “Proxima” to the star, meaning “nearest in Latin”.

    >> Mass Estimation

    • According to the data used by Anglada-Escudé et al. (2016), Proxima Centauri has an estimated mass of only 0.120±0.015 times that of the Sun, a radius of 0.140±0.012 times that of the Sun and an effective temperature of 3050±100 K. It is among the smallest main sequence stars known with a mass only about a third again more than the least massive normal star theoretically possible.

    Potential Earth-Like Planet

    Piquito veloz´s screenshot – This image was created with Celestia. Proxima Cen b texture by NASA/JPL-Caltech/MIT (2020) (license). Earth texture by NASA Blue Marble (2002) (license) Default cloud map in Celestia. Purple background is an adapted version since the Planck’s Cosmic Microwave Background Map (2015 alt. color scheme) (license).

    Proxima Centauri B is the closest planet to Proxima Centauri.

    >> Discovery and Distance

    • Proxima Centauri B was found using Radial Velocity (Anglada-Escudé et al.,2016). Radial Velocity also gives us the Orbital Period, Minimum Mass, Eccentricity, and the Semi-Major Axis. 
    • Orbital distance: 0.05 AU (5% of the Earth-Sun distance) 
    • Orbital period: 11.2 days 
    • Multiple independent telescope datasets confirm the same orbit.

    Proxima Centauri B is similar in size and mass to our home planet – Earth, which may signal Earth-like physical conditions that are suitable for human living.

    >> Mass Estimation – minimum 1.27 Earth Masses

    • Minimum Mass: Anglada-Escudé et al. (2016) estimated the minimum mass of Proxima Centauri B by using Radial Velocity measurements from HARPS and UVES.
    • Maximum Mass: Based on statistical analysis of Kepler results performed by Leslie Rogers (Hubble Fellow at Caltech) and others, it is known that exoplanets seem to transition from being predominantly rocky to predominantly volatile-rich probably at a radius of about 1.5 RE and certainly no greater than 1.6 RE. A planet with this radius corresponds to a mass of about 6 ME, assuming an Earth-like composition. With an unconstrained orbital inclination, there is about a 98% chance that Proxima Centauri b (minimum mass of 1.27 ME) has an actual mass below this threshold.

    >> Size Estimation – 1.1~1.3 Earth radii

    • Proxima Centauri b’s radius was not measured directly but estimated by empirical models with assumption of rocky composition and no large gas envelope.
    • The calculation was based on Mass-Radius Relation from other known rocky exoplanets and the 1.27 minimum Earth masses. 
    • Anglada-Escudé et al. (2016) mentioned that there is a 1.5% chance that the orbit of Proxima Centauri b is oriented to produce transits visible to us here on Earth. If such transits actually occur, the actual mass and the radius of this new exoplanet could be determined in the near future.

    >> Rocky Composition or Ocean Planet?

    • Based on the estimated minimum mass, the planet is likely rocky comparing other planets in this mass range (Anglada-Escudé et al.,2016) 
    • Brugger et al. (2016) modelled possible compositions and confirmed that a rocky planet with or without some water is the most plausible scenario.

    Potential Habitable Features

    NASA – https://science.nasa.gov/universe/exoplanets/an-earth-like-atmosphere-may-not-survive-proxima-bs-orbit/

    Proxima Centauri B is within the habitable zone of Proxima Centauri. It speculatively has habitable Features.

    >> What is the Habitable Zone (HZ) of a star? 

    • The HZ of a star is the region around a star where the temperature of an exoplanet could be good for liquid water to exist. 

    >> How do we know Proxima Centauri B is within Proxima Centauri HZ?

    • Based on detailed climate and geophysical modelling, for a star like Proxima Centauri with a surface temperature of 3050 K and an Earth-size planet orbiting Proxima Centauri, the outer and inner limit of the HZ is between 0.081 AU and 0.041 AU. (Kopparapu et al.,2013, 2014). Proxima Centauri b is 0.04848 AU from Proxima Centauri.

    Gravity Conditions may be suitable for life

    >> Habitable Range

    • 0.75 – 1.05 times Earth gravity calculated by gravity formula with mass and radius of Proxima Centauri b.
    • The estimated gravity is within a habitable range (0.5-2g Earth gravities).

    Temperatures could be liveable with an Earth-like atmosphere

    >> How would we estimate it?

    • Meadows et al. (2018) used climate-photochemistry models to simulate several plausible states for the atmosphere environment of Proxima Cen b. For Earth-like atmosphere, only modest amounts of carbon dioxide (0.05 bar) or methane (0.01–0.03 bar) are required to warm the planetary surface and can obtain cold but habitable surface conditions.

    >> Complicating factors – Atmosphere and Rotation

    • Stellar flares could strip the atmosphere to make the surface hostile.
    • Tidally locked: 7°C to 27 °C on dayside, -223°C to -123°C on nightside depending on atmosphere thickness. With ocean heat transport, temperature differences are smaller. (Turbet et al.,2016 using 3D climate models)

    Water as the source of life may exist within the HZ

    >> How would we know?

    • No direct observation yet of liquid water (or water vapor) on Proxima b.
    • Most of our estimates are based on theoretical modeling and simulations.
    • Water could possibly exist on Proxima Centauri b according to many models. Brugger et al. (2016) showed models where up to ~50% water by mass is possible, leading to a deep global ocean. Coleman et al. (2016/17) modelled several distinct formation paths and found that the planet’s water content depends strongly on where and how it formed. Meadows et al. (2018) found that whether water is currently present depends heavily on atmospheric composition and how much the star’s radiation has stripped things away over time.

    A range of gases (CO₂, N₂, H₂O) may exist to support human life, plant growth, and protect inhabitants

    >> How would we know?

    • Noack, Kislyakova, Johnstone, et al. (2021) modeled interior heating (including induction heating) and long-term outgassing for Proxima b, and suggested that volcanic outgassing could supply CO₂, H₂O, and other volatiles, depending on interior composition and thermal evolution. This is important because the supply of gas from the planet’s interior could help maintain an atmosphere against losses.

    >> Atmospheric Challenges

    • The potential gases depend on planet’s formation and atmospheric retention.
    • Many models suggested that high stellar flaring, XUV flux, and particle radiation significantly influence atmospheric chemistry and loss.

    Potential Challenges

    NASA – https://science.nasa.gov/universe/exoplanets/neighboring-stars-bad-behavior-large-and-frequent-flares/

    Damaging Stellar Flares

    >> Harmful effects on Proxima Centauri b

    • The high-energy radiation from the stellar flare can heat and erode the atmosphere, break apart water molecules, destroy potential ozone layer, and threaten any potential lifeforms on the surface.
    • Entire spectrum of electromagnetic radiation 

    >> Extremely strong X-ray and UV radiation

    • Proxima b’s XUV (X-ray + extreme-UV) flux is much higher than Earth’s; “nearly 60 times higher than Earth” for its high-energy flux, according to full spectral energy distribution model constructed by Ribas, Gregg, Boyajian, & Bolmont (2017).

    >> Extremely high-energy solar flare burst 

    • The biggest flare briefly made the star 14,000 times brighter than normal as observed by MacGregor led group in 2019 by using simultaneous observations with nine telescopes. (NASA, 2017)

    Very Thin or Absent Atmosphere

    • Atmospheres are also essential for life as we know it: Having the right atmosphere allows for climate regulation, the maintenance of a water-friendly surface pressure, shielding from hazardous space weather, and the housing of life’s chemical building blocks. 
    • Stellar flare strips away the atmosphere and Earth-like atmosphere would not survive. Katherine Garcia-Sage et al. (2017)’s model suggested that Proxima Centauri’s powerful radiation drains the Earth-like atmosphere as much as 10,000 times faster than what happens at Earth.

    Lack of Strong Magnetic Field Protection

    • Interior models suggested magnetic fields are possible and likely multipolar in nature due to slow rotation speeds. The field strength was predicted to have values of 0.06 – 0.23G. (Herath et al., 2010). It is lower than Earth magnetic fields value ~0.3G.

    Tidally Locked Orbit

    • Proxima Centauri B is also highly likely to be tidally locked, as it is much closer to Proxima Centauri than Earth to the Sun. Rather than having a day/night cycle like Earth, Proxima Centauri B would have one hemisphere in constant sunlight, and the other in constant dark. (NASA, 2020)

    Potential Vehicle Design Implications

    Finch (flm) directed by Miguel Sapochnik
    https://wallpapercat.com/finch-movie-wallpapers

    Thermal Control

    • Without a thicker atmosphere, the vehicle must be heavily insulated with active thermal control systems to maintain stable temperatures.

    Heavy Shielding

    • The vehicle must have heavy shielding to protect its vital components from radiation and cosmic rays. (for example, lead)

    Dust Protection

    • The vehicle’s joints and sensors may need to be sealed and protected to prevent dust (like on Mars) from wearing down and disrupting components.

    Autonomous Navigation

    • Sometimes there are environments where it is too difficult or expensive to protect humans on the vehicle, so the the vehicle would need Autonomous Navigation to move without human control. Additionally, Autonomous Navigation may be able to control a bulky vehicle in unknown environments better and safer than a human could.

    Movement

    • Without a thicker atmosphere, the vehicle cannot rely on any form of aerodynamic movement or braking.

    Limitation on Data Collection

    Due to the fact that Proxima Centauri B hasn’t transitted, it eludes the usual method for learning about its atmosphere. Instead, scientists must rely on models to understand whether the exoplanet is habitable.

    >> What is a Transit?

    • A transit occurs when a planet passes between a star and its observer. Transits within our solar system can be observed from Earth when Venus or Mercury travel between us and the Sun. (NASA, 2020)
    • The planet passing in front of its star ever so slightly dims its light. This dimming can be seen in light curves: graphs showing light received over a period of time. (NASA, 2020)
    • However, Proxima Centauri B has not been detected passing in front of its star. (NASA, 2020)

    >> Importance of Transits

    • Transits can help determine a variety of different exoplanet characteristics: the size of its orbit, its orbital period, and the size of the planet itself. (NASA, 2020)
    • We can also learn about an exoplanet’s atmosphere during a transit. As it transits, some light will go through its atmosphere and that light can be analyzed to determine what different atmospheric elements influenced its particular dispersion. (NASA, 2020)
    • These can help determine the temperature of the planet itself. This can tell us whether the surface has a comfortable temperature suitable for life. (NASA, 2020)

    AI Use Statement

    I asked AI specific questions and resources for its answers. Then I selected the resources I used and found their original papers to ensure the credibility of the answers.

    AI Transcript – Google Drive


    References

    Anglada-Escudé, G., Amado, P., Barnes, J. et al (2016). A terrestrial planet candidate in a temperate orbit around Proxima Centauri. Nature 536, 437–440. https://doi.org/10.1038/nature19106

    Barnes, R. (2024). Can Humans Live on Proxima b? – Analyzing the Current Facts. PaleRedDot Exoplanetarium https://www.palereddot.org/opportunities-and-obstacles-for-life-on-proxima-b/

    Brugger, B., Mousis, O., Deleuil, M., Deschamps, F. (2017). Constraints on Super-Earth Interiors from Stellar Abundances. The Astrophysical Journal 850 https://www.doi.org/10.3847/1538-4357/aa965a

    Buis, A. (2021). Earth’s Magnetosphere: Protecting Our Planet from Harmful Space Energy. NASA Science https://science.nasa.gov/science-research/earth-science/earths-magnetosphere-protecting-our-planet-from-harmful-space-energy/

    Coleman, G. A. L., Nelson, R. P., Paardekooper, S. J., Dreizler, S., Giesers, B., Anglada-Escudé, G. (2017). Exploring plausible formation scenarios for the planet candidate orbiting Proxima Centauri. Monthly Notices of the Royal Astronomical Society 467, 996–1007, https://doi.org/10.1093/mnras/stx169

    Garcia-Sage, K., Glocer, A., Drake, J., Gronoff, G., Cohen, O. (2017). On the Magnetic Protection of the Atmosphere of Proxima Centauri B. The Astrophysical Journal Letters 884 https://doi.org/10.3847/2041-8213/aa7eca

    Gilster, P. (2016). Proxima b: Obstacles and Opportunities. Centauri Dreams. https://www.centauri-dreams.org/2016/09/01/proxima-b-opportunities-and-obstacles/

    Herath, M., Gunesekera, S., Jayaratne, C.(2021). Characterizing the possible interior structures of the nearby Exoplanets Proxima Centauri B and Ross-128 B. Monthly Notices of the Royal Astronomical Society 500, 333–354, https://doi.org/10.1093/mnras/staa3110

    Kopparapu, R., Ramirez, R., SchottelKotte, J., Kasting, J., Domagol-Goldman, S., Eymet, V. (2014). HABITABLE ZONES AROUND MAIN-SEQUENCE STARS: DEPENDENCE ON PLANETARY MASS. The Astrophysical Journal Letters 787 https://doi.org/10.1088/2041-8205/787/2/L29

    LePage, A. (2016). Habitable Planet Reality Check: Proxima Centauri b. DrewExMachina – Astronomy. https://www.drewexmachina.com/2016/08/29/habitable-planet-reality-check-proxima-centauri-b/

    Meadows, V. S., Arney, G. N., Schwieterman, E. W., Lustig-Yaeger, J., Lincowski, A. P., Robinson, T., Domagal-Goldman, S. D., Deitrick, R., Barnes, R. K., Fleming, D. P., Luger, R., Driscoll, P. E., Quinn, T. R., & Crisp, D. (2018). The Habitability of Proxima Centauri b: Environmental States and Observational Discriminants. Astrobiology, 18(2), 133–189. https://doi.org/10.1089/ast.2016.1589

    National Aeronautics and Space Administration. (2017). Imagine the Universe – Cosmic Rays. https://imagine.gsfc.nasa.gov/science/toolbox/cosmic_rays1.html

    National Aeronautics and Space Administration. (2017). An Earth-like atmosphere may not survive Proxima b’s orbit. https://science.nasa.gov/universe/exoplanets/an-earth-like-atmosphere-may-not-survive-proxima-bs-orbit/

    National Aeronautics and Space Administration. (2021). An Earth-like atmosphere may not survive Proxima b’s orbit. https://science.nasa.gov/universe/exoplanets/neighboring-stars-bad-behavior-large-and-frequent-flares/

    National Aeronautics and Space Administration. (2020). Earth Versus Proxima Centauri b Rotation Rates. https://svs.gsfc.nasa.gov/4778/

    National Aeronautics and Space Administration. (2020). Earth What’s a transit? https://science.nasa.gov/exoplanets/whats-a-transit/

    National Aeronautics and Space Administration. (2019). Proxima Centauri B. https://science.nasa.gov/exoplanet-catalog/proxima-centauri-b/

    Noack, L., Kislyakova, K., Johnstone, C., Güdel, M., Fossati, L. (2021). Interior heating and outgassing of Proxima Centauri b: Identifying critical parameters. Astronomy & Astrophysics 651 https://doi.org/10.1051/0004-6361/202040176

    Ribas, I., Gregg, M. Boyajian, T., Bolmont, E. (2017). The full spectral radiative properties of Proxima Centauri. Astronomy & Astrophysics 603 https://doi.org/10.1051/0004-6361/201730582

    The European Space Agency. What are Solar Flares? https://www.esa.int/Science_Exploration/Space_Science/What_are_solar_flares

    Turbet, M., Leconte, J., Selsis, F., Bolmont, E., Forget, F., Ribas, I., Raymond, S., Anglada-Escudé, G. (2016). The habitability of Proxima Centauri b. Astronomy & Astrophysics 596 https://doi.org/10.1051/0004-6361/201629577

  • Rotating Display

    This project intends to demonstrate the basics of robotics through the use of Arduino. I was really interested in the Geneva Drive mechanism, so I decided to base my project around it.

    I designed a display stand that can rotate to show four different things. However, a constantly rotating display stand would make it hard to see each thing clearly. By using a Geneva drive, I can transform a constant turning motion into separate 90° turns with pauses in between. Additionally, I added LEDs to light up each display item with a different colour and set them up to match the rotations of the display.

    I selected four Lego Ninjago Minifigures to put on the display as Lego Ninjago is one of my favourite tv show and an integral part of my childhood.

    Video Demo

    https://drive.google.com/file/d/1XJ2-KDv5k0xWASW0IL4BOMyf_LY_ffpl/view

    Virtual Prototype

    ◈ Onshape Design

    Geneva Drive Mechanism Demo


    Geneva Drive Mechanism (Driven Wheel, Driving Wheel, and Wheel Guide)


    Motor Adapter and Connector to Driving Wheel and Wheel Guide


    Display Stand and Connector to Driven Wheel


    Full Assembly


    ◈ Circuit Schematic

    TinkerCAD Circuit


    Auto-Generated TinkerCAD Circuit Schematic


    The Motor is connected to a 3.3V and a different grounded resistor. I tested many different resistance values to find a suitable rotation speed and landed with a 10Ω resistor.

    The 4 LEDs are each connected to a different Arduino Pin (D2 to D5) and then to the same grounded resistor (220Ω).


    ◈ Bill of Materials (BOM)

    Auto-Generated TinkerCAD Bill of Materials

    I also need many wires and a breadboard to connect these components.


    Laser-cut pieces


    Lego pieces


    ◈ Arduino Code

    const int PINS[] = {2, 3, 4, 5};
    const int RPM = 30;
    const int DELAYMS = 60000 / RPM;
    int idx = 3;
    
    void setup() {
      for (int pin : PINS) {
        pinMode(pin, OUTPUT);
        digitalWrite(pin, LOW);
      }
      // Serial.begin(9600);
    }
    
    void loop() {
      digitalWrite(PINS[idx], LOW);
      idx = (idx + 1) % 4;
      digitalWrite(PINS[idx], HIGH);
      delay(DELAYMS);
    }

    I included 3 constants in my code:

    • “PINS” is an array that stores which pins are connected to the LEDs.
    • “RPM” is a constant that stores how many rotations per minute the motor spins.
    • “DELAYMS” is the number of milliseconds per revolution given the RPM.

    The “setup” function runs on start, sets pins 2 through 5 to OUTPUT pin mode, and turns them off.

    The “loop” function loops continuously and turns off the previous light and turns on the next light. “loop” will repeat once every revolution.

    Physical Prototype

    ◈ From Virtual to Physical

    My virtual prototype did not account for gravity. In the physical prototype, many of the axles were wobbling and leaning to the sides. To fix this issue, I added many support structures to stabilize the axles.

    To keep the entire Arduino system within the physical base, I used a separate battery source to power the Arduino, rather than connecting to my computer.

    My virtual prototype lacked a switch to control the motor. It was annoying to have to take out and put back the wires to control the motor, so I decided to connect a switch between the 3.3V source and motor.

    ◈ Photos

    Rotating Display System From Four Sides


    Close-up Inner Assembly (Breadboard, Arduino, Battery, Motor, Wiring)


    Close-up Geneva Drive Mechanism

    AI Usage

    As I had some experience with robotics already, I did not need to use AI to learn robotics.


    Revision

    This is my revised design of circuit that allows the arduino to control the starting and stopping of the motor that rotates my display.

    ◈ Circuit Schematic

    TinkerCAD Circuit


    Auto-Generated TinkerCAD Circuit Schematic


    I added an NPN transistor to control the motor, and a slideswitch to get user input.

    The Motor is connected to the collector pin of the NPN transistor and a 5V power source through a resistor.

    The NPN transistor’s emitter pin is connected to ground and its base pin is connected to Arduino Pin 7 through a 220Ω resistor. This allows the Arduino to control when the motor is “on” or “off” through Arduion Pin 7.

    The slideswitch is connected to power, ground, and Arduino Pin 2, which is in “input_pullup” mode to read the user’s slideswitch input.


    ◈ Bill of Materials (BOM)

    Auto-Generated TinkerCAD Bill of Materials

    I also need many wires and a breadboard to connect these components.


    ◈ Arduino Code

    const int PINS[] = {3, 4, 5, 6};
    const int PIN_M = 7;
    const int PIN_S = 2;
    const int RPM = 30;
    const int eps = 53;
    const int DELAYMS = 60000 / RPM - eps;
    int idx = 0;
    
    void setup() {
      for (int pin : PINS) {
        pinMode(pin, OUTPUT);
        digitalWrite(pin, LOW);
      }
      pinMode(PIN_M, OUTPUT);
      digitalWrite(PIN_M, LOW);
      pinMode(PIN_S, INPUT_PULLUP);
    }
    
    void loop() {
      if (digitalRead(PIN_S) != 1) {
        digitalWrite(PINS[idx], HIGH);
        digitalWrite(PIN_M, HIGH);
        delay(DELAYMS);
        digitalWrite(PINS[idx], LOW);
        digitalWrite(PIN_M, LOW);
        idx = (idx + 1) % 4;
        delay(1000); 
      } else {
        delay(100);
      }
    }

    I added 3 new constants in my code to control the motor:

    • “PIN_M” is a constant that stores which pin controls the motor.
    • “PIN_S” is a constant that stores which pin reads the slideswitch input.
    • “eps” is a constant that adjusts for rotation error. The motor spins at RPM rotations per minute but turning it off after DELAYMS milliseconds isn’t perfect, so we use “eps” to fix it.

    I changed the “setup” function accordingly with the 2 new pins.

    I also added slideswitch input handling (if statement) and motor on/off control (digitalWrite(PIN_M)) to the “loop” function.

    Additionally, I added a short 1 second delay to increase the pause between each rotation. In turn, this makes each side of the display stop for a longer time.

  • Second Project – CAD

    Overview

    I made a mechanism that can open and close a claw. It is controlled by sliding one part.

    Skills

    Sketching

    I drew a hooked stick and two straight sticks for the grabber, slider, and connection parts.

    I drew a rectangle split into three sections for the shaft.

    Each part has appropriate holes of diameter 5mm.

    Sketch for the Grabber Part.
    Extruding

    The entire mechanism will be split into three vertical 2.5mm thick layers: Shaft Floor, Lower, and Upper.

    The grabber will consist of two parts: a handle, a hook. The handle will be 1 layer and the hook is 2 layers thick.

    The slider is 2 layers thick, and the connector is 1 layer thick.

    The shaft is made of the shaft floor, walls, and joint. The walls and joint are on the lower layer.

    Extrusion of Handle and Hook for the Grabber Part.
    Assembly

    I connected the grabber to connectors and connectors to the slider with revolve mates.

    The slider and shaft with a slider mate.

    The shaft and the origin with a fasten mate.

    Full Assembly
    Mechanical Drawing

    For each part:

    1. Top/Bottom
    2. Left/Right
    3. Isometric
    4. Appropriate Measurements

    For the assembly:

    1. Isometric
    Mechanical Drawing for the Grabber Part.
    Bill of Materials

    https://sgs.onshape.com/documents/da92b8df7297a672c9fd55ea/w/34b0cf1f63d108998f1efccf/e/94408fd0c280806c1f893b32?renderMode=0&uiState=68e2b66bf844e7cf94edf4c9

    AI Usage

    I did not use AI for this assignment.

  • First Project – Calendar App

    Intent

    For this project I made a Calendar App where you can add events and view your event schedule for the day. These events can also be repeating.

    This project is meant as a tool to help with time-management and planning.

    Code Planning

    Part One: Save Event Format

    1. Events are listed in increasing end time order.
    2. Event Format
      • -Completeness
      • -EndTime
      • -StartTime
      • -StepTime
      • -Description
      • #

    Example Save File

    -0
    -1759302000
    -1756710000
    -86400
    -Daily 7am reminder 
    #

    Part Two: Load Event Format

    1. Each event will be stored as a list
      • [end time, start time, step amt, name, desc, finished]
    2. Multiple events will be stored in a master list and sorted by increasing end time.

    Part Three: Get Events on a Specific Day

    Returns a list of event indices and event times

    1. First Check: Did this group of events already end
    2. Second Check: Did this group of events start yet
    3. Third Check: Is there an event TODAY.
      • Let k be the smallest integer where
        • Start + k * Step > Date
      • The event is TODAY if
        • Start + k * step < Date + 86400
    4. Calculate the time of the event on that day
      • Event Time = Start + k * step – Date
    5. Add tuple to a list
      • (Event Index, Event Time)
    6. Return this list

    Part Four: Get and Update Inputs of Mouse and Keyboard

    1. Save Inputs from Previous Frame
    2. Save Inputs from Current Frame
    3. Save Mouse Position

    Part Five: Query Inputs of Mouse and Keyboard

    1. Parameters: Mode (String), Key (id)
      • Key identifies which specific input we are querying.
    2. “down” Mode: Return if the specific input is on
    3. “press” Mode: Return if the specific input was turned on this frame
    4. “release” Mode: Return if the specific input was turned off this frame

    Part Six: Add Event

    1. Get Inputs: Start Time, End Time, Step, Name, and Description
    2. Insert Event into Events list while maintaining sorted by end time. (USE LOAD EVENT FORMAT)

    Part Seven: Remove Event

    1. Delete event from events list.

    Note: All times will be saved using seconds since epoch.

    Flowchart

    The following flowchart shows the structure and logic of how the above is implemented.

    Flowchart

    Code

    File Loader Class
    To save and load events and get events on a specific day. (See parts One to Three)
    class CalendarLoader:
        def __init__(self, filename):
            self.events = []
            self.filename = filename
    
        def set_file(self, filename):
            self.filename = filename
    
        def load_events(self, filepath=None):
            if filepath is None:
                filepath = self.filename
            self.events = []
            infile = open(filepath, "r")
            lines = [] # all the lines between previous and next "#" markings
            for line in infile:
                if line[0] == '#':
                    # gets event settings based on above format 
                    b = int(lines[0])
                    end = int(lines[1])
                    start = int(lines[2])
                    step = int(lines[3])
                    name = lines[4]
                    desc = "\n".join(lines[5:])
                    self.events.append([end, start, step, name, desc, b])
                    lines = [] # reset lines 
                else:
                    # add line to lines list. 
                    lines.append(line[1:-1])
            infile.close()
    
        def save_events(self, filepath=None):
            if filepath is None:
                filepath = self.filename
            s = ""
            for event in self.events:
                # Saves each event based on the above format 
                s += f"-{int(event[5])}\n"
                s += f"-{int(event[0])}\n"
                s += f"-{int(event[1])}\n"
                s += f"-{int(event[2])}\n"
                s += f"-{event[3]}\n"
                s += f"-{event[4]}\n"
                s += "#\n"
            outfile = open(filepath, "w")
            outfile.write(s)
            outfile.close()
    
        def search(self, time):
            # Basic Binary Search
            low = -1
            high = len(self.events) - 1
            while low < high:
                mid = low + (high - low + 1) // 2
                if self.events[mid][0] <= time:
                    low = mid
                else:
                    high = mid - 1
            return low + 1
    
        def getDay(self, date):
            # Follows the above Instructions to get all the events that happen on that day. 
            events = []
            for i in range(len(self.events) - 1, -1, -1):
                event = self.events[i]
                if event[0] < date:
                    break
                k = (date - event[1] - 1) // event[2] + 1
                t = event[1] + k * event[2] - date
                if k >= 0 and t < 86400:
                    events.append((t, i))
            return events
    
        def getEvent(self, idx):
            return self.events[idx]
    Pygame Input Handler
    To handle updating and querying inputs from the mouse and keyboard. (See parts Four to Five)
    import pygame
    
    class InputHandler:
        # Get Modes: "down", "press", and "release" 
        def __init__(self):
            self.keyDown = pygame.key.get_pressed()
            self.prevKeyDown = pygame.key.get_pressed()
            self.mouseDown = pygame.mouse.get_pressed(3)
            self.prevMouseDown = pygame.mouse.get_pressed(3)
            self.mousePos = pygame.mouse.get_pos()
    
        def update(self):
            # Set Previous Mouse and Key Downs 
            self.prevKeyDown = self.keyDown
            self.prevMouseDown = self.mouseDown
            # Set Current Mouse and Key Downs 
            self.keyDown = pygame.key.get_pressed()
            self.mouseDown = pygame.mouse.get_pressed(3)
            self.mousePos = pygame.mouse.get_pos()
    
        def getKey(self, key, mode="down"):
            match mode:
                case "down": # Is Key Down 
                    return self.keyDown[key]
                case "press": # Was Key Pressed this frame 
                    return self.keyDown[key] and not self.prevKeyDown[key]
                case "release": Was Key Released this frame 
                    return self.prevKeyDown[key] and not self.keyDown[key]
                case _:
                    print(f"Unknown Mode {mode}")
                    return False
    
        def getMouse(self, mouse, mode="down"):
            match mode:
                case "down": # Is Mouse Button Down 
                    return self.mouseDown[mouse]
                case "press": # Was Mouse Button Pressed this frame 
                    return self.mouseDown[mouse] and not self.prevMouseDown[mouse]
                case "release": # Was Mouse Button Release this frame 
                    return self.prevMouseDown[mouse] and not self.mouseDown[mouse]
                case _:
                    print(f"Unknown Mode {mode}")
                    return False
    
    Primary Calendar Class
    To Add and Remove events. (See parts Six to Seven)
    import datetime
    import time
    import pygame
    from CalendarLoader import *
    
    utc = datetime.timezone.utc
    
    class Calendar:
        def __init__(self, filename):
            self.loader = CalendarLoader(filename)
            self.loader.load_events()
            # Floor "now" to 00:00 on the same day 
            now = (time.time() // 86400) * 86400
            self.line = 20
            self.date = now
            self.start = now
            self.end = now
            self.step = 86400
            self.addIdx = 0
            self.selected = 0
    
        def set_filename(self, filename):
            self.loader.set_file(filename)
    
        def render(self, font, inHand, offset):
            # Get all the events on the current date 
            events = sorted(self.loader.getDay(self.date))
            relx, rely = inHand.mousePos[0] - offset[0], inHand.mousePos[1] - offset[1]
            idx = -1
            if 20 < relx < 250 and 5 + 1.5 * self.line < rely:
                idx = int((rely - 5) / self.line - 1.5)
            if idx >= len(events):
                idx = -1
            if inHand.getMouse(0, "press") and idx != -1:
                self.selected = idx # Check if any events where clicked 
            if inHand.getKey(pygame.K_UP, "press"):
                self.addIdx = (self.addIdx - 1 + 3) % 3 # decrease add index 
            if inHand.getKey(pygame.K_DOWN, "press"):
                self.addIdx = (self.addIdx + 1) % 3 # increase add index 
    
            # Calendar Window 
            surf = pygame.Surface((600, 400))
            surf.fill((0, 255, 255))
            surf.blit(font.render(datetime.datetime.fromtimestamp(self.date, utc).strftime("%A %B %d, %Y"), True, (0, 0, 0)), (20, 5))
            for i, (t, eventIdx) in enumerate(events):
                event = self.loader.getEvent(eventIdx)
                timing = datetime.datetime.fromtimestamp(t, utc).strftime("%I:%M %p")
                colour = ((0, 0, 0) if event[5] != 1 else (0, 180, 0)) if i != self.selected else (0, 0, 127)
                status = "[]" if event[5] == 1 else "X"
                surf.blit(font.render(f"{status} {timing}: {event[3]}", True, colour), (20, 5 + (i + 1.5) * self.line))
                if i == self.selected:
                    surf.blit(font.render(datetime.datetime.fromtimestamp(event[1], utc).strftime("Start: %A %B %d, %Y - %H"), True, (0, 0, 0)), (270, 5 + 9 * self.line))
                    surf.blit(font.render(datetime.datetime.fromtimestamp(event[0], utc).strftime("End: %A %B %d, %Y - %H"), True, (0, 0, 0)), (270, 5 + 10 * self.line))
                    surf.blit(font.render(datetime.datetime.fromtimestamp(event[2] - 86400, utc).strftime("Step: %d days; %H hours"), True, (0, 0, 0)), (270, 5 + 11 * self.line))
                    surf.blit(font.render("=== Description ===", True, (0, 0, 0)), (270, 5 + 12 * self.line))
                    surf.blit(font.render(f"{event[4]}", True, (0, 0, 0)), (270, 5 + 13 * self.line))
            if inHand.getKey(pygame.K_f, "press") and 0 <= self.selected < len(events):
                self.loader.events[events[self.selected][1]][5] = 1 - self.loader.events[events[self.selected][1]][5]
            colours = [(0, 0, 0), (0, 0, 0), (0, 0, 0)]
            colours[self.addIdx] = (0, 0, 127)
            surf.blit(font.render(datetime.datetime.fromtimestamp(self.start, utc).strftime("Start: %A %B %d, %Y - %H"), True, colours[0]), (270, 5))
            surf.blit(font.render(datetime.datetime.fromtimestamp(self.end, utc).strftime("End: %A %B %d, %Y - %H"), True, colours[1]), (270, 5 + self.line))
            surf.blit(font.render(datetime.datetime.fromtimestamp(self.step - 86400, utc).strftime("Step: %d days; %H hours"), True, colours[2]), (270, 5 + 2 * self.line))
            surf.blit(font.render(("START", "END", "STEP")[self.addIdx], True, (0, 127, 0)), (300, 5 + 4 * self.line))
            clickIdx = (-1, -1)
            for t in range(2):
                for i in range((4, 4, 2)[self.addIdx]):
                    bx = 370 + i * 25
                    by = 5 + 4 * self.line + t * 25
                    pygame.draw.rect(surf, (0, 0, 0), (bx - 10, by - 10, 20, 20), 2, 4)
                    symbol = "+" if t == 0 else "-"
                    surf.blit(font.render(symbol, True, (0, 0, 0)), (bx - 3, by - 13))
                    if abs(relx - bx) < 10 and abs(rely - by) < 10:
                        clickIdx = (i, t)
            if inHand.getMouse(0, "press") and clickIdx[0] != -1:
                m = (1, -1)[clickIdx[1]]
                i = clickIdx[0]
                match self.addIdx:
                    case 0:
                        if i == 1:
                            self.start += m * 86400
                        else:
                            dt = datetime.datetime.fromtimestamp(self.start, utc)
                            para = [dt.month, dt.day, dt.year, dt.hour]
                            if i == 0:
                                para[i] = (para[i] + m + 11) % 12 + 1
                            elif i == 3:
                                para[i] = (para[i] + m + 24) % 24
                            else:
                                para[i] += m
                            self.start = datetime.datetime(para[2], para[0], para[1], para[3], tzinfo=utc).timestamp()
                    case 1:
                        if i == 1:
                            self.end += m * 86400
                        else:
                            dt = datetime.datetime.fromtimestamp(self.end, utc)
                            para = [dt.month, dt.day, dt.year, dt.hour]
                            if i == 0:
                                para[i] = (para[i] + m + 11) % 12 + 1
                            elif i == 3:
                                para[i] = (para[i] + m + 24) % 24
                            else:
                                para[i] += m
                            self.end = datetime.datetime(para[2], para[0], para[1], para[3], tzinfo=utc).timestamp()
                    case 2:
                        self.step += m * (86400, 3600)[i]
                self.start = max(self.start, 0)
                self.step = max(self.step, 86400)
                self.end = max(self.end, 0)
    
            clickIdx = -1
            surf.blit(font.render("Events: Add    Remove    Save", True, (0, 0, 0)), (270, 7 * self.line - 8))
            for t in range(3):
                bx = 340 + t * 55
                by = 5 + 8 * self.line
                pygame.draw.rect(surf, (0, 0, 0), (bx - 10, by - 10, 20, 20), 2, 4)
                symbol = "+-S"[t]
                o = 2 if t == 2 else 0
                surf.blit(font.render(symbol, True, (0, 0, 0)), (bx - 3 - o, by - 13 + o))
                if abs(relx - bx) < 10 and abs(rely - by) < 10:
                    clickIdx = t
            if inHand.getMouse(0, "press") and clickIdx != -1:
                if clickIdx == 0:
                    print("Name Of Event: ")
                    name = input()
                    desc = ""
                    print("Description: ")
                    while True:
                        add = input()
                        if add == "":
                            break
                        desc += add + '\n'
                    index = self.loader.search(time.time() - 2073600)
                    self.loader.events = self.loader.events[index:]
                    self.loader.events.insert(self.loader.search(self.end), [self.end, self.start, self.step, name, desc[:-1], 0])
                elif clickIdx == 1:
                    if 0 <= self.selected < len(events):
                        self.loader.events.pop(events[self.selected][1])
                elif clickIdx == 2:
                    self.loader.save_events()
    
            return surf
    
    Pygame Main Loop
    To integrate all parts.
    import pygame
    from InputHandler import *
    from Calendar import *
    
    # Basic Pygame Setup
    pygame.init()
    pygame.font.init()
    high_score = 0
    score = 0
    fps = 60
    fpsClock = pygame.time.Clock()
    
    width, height = 840, 480
    screen = pygame.display.set_mode((width, height))
    pygame.display.set_caption("Calendar App")
    
    font = pygame.font.SysFont("comicsans", 25)
    inHand = InputHandler()
    calendar = Calendar("event.txt")
    
    # print(calendar.loader.events)
    
    # Main Game Loop
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                running = False
    
        if not running:
            break
        inHand.update()
    
        # White Background 
        screen.fill((255, 255, 255))
    
        # Handles Click Detection for the Calendar Date Controller 
        clickIdx = (-1, -1)
        screen.blit(font.render("Calendar Date Controller", True, (0, 0, 0)), (630, 18))
        for i in range(3):
            by = 55 + i * 25
            label = ("Year", "Month", "Day")[i]
            screen.blit(font.render(f"{label}: ", True, (0, 0, 0)), (630, by - 12))
            for t in range(2):
                bx = 700 + t * 25
                pygame.draw.rect(screen, (0, 0, 0), (bx - 10, by - 10, 20, 20), 2, 4)
                symbol = "+" if t == 0 else "-"
                screen.blit(font.render(symbol, True, (0, 0, 0)), (bx - 3, by - 13))
                if abs(inHand.mousePos[0] - bx) < 10 and abs(inHand.mousePos[1] - by) < 10:
                    clickIdx = (i, t)
        if inHand.getMouse(0, "press") and clickIdx[0] != -1:
            m = (1, -1)[clickIdx[1]]
            i = clickIdx[0]
            if i == 2:
                calendar.date += m * 86400
            else:
                dt = datetime.datetime.fromtimestamp(calendar.date, utc)
                para = [dt.year, dt.month, dt.day]
                if i == 1:
                    para[i] = (para[i] + m + 11) % 12 + 1
                else:
                    para[i] += m
                calendar.date = datetime.datetime(para[0], para[1], para[2], tzinfo=utc).timestamp()
            calendar.date = max(calendar.date, 0)
    
        screen.blit(calendar.render(font, inHand, (20, 20)), (20, 20))
    
        pygame.display.flip()
        fpsClock.tick(fps)
    
    # Closed the game
    print("App Closed Successfully")
    
    Single-File Full Code
    import time
    import pygame
    import datetime
    
    # ============ FILE LOADER ============ 
    
    class CalendarLoader:
        def __init__(self, filename):
            self.events = []
            self.filename = filename
    
        def set_file(self, filename):
            self.filename = filename
    
        def load_events(self, filepath=None):
            if filepath is None:
                filepath = self.filename
            self.events = []
            infile = open(filepath, "r")
            lines = []
            for line in infile:
                if line[0] == '#':
                    b = int(lines[0])
                    end = int(lines[1])
                    start = int(lines[2])
                    step = int(lines[3])
                    name = lines[4]
                    desc = "\n".join(lines[5:])
                    self.events.append([end, start, step, name, desc, b])
                    lines = []
                else:
                    lines.append(line[1:-1])
            infile.close()
    
        def save_events(self, filepath=None):
            if filepath is None:
                filepath = self.filename
            s = ""
            for event in self.events:
                s += f"-{int(event[5])}\n"
                s += f"-{int(event[0])}\n"
                s += f"-{int(event[1])}\n"
                s += f"-{int(event[2])}\n"
                s += f"-{event[3]}\n"
                s += f"-{event[4]}\n"
                s += "#\n"
            outfile = open(filepath, "w")
            outfile.write(s)
            outfile.close()
    
        def search(self, time):
            # Basic Binary Search
            low = -1
            high = len(self.events) - 1
            while low < high:
                mid = low + (high - low + 1) // 2
                if self.events[mid][0] <= time:
                    low = mid
                else:
                    high = mid - 1
            return low + 1
    
        def getDay(self, date):
            # Follows the above Instructions to get all the events that happen on that day. 
            events = []
            for i in range(len(self.events) - 1, -1, -1):
                event = self.events[i]
                if event[0] < date:
                    break
                k = (date - event[1] - 1) // event[2] + 1
                t = event[1] + k * event[2] - date
                if k >= 0 and t < 86400:
                    events.append((t, i))
            return events
    
        def getEvent(self, idx):
            return self.events[idx]
    
    # ============ PYGAME INPUT HANDLER ============ 
    
    class InputHandler:
        def __init__(self):
            self.keyDown = pygame.key.get_pressed()
            self.prevKeyDown = pygame.key.get_pressed()
            self.mouseDown = pygame.mouse.get_pressed(3)
            self.prevMouseDown = pygame.mouse.get_pressed(3)
            self.mousePos = pygame.mouse.get_pos()
    
        def update(self):
            self.prevKeyDown = self.keyDown
            self.prevMouseDown = self.mouseDown
            self.keyDown = pygame.key.get_pressed()
            self.mouseDown = pygame.mouse.get_pressed(3)
            self.mousePos = pygame.mouse.get_pos()
    
        def getKey(self, key, mode="down"):
            match mode:
                case "down":
                    return self.keyDown[key]
                case "press":
                    return self.keyDown[key] and not self.prevKeyDown[key]
                case "release":
                    return self.prevKeyDown[key] and not self.keyDown[key]
                case _:
                    print(f"Unknown Mode {mode}")
                    return False
    
        def getMouse(self, mouse, mode="down"):
            match mode:
                case "down":
                    return self.mouseDown[mouse]
                case "press":
                    return self.mouseDown[mouse] and not self.prevMouseDown[mouse]
                case "release":
                    return self.prevMouseDown[mouse] and not self.mouseDown[mouse]
                case _:
                    print(f"Unknown Mode {mode}")
                    return False
    
    # ============ CALENDAR CLASS ============ 
    
    utc = datetime.timezone.utc
    
    class Calendar:
        def __init__(self, filename):
            self.loader = CalendarLoader(filename)
            self.loader.load_events()
            now = (time.time() // 86400) * 86400
            self.line = 20
            self.date = now
            self.start = now
            self.end = now
            self.step = 86400
            self.addIdx = 0
            self.selected = 0
    
        def set_filename(self, filename):
            self.loader.set_file(filename)
    
        def render(self, font, inHand, offset):
            events = sorted(self.loader.getDay(self.date))
            relx, rely = inHand.mousePos[0] - offset[0], inHand.mousePos[1] - offset[1]
            idx = -1
            if 20 < relx < 250 and 5 + 1.5 * self.line < rely:
                idx = int((rely - 5) / self.line - 1.5)
            if idx >= len(events):
                idx = -1
            if inHand.getMouse(0, "press") and idx != -1:
                self.selected = idx
            if inHand.getKey(pygame.K_UP, "press"):
                self.addIdx = (self.addIdx - 1 + 3) % 3
            if inHand.getKey(pygame.K_DOWN, "press"):
                self.addIdx = (self.addIdx + 1) % 3
    
            surf = pygame.Surface((600, 400))
            surf.fill((0, 255, 255))
            surf.blit(font.render(datetime.datetime.fromtimestamp(self.date, utc).strftime("%A %B %d, %Y"), True, (0, 0, 0)), (20, 5))
            for i, (t, eventIdx) in enumerate(events):
                event = self.loader.getEvent(eventIdx)
                timing = datetime.datetime.fromtimestamp(t, utc).strftime("%I:%M %p")
                colour = ((0, 0, 0) if event[5] != 1 else (0, 180, 0)) if i != self.selected else (0, 0, 127)
                status = "[]" if event[5] == 1 else "X"
                surf.blit(font.render(f"{status} {timing}: {event[3]}", True, colour), (20, 5 + (i + 1.5) * self.line))
                if i == self.selected:
                    surf.blit(font.render(datetime.datetime.fromtimestamp(event[1], utc).strftime("Start: %A %B %d, %Y - %H"), True, (0, 0, 0)), (270, 5 + 9 * self.line))
                    surf.blit(font.render(datetime.datetime.fromtimestamp(event[0], utc).strftime("End: %A %B %d, %Y - %H"), True, (0, 0, 0)), (270, 5 + 10 * self.line))
                    surf.blit(font.render(datetime.datetime.fromtimestamp(event[2] - 86400, utc).strftime("Step: %d days; %H hours"), True, (0, 0, 0)), (270, 5 + 11 * self.line))
                    surf.blit(font.render("=== Description ===", True, (0, 0, 0)), (270, 5 + 12 * self.line))
                    surf.blit(font.render(f"{event[4]}", True, (0, 0, 0)), (270, 5 + 13 * self.line))
            if inHand.getKey(pygame.K_f, "press"):
                self.loader.events[events[self.selected][1]][5] = 1 - self.loader.events[events[self.selected][1]][5]
            colours = [(0, 0, 0), (0, 0, 0), (0, 0, 0)]
            colours[self.addIdx] = (0, 0, 127)
            surf.blit(font.render(datetime.datetime.fromtimestamp(self.start, utc).strftime("Start: %A %B %d, %Y - %H"), True, colours[0]), (270, 5))
            surf.blit(font.render(datetime.datetime.fromtimestamp(self.end, utc).strftime("End: %A %B %d, %Y - %H"), True, colours[1]), (270, 5 + self.line))
            surf.blit(font.render(datetime.datetime.fromtimestamp(self.step - 86400, utc).strftime("Step: %d days; %H hours"), True, colours[2]), (270, 5 + 2 * self.line))
            surf.blit(font.render(("START", "END", "STEP")[self.addIdx], True, (0, 127, 0)), (300, 5 + 4 * self.line))
            clickIdx = (-1, -1)
            for t in range(2):
                for i in range((4, 4, 2)[self.addIdx]):
                    bx = 370 + i * 25
                    by = 5 + 4 * self.line + t * 25
                    pygame.draw.rect(surf, (0, 0, 0), (bx - 10, by - 10, 20, 20), 2, 4)
                    symbol = "+" if t == 0 else "-"
                    surf.blit(font.render(symbol, True, (0, 0, 0)), (bx - 3, by - 13))
                    if abs(relx - bx) < 10 and abs(rely - by) < 10:
                        clickIdx = (i, t)
            if inHand.getMouse(0, "press") and clickIdx[0] != -1:
                m = (1, -1)[clickIdx[1]]
                i = clickIdx[0]
                match self.addIdx:
                    case 0:
                        if i == 1:
                            self.start += m * 86400
                        else:
                            dt = datetime.datetime.fromtimestamp(self.start, utc)
                            para = [dt.month, dt.day, dt.year, dt.hour]
                            if i == 0:
                                para[i] = (para[i] + m + 11) % 12 + 1
                            elif i == 3:
                                para[i] = (para[i] + m + 24) % 24
                            else:
                                para[i] += m
                            self.start = datetime.datetime(para[2], para[0], para[1], para[3], tzinfo=utc).timestamp()
                    case 1:
                        if i == 1:
                            self.end += m * 86400
                        else:
                            dt = datetime.datetime.fromtimestamp(self.end, utc)
                            para = [dt.month, dt.day, dt.year, dt.hour]
                            if i == 0:
                                para[i] = (para[i] + m + 11) % 12 + 1
                            elif i == 3:
                                para[i] = (para[i] + m + 24) % 24
                            else:
                                para[i] += m
                            self.end = datetime.datetime(para[2], para[0], para[1], para[3], tzinfo=utc).timestamp()
                    case 2:
                        self.step += m * (86400, 3600)[i]
                self.start = max(self.start, 0)
                self.step = max(self.step, 86400)
                self.end = max(self.end, 0)
    
            clickIdx = -1
            surf.blit(font.render("Events: Add    Remove    Save", True, (0, 0, 0)), (270, 7 * self.line - 8))
            for t in range(3):
                bx = 340 + t * 55
                by = 5 + 8 * self.line
                pygame.draw.rect(surf, (0, 0, 0), (bx - 10, by - 10, 20, 20), 2, 4)
                symbol = "+-S"[t]
                o = 2 if t == 2 else 0
                surf.blit(font.render(symbol, True, (0, 0, 0)), (bx - 3 - o, by - 13 + o))
                if abs(relx - bx) < 10 and abs(rely - by) < 10:
                    clickIdx = t
            if inHand.getMouse(0, "press") and clickIdx != -1:
                if clickIdx == 0:
                    print("Name Of Event: ")
                    name = input()
                    desc = ""
                    print("Description: ")
                    while True:
                        add = input()
                        if add == "":
                            break
                        desc += add + '\n'
                    index = self.loader.search(time.time() - 2073600)
                    self.loader.events = self.loader.events[index:]
                    self.loader.events.insert(self.loader.search(self.end), [self.end, self.start, self.step, name, desc[:-1], 0])
                elif clickIdx == 1:
                    if 0 <= self.selected < len(events):
                        self.loader.events.pop(events[self.selected][1])
                elif clickIdx == 2:
                    self.loader.save_events()
    
            return surf
    
    # ============ PRIMARY PYGAME LOOP ============ 
    
    pygame.init()
    pygame.font.init()
    high_score = 0
    score = 0
    fps = 60
    fpsClock = pygame.time.Clock()
    
    width, height = 840, 480
    screen = pygame.display.set_mode((width, height))
    pygame.display.set_caption("Calendar App")
    
    font = pygame.font.SysFont("comicsans", 25)
    inHand = InputHandler()
    calendar = Calendar("event.txt")
    
    # print(calendar.loader.events)
    
    # Main Game Loop
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                running = False
    
        if not running:
            break
        inHand.update()
    
        screen.fill((255, 255, 255))
    
        clickIdx = (-1, -1)
        screen.blit(font.render("Calendar Date Controller", True, (0, 0, 0)), (630, 18))
        for i in range(3):
            by = 55 + i * 25
            label = ("Year", "Month", "Day")[i]
            screen.blit(font.render(f"{label}: ", True, (0, 0, 0)), (630, by - 12))
            for t in range(2):
                bx = 700 + t * 25
                pygame.draw.rect(screen, (0, 0, 0), (bx - 10, by - 10, 20, 20), 2, 4)
                symbol = "+" if t == 0 else "-"
                screen.blit(font.render(symbol, True, (0, 0, 0)), (bx - 3, by - 13))
                if abs(inHand.mousePos[0] - bx) < 10 and abs(inHand.mousePos[1] - by) < 10:
                    clickIdx = (i, t)
        if inHand.getMouse(0, "press") and clickIdx[0] != -1:
            m = (1, -1)[clickIdx[1]]
            i = clickIdx[0]
            if i == 2:
                calendar.date += m * 86400
            else:
                dt = datetime.datetime.fromtimestamp(calendar.date, utc)
                para = [dt.year, dt.month, dt.day]
                if i == 1:
                    para[i] = (para[i] + m + 11) % 12 + 1
                else:
                    para[i] += m
                calendar.date = datetime.datetime(para[0], para[1], para[2], tzinfo=utc).timestamp()
            calendar.date = max(calendar.date, 0)
    
        screen.blit(calendar.render(font, inHand, (20, 20)), (20, 20))
    
        pygame.display.flip()
        fpsClock.tick(fps)
    
    # Closed the game
    print("App Closed Successfully")
    

    How to use

    How To Run
    1. Open trinket.io/pygame
    2. Paste code from “Single File Full Code” into editor
    3. Create new file called “event.txt”
    4. Run the code
    App Manual

    Add Event

    1. Set Start, End, and Step (Using Up/Down Arrow Keys and on-screen [+][-] buttons)
    2. Use Add [+] Button
    3. Fill in the name and description in python console.

    Remove Event

    1. Click on Event from the left view.
    2. Use Remove [-] Button

    Change View Date

    1. Use the right “Calendar Date Controller” to increment or decrement the months, year, or day of viewing.

    Save Changes

    1. Use Save [S] Button

    View More Details

    1. Click on Event from the left view.
    2. A more detailed description of the event will appear in the middle right of the cyan part.

    Mark/Unmark Event

    1. Click on Event from the left view.
    2. Press “f” to toggle Finished/Unfinished
    Demo

    https://drive.google.com/file/d/188g953LIu57BodflmnT3jkoBiPxKbs2b/view

    AI usage

    I did not use AI for this assignment.