initial commit
This commit is contained in:
97
riddle_server/app/models.py
Normal file
97
riddle_server/app/models.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from banjo.models import Model, StringField, IntegerField
|
||||
from fuzzywuzzy import fuzz
|
||||
|
||||
class Riddle(Model):
|
||||
question = StringField()
|
||||
answer = StringField()
|
||||
guesses = IntegerField()
|
||||
correct = IntegerField()
|
||||
|
||||
MIN_FUZZ_RATIO = 80
|
||||
|
||||
def __repr__(self):
|
||||
"""Declares how to represent a Riddle as a string.
|
||||
A riddle's string will look something like this:
|
||||
<Riddle 12: Where can you get dragon milk? (3/15)>
|
||||
"""
|
||||
return "<Riddle {}: {} ({}/{})>".format(
|
||||
self.id or '(unsaved)',
|
||||
self.question,
|
||||
self.correct,
|
||||
self.guesses
|
||||
)
|
||||
|
||||
def is_valid(self):
|
||||
"Checks whether this riddle is valid. In other words, when validate() finds no errors."
|
||||
return len(self.validate()) == 0
|
||||
|
||||
def validate(self):
|
||||
"Checks whether this riddle can be saved"
|
||||
errors = []
|
||||
if self.question is None:
|
||||
errors.append("question is required")
|
||||
if self.answer is None:
|
||||
errors.append("answer is required")
|
||||
return errors
|
||||
|
||||
def difficulty(self):
|
||||
"""Calculates and returns the riddle's difficulty.
|
||||
|
||||
The difficulty is basically 1 minus the fraction of guesses which were correct.
|
||||
So a Riddle with a difficulty of 1 is impossibly hard, while a Riddle with a difficulty
|
||||
of 0 is easy--everyone gets it right!
|
||||
|
||||
There is an interesting detail here though. Instead of 1 - correct/guesses, we add 1 to
|
||||
correct and we add 1 to guesses. This is called "smoothing" and it provides two benefits:
|
||||
First, we avoid having an undefined difficulty when there have been no guesses (0/0 would
|
||||
raise a ZeroDivisionError) Second, it gives better values for difficulty when there have been no
|
||||
correct guesses or no incorrect guesses. Consider an impossible riddle:
|
||||
|
||||
Number of wrong guesses Difficulty with smoothing Difficulty without smoothing
|
||||
0 0 error
|
||||
1 0.5 1
|
||||
2 0.66 1
|
||||
3 0.75 1
|
||||
4 0.8 1
|
||||
100 0.99 1
|
||||
1000 0.999 1
|
||||
|
||||
With smoothing, a Riddle's difficulty can only be really high if there are few correct guesses
|
||||
and a lot of guesses. This seems like the right way to define difficulty.
|
||||
"""
|
||||
return 1 - (self.correct + 1) / (self.guesses + 1)
|
||||
|
||||
def to_dict(self, with_answer=True):
|
||||
"Returns this Riddle's properties in a dict, optionally including the answer"
|
||||
result = {
|
||||
"id": self.id,
|
||||
"question": self.question,
|
||||
"guesses": self.guesses,
|
||||
"correct": self.correct,
|
||||
"difficulty": self.difficulty(),
|
||||
}
|
||||
if with_answer:
|
||||
result["answer"] = self.answer
|
||||
return result
|
||||
|
||||
def check_guess(self, guess):
|
||||
"""Checks whether a guess is correct and logs the attempt.
|
||||
We don't want to be too strict, so we will accept guesses which are close to the answer.
|
||||
Fuzzy string-matching is an interesting problem, which we will sidestep by using the
|
||||
`fuzzywuzzy` library. `FUZZ_RATIO` is our limit for how similar the answers have to be.
|
||||
Also, we don't care about upper-case and lower-case, so we'll cast everything to lower.
|
||||
For example, consider the riddle, "What's brown and sticky?" The answer, of course, is
|
||||
"A stick" Here are some attempts with their fuzz ratios:
|
||||
- "a stick" 100
|
||||
- "a stik" 92
|
||||
- "stick" 83
|
||||
- "it's a stick" 74
|
||||
- "idk" 40 """
|
||||
self.guesses += 1
|
||||
similarity = fuzz.ratio(guess.lower(), self.answer.lower())
|
||||
is_correct = similarity >= self.MIN_FUZZ_RATIO
|
||||
if is_correct:
|
||||
self.correct+= 1
|
||||
self.save()
|
||||
return is_correct
|
||||
|
41
riddle_server/app/views.py
Normal file
41
riddle_server/app/views.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from banjo.urls import route_get, route_post
|
||||
from banjo.http import BadRequest, NotFound
|
||||
from app.models import Riddle
|
||||
|
||||
@route_get('all', args={})
|
||||
def list_riddles(params):
|
||||
riddles = sorted(Riddle.objects.all(), key=lambda riddle: riddle.difficulty())
|
||||
return {'riddles': [riddle.to_dict(with_answer=False) for riddle in riddles]}
|
||||
|
||||
@route_post('new', args={'question': str, 'answer': str})
|
||||
def create_riddle(params):
|
||||
riddle = Riddle.from_dict(params)
|
||||
errors = riddle.validate()
|
||||
if len(errors) == 0:
|
||||
riddle.save()
|
||||
return riddle.to_dict(with_answer=False)
|
||||
else:
|
||||
raise BadRequest("Riddle not found")
|
||||
|
||||
@route_get('show', args={'id': int})
|
||||
def show_riddle(params):
|
||||
try:
|
||||
riddle = Riddle.objects.get(id=params['id'])
|
||||
return riddle.to_dict(with_answer=False)
|
||||
except Riddle.DoesNotExist:
|
||||
raise NotFound("Riddle not found")
|
||||
|
||||
@route_post('guess', args={'id': int, "answer": str})
|
||||
def guess_answer(params):
|
||||
try:
|
||||
riddle = Riddle.objects.get(id=params['id'])
|
||||
correct = riddle.check_guess(params['answer'])
|
||||
return {
|
||||
"guess": params['answer'],
|
||||
"correct": correct,
|
||||
"riddle": riddle.to_dict(with_answer=correct)
|
||||
}
|
||||
except Riddle.DoesNotExist:
|
||||
raise NotFound("Riddle not found")
|
||||
|
||||
|
Reference in New Issue
Block a user