Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ctf #1081

Merged
merged 20 commits into from
Sep 25, 2023
Merged

ctf #1081

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
283 changes: 283 additions & 0 deletions backend/server/routers/ctf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
"""
Implementation for the 2023 CTF.


Scenario:
In the whimsical world of the CTF (Capture The Flag), an extraordinary
challenge awaits our protagonist, Ollie the Otter. Ollie, a brilliant otter
with a passion for education, has embarked on an academic journey like no
other. But there's a twist – Ollie's unique otter nature has led to some
special conditions on his degree planning adventure.

Ollie's task is to navigate through a degree planner system called
"Circles," specially designed to accommodate his otter-specific
requirements. In this intriguing scenario, you must help Ollie, the otter
scholar, chart his academic path using Circles.

Ollie is planning his 3 year Computer Science (3778) degree starting in 2024
and wants to take a Computer Science major and Mathematics minor. Help him set
up his degree in the degree wizard!

When you are done, press the `Validate CTF` button on the term planner page
to recieve your flags.

# Stage 1. The Otter's Academic Odyssey: Charting Uncharted Waters
Being the pioneer Otter in academia, Ollie faces some unique conditions
that must be fulfilled for him to successfully navigate his degree. Let's
tackle parts 0-3 to obtain the first flag.

# Stage 2. The Kafka Quandary: Or How I Learned To Stop Worrying and Love The
# Handbook
Ollie encounters additional hurdles due to university policies, with
challenges 4 through 7 directly related to these policy-driven obstacles.
Join Ollie in overcoming these challenges and guiding him through this
academic maze

# Stage 3: (HARD) Numbers, Notions, and the Cult of Calculus: An O-Week Odyssey
During Orientation Week (O-week), Ollie had a chance encounter with a group
of math students. Little did he know that this encounter would lead to him
being initiated into a number-centric cult. Over time, Ollie developed
superstitions about certain numbers, and the ultimate challenges he faces
revolve around aligning his degree plan with his newfound beliefs. With the
last challenges, Ollie on his quest to complete his degree while also
seeking the approval of the enigmatic number cult
"""
from typing import Callable, Optional

from fastapi import APIRouter
from server.routers.model import PlannerData

router = APIRouter(
prefix="/ctf", tags=["ctf"], responses={404: {"description": "Not found"}}
)

def all_courses(data: PlannerData) -> set[str]:
"""
Returns all courses from a planner
"""
return {
course
for year in data.plan
for term in year
for course in term
}


def get_code(course: str) -> int:
"""
Returns the code of a courseCode
EG: COMP1511 -> 1511
"""
return int(course[4:])


def get_faculty(course: str) -> str:
"""
Returns the faculty of a courseCode
EG: COMP1511 -> COMP
"""
return course[:4]


def gen_eds(courses: set[str]) -> set[str]:
"""
Returns all gen eds from a set of courses
"""
return set(
course
for course in courses
if not course.startswith("COMP") and not course.startswith("MATH")
)


def hard_requirements(data: PlannerData) -> bool:
# NOTE: Can't check start year from this
# Frontend should handle most of this anyways
# including validity of the program
return (
data.program == "3778"
and "COMPA1" in data.specialisations
and "MATHC2" in data.specialisations
and len(data.plan) == 3
)


def extended_courses(data: PlannerData) -> bool:
"""
Must take atleast 3 courses with extended in the name
"""
extended_courses = {
"COMP3821",
"COMP3891",
"COMP6841",
"COMP6843"
"COMP6845"
}
return len(extended_courses & all_courses(data)) >= 3


def summer_course(data: PlannerData) -> bool:
"""
Must take atleast one summer course
"""
return any(
course.startswith("COMP")
for year in data.plan
for course in year[0]
)



def term_sums_even(data: PlannerData) -> bool:
"""
Check that the sum of the course codes in even terms is even
"""
is_even: Callable[[int], bool] = lambda x: x % 2 == 0
print("Checking even")
for y, year in enumerate(data.plan):
# Exclude summer term + odd terms
for i, term in enumerate(year[2::2], 2):
term_sum = sum(map(get_code, term.keys()))
print(f"{y}T{i} sum: {term_sum}")
if not is_even(term_sum):
print("failed: ", term)
return False

return True

# TODO
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

def term_sums_odd(data: PlannerData) -> bool:
"""
Check that the sum of the course codes in odd terms is odd
"""
is_odd: Callable[[int], bool] = lambda x: x % 2 == 1
print("Checking odd")
for year in data.plan[::2]:
# Exclude summer term + even terms
for term in year[1::2]:
term_sum = sum(map(get_code, term.keys()))
if not is_odd(term_sum):
print("failed: ", term)
return False
return True

def comp1511_marks(data: PlannerData) -> bool:
"""
Ollie must achieve a mark of 100 in COMP1511 to keep his scholarship
"""
for year in data.plan:
for term in year:
for course in term:
_, marks = term[course] # type: ignore
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debugging stuff

if course == "COMP1511":
return marks == 100

return False
Comment on lines +168 to +175
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on turning this into a generator comprehension with next?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, this can be triv-solved with

return any(
  course
  for term in year
  for course, mark in term
  if course == "COMP1511" and mark == 100
)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true don't even need next, but what about simplifying this even further and doing

