generated from mwc/lab_server
	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