return any(
  course == "COMP1511" and mark == 100
  for term in year
  for course, mark in term
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THIS is incredibel +++EV!!!!!!



def gen_ed_sum(data: PlannerData) -> bool:
"""
The sum of GENED course codes must not exceed 2200
"""
return sum(map(get_code, gen_eds(all_courses(data)))) <= 2200


def gen_ed_faculty(data: PlannerData) -> bool:
"""
Gen-Eds must all be from different faculties
"""
gen_eds_facs = list(map(get_faculty, gen_eds(all_courses(data))))
return len(gen_eds_facs) == len(set(gen_eds_facs))


def same_code_diff_faculty(data: PlannerData) -> bool:
"""
Must take two courses with the same code but, from different faculties
"""
codes = list(map(get_code, all_courses(data)))
# Can't have duplicate of a course since it's a set
return len(codes) != len(set(codes))


def math_limit(data: PlannerData) -> bool:
"""
In your N-th year, you can only take N + 1 math courses
"""
for i, year in enumerate(data.plan, 1):
num_math = len([
course
for term in year
for course in term
if course.startswith("MATH")
])
Comment on lines +207 to +212
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on using sum with a generator comprehension where every item is 1 instead of course so we don't need to build an entire list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actly not bad idea

if num_math > i + 1:
return False

return True

def six_threes_limit(data: PlannerData) -> bool:
"""
There can by at most 6 occurrences of the number 3 in the entire
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ummmmmmmmm by should be be

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong

planner
"""
all_codes = "".join(str(get_code(course)) for course in all_courses(data))
return all_codes.count("3") <= 6

def comp1531_third_year(data: PlannerData) -> bool:
"""
COMP1531 must be taken in the third year
"""
third_year = data.plan[2]
for term in third_year:
for course in term:
if course == "COMP1531":
return True

return False

# (validator_func, message, Optional<flag>)
requirements: list[tuple[Callable[[PlannerData], bool], str, Optional[str]]] = [
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thoughts on extracting the validator_func type to make this more readable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its already readable, skill diff

# Challenge 1
(hard_requirements, "Before you can submit, you must check that you are in a 3 year CS degree and have a math minor", None),
(summer_course, "Ollie must take one summer COMP course.", None),
(comp1511_marks, "To keep their scholarship, Ollie must achieve a mark of 100 in COMP1511.", None),
(extended_courses, "Ollie must complete at least THREE COMP courses with extended in the name that have not been discontinued.", "levelup{mVd3_1t_2_un1}"),
# Challenge 2
(comp1531_third_year, "Unable to find a partner earlier, Ollie must take COMP1531 in their third year.", None),
(gen_ed_faculty, "The university has decided that General Education must be very general. As such, each Gen-Ed unit that Ollie takes must be from a different faculty.", None),
(math_limit, "The university has become a big believer in spaced repetition and want to prevent students from cramming subjects for their minors. Now, in their N-th year, Ollie can only take N + 1 math courses.", None),
(gen_ed_sum, "Course codes now reflect the difficulty of a course. To avoid extremely stressful terms, the sum of Olli's Gen-Ed course codes must not exceed 2200.", "levelup{i<3TryMesters}"),
# Challenge 3
(same_code_diff_faculty, "You must take two courses from different faculties that have the same course code.", None),
(term_sums_even, "You must ensure that the sum of your course codes in even terms is even. Note that summer courses do not count towards this.", None),
(term_sums_odd, "You must ensure that the sum of your course codes in odd terms is odd. Note that summer courses do not count towards this.", None),
(six_threes_limit, "In all your course codes, there can be at most 6 occurrences of the number 3", "levelup{CU1Tur3d}"),
]

@router.post("/validateCtf/")
def validate_ctf(data : PlannerData):
"""
Validates the CTF
"""
passed: list[str] = []
flags: list[str] = []
for req_num, (fn, msg, flag) in enumerate(requirements):
if not fn(data):
return {
"valid": False,
"passed": passed,
"failed": req_num,
"flags": flags,
"message": msg
}
passed.append(msg)
if flag is not None:
flags.append(flag)
print("Ok: ", req_num)
return {
"valid": True,
"failed": -1,
"passed": passed,
"flags": flags,
"message": "Congratulations! You have passed all the requirements for the CTF."
}
3 changes: 2 additions & 1 deletion backend/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from fastapi.middleware.cors import CORSMiddleware
from data.config import LIVE_YEAR

from server.routers import courses, planner, programs, specialisations, followups
from server.routers import courses, planner, programs, specialisations, followups, ctf

app = FastAPI()

Expand Down Expand Up @@ -45,6 +45,7 @@
app.include_router(programs.router)
app.include_router(specialisations.router)
app.include_router(followups.router)
app.include_router(ctf.router)


@app.get("/")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import HelpMenu from '../HelpMenu/HelpMenu';
import ImportPlannerMenu from '../ImportPlannerMenu';
import SettingsMenu from '../SettingsMenu';
import { isPlannerEmpty } from '../utils';
import ValidateCtfButton from '../ValidateCtfButton/ValidateCtfButton';
import S from './styles';
// Used for tippy stylings
import 'tippy.js/dist/tippy.css';
Expand Down Expand Up @@ -94,6 +95,11 @@ const OptionsHeader = ({ plannerRef }: Props) => {
</Tooltip>
</div>
</Tippy>
<Tippy>
<div>
<ValidateCtfButton />
</div>
</Tippy>

{!isPlannerEmpty(years) && (
<Tooltip title="Unplan all courses">
Expand Down
Loading
Loading