From 4d7db99ded9d160d8f21c32de08cd867490de082 Mon Sep 17 00:00:00 2001 From: Vilhjalmur Thorsteinsson Date: Sun, 24 Jan 2021 19:12:36 +0000 Subject: [PATCH 1/2] Bug fix in ResponseMove logic; removed indices in GameModel --- .gitignore | 3 +- admin.py | 14 +-- netskrafl.py | 35 +++++--- resources/ordalisti.add.txt | 8 ++ resources/ordalisti.remove.txt | 1 + skrafldb.py | 15 ++-- skraflgame.py | 28 +++--- skraflmechanics.py | 159 +++++++++++++++++++-------------- skrafltester.py | 8 +- 9 files changed, 158 insertions(+), 113 deletions(-) diff --git a/.gitignore b/.gitignore index 7ed78c2..94989a8 100755 --- a/.gitignore +++ b/.gitignore @@ -37,8 +37,7 @@ resources/TWL06.txt resources/sowpods.txt # CSS files generated from Less -static/skrafl-curry.css -static/skrafl-desat.css +static/skrafl-*.css # Concatenated JS static/netskrafl.js diff --git a/admin.py b/admin.py index f6d0f5e..4049b66 100755 --- a/admin.py +++ b/admin.py @@ -20,6 +20,7 @@ import logging from threading import Thread +from datetime import datetime from flask import jsonify from flask import request @@ -126,21 +127,22 @@ def admin_loadgame() -> str: uuid = request.form.get("uuid", None) game = None - g: Optional[Dict[str, Any]] + g: Optional[Dict[str, Any]] = None if uuid: # Attempt to load the game whose id is in the URL query string game = Game.load(uuid) - if game: + if game is not None and game.state is not None: board = game.state.board() + now = datetime.utcnow() g = dict( uuid=game.uuid, - timestamp=Alphabet.format_timestamp(game.timestamp), + timestamp=Alphabet.format_timestamp(game.timestamp or now), player0=game.player_ids[0], player1=game.player_ids[1], robot_level=game.robot_level, - ts_last_move=Alphabet.format_timestamp(game.ts_last_move), + ts_last_move=Alphabet.format_timestamp(game.ts_last_move or now), irack0=game.initial_racks[0], irack1=game.initial_racks[1], prefs=game._preferences, @@ -148,15 +150,13 @@ def admin_loadgame() -> str: moves=[ ( m.player, - m.move.summary(board), + m.move.summary(game.state), m.rack, Alphabet.format_timestamp(m.ts), ) for m in game.moves ], ) - else: - g = None return jsonify(game=g) diff --git a/netskrafl.py b/netskrafl.py index 5f9a17a..effb789 100755 --- a/netskrafl.py +++ b/netskrafl.py @@ -1197,6 +1197,8 @@ def gamestate(): uuid = rq.get("game") user_id = current_user_id() + assert user_id is not None + game = Game.load(uuid) if uuid else None if game is None: @@ -1567,7 +1569,7 @@ def chatmsg() -> Response: if uuid: game = Game.load(uuid) - if game is None or not game.has_player(user_id): + if game is None or user_id is None or not game.has_player(user_id): # The logged-in user must be a player in the game return jsonify(ok=False) @@ -1584,8 +1586,11 @@ def chatmsg() -> Response: msg = {} for p in range(0, 2): # Send a Firebase notification to /game/[gameid]/[userid]/chat - msg["game/" + uuid + "/" + game.player_id(p) + "/chat"] = md - firebase.send_message(msg) + pid = game.player_id(p) + if pid is not None: + msg["game/" + uuid + "/" + pid + "/chat"] = md + if msg: + firebase.send_message(msg) return jsonify(ok=True) @@ -1609,7 +1614,7 @@ def chatload() -> Response: if uuid: game = Game.load(uuid) - if game is None or not game.has_player(user_id): + if game is None or user_id is None or not game.has_player(user_id): # The logged-in user must be a player in the game return jsonify(ok=False) @@ -1680,9 +1685,10 @@ def review() -> ResponseType: # 19 is what fits on screen best_moves = apl.generate_best_moves(19) - if game.has_player(user.id()): + uid = user.id() + if uid is not None and game.has_player(uid): # Look at the game from the point of view of this player - user_index = game.player_index(user.id()) + user_index = game.player_index(uid) else: # This is an outside spectator: look at it from the point of view of # player 0, or the human player if player 0 is an autoplayer @@ -1748,9 +1754,10 @@ def bestmoves() -> Response: (player_index, m.summary(state)) for m, _ in apl.generate_best_moves(19) ] - if game.has_player(user.id()): + uid = user.id() + if uid is not None and game.has_player(uid): # Look at the game from the point of view of this player - user_index = game.player_index(user.id()) + user_index = game.player_index(uid) else: # This is an outside spectator: look at it from the point of view of # player 0, or the human player if player 0 is an autoplayer @@ -2243,11 +2250,13 @@ def board() -> ResponseType: # Delete the Firebase subtree for this game, # to get earlier move and chat notifications out of the way if firebase_token is not None and user is not None: - msg = { - "game/" + game.id() + "/" + uid: None, - "user/" + uid + "/wait": None, - } - firebase.send_message(msg) + game_id = game.id() + if game_id is not None: + msg = { + "game/" + game_id + "/" + uid: None, + "user/" + uid + "/wait": None, + } + firebase.send_message(msg) # No need to clear other stuff on the /user/[user_id]/ path, # since we're not listening to it in board.html diff --git a/resources/ordalisti.add.txt b/resources/ordalisti.add.txt index 4267921..b1789fa 100644 --- a/resources/ordalisti.add.txt +++ b/resources/ordalisti.add.txt @@ -44,6 +44,7 @@ ey fa fá fé +fleiru fleirum fæ gá @@ -112,6 +113,13 @@ pæ rá re ré +rita +ritan +ritu +rituna +ritunnar +ritunni +riturnar rí ró rú diff --git a/resources/ordalisti.remove.txt b/resources/ordalisti.remove.txt index e7d9340..404343a 100644 --- a/resources/ordalisti.remove.txt +++ b/resources/ordalisti.remove.txt @@ -5,3 +5,4 @@ hjálpsamligana hralestðu ennig gínðu +býj diff --git a/skrafldb.py b/skrafldb.py index 274fbef..856e803 100755 --- a/skrafldb.py +++ b/skrafldb.py @@ -446,16 +446,15 @@ class GameModel(ndb.Model): player0 = ndb.KeyProperty(kind=UserModel) player1 = ndb.KeyProperty(kind=UserModel) - # The racks - rack0 = ndb.StringProperty() - rack1 = ndb.StringProperty() + rack0 = ndb.StringProperty(indexed=False) + rack1 = ndb.StringProperty(indexed=False) # The scores - score0 = ndb.IntegerProperty() - score1 = ndb.IntegerProperty() + score0 = ndb.IntegerProperty(indexed=False) + score1 = ndb.IntegerProperty(indexed=False) # Whose turn is it next, 0 or 1? - to_move = ndb.IntegerProperty() + to_move = ndb.IntegerProperty(indexed=False) # How difficult should the robot player be (if the opponent is a robot)? # None or 0 = most difficult @@ -474,8 +473,8 @@ class GameModel(ndb.Model): moves = ndb.LocalStructuredProperty(MoveModel, repeated=True, indexed=False) # The initial racks - irack0 = ndb.StringProperty(required=False, default=None) - irack1 = ndb.StringProperty(required=False, default=None) + irack0 = ndb.StringProperty(required=False, indexed=False, default=None) + irack1 = ndb.StringProperty(required=False, indexed=False, default=None) # Game preferences, such as duration, alternative bags or boards, etc. prefs = ndb.JsonProperty(required=False, default=None) diff --git a/skraflgame.py b/skraflgame.py index 6dbffec..cfab120 100755 --- a/skraflgame.py +++ b/skraflgame.py @@ -685,7 +685,7 @@ def _make_new( player1_id: Optional[str], robot_level: int = 0, prefs: Optional[PrefsDict] = None, - ): + ) -> None: """ Initialize a new, fresh game """ self._preferences = prefs # If either player0_id or player1_id is None, this is a human-vs-autoplayer game @@ -709,7 +709,7 @@ def new( player1_id: Optional[str], robot_level: int = 0, prefs: Optional[PrefsDict] = None, - ): + ) -> Game: """ Start and initialize a new game """ game = cls(Unique.id()) # Assign a new unique id to the game if randint(0, 1) == 1: @@ -724,20 +724,20 @@ def new( return game @classmethod - def load(cls, uuid, use_cache=True): + def load(cls, uuid: str, use_cache: bool = True) -> Optional[Game]: """ Load an already existing game from persistent storage """ with Game._lock: # Ensure that the game load does not introduce race conditions return cls._load_locked(uuid, use_cache) - def store(self): + def store(self) -> None: """ Store the game state in persistent storage """ # Avoid race conditions by securing the lock before storing with Game._lock: self._store_locked() @classmethod - def _load_locked(cls, uuid, use_cache=True): + def _load_locked(cls, uuid: str, use_cache: bool = True) -> Optional[Game]: """ Load an existing game from cache or persistent storage under lock """ gm = GameModel.fetch(uuid, use_cache) @@ -838,7 +838,7 @@ def _load_locked(cls, uuid, use_cache=True): elif mm.tiles == "RESP": # Response to challenge - m = ResponseMove() + m = ResponseMove(mm.score) if m is None: # Something is wrong: mark the game as erroneous @@ -872,7 +872,7 @@ def _load_locked(cls, uuid, use_cache=True): return game - def _store_locked(self): + def _store_locked(self) -> None: """ Store the game after having acquired the object lock """ assert self.uuid is not None @@ -1103,7 +1103,7 @@ def net_moves(self) -> List[MoveTuple]: assert self.state is not None net_m: List[MoveTuple] = [] for m in self.moves: - if isinstance(m.move, ResponseMove) and m.move.score(self.state) < 0: + if m.move.is_successful_challenge(self.state): # Successful challenge: Erase the two previous moves # (the challenge and the illegal move) assert len(net_m) >= 2 @@ -1463,13 +1463,17 @@ def is_last_challenge(self) -> bool: return self.state.is_last_challenge() def client_state( - self, player_index: int, lastmove: Optional[MoveBase] = None, deep: bool = False + self, + player_index: Optional[int], + lastmove: Optional[MoveBase] = None, + deep: bool = False, ) -> Dict[str, Any]: """ Create a package of information for the client about the current state """ assert self.state is not None reply: Dict[str, Any] = dict() num_moves = 1 - lm = None + lm: Optional[MoveBase] = None + succ_chall = False if self.last_move is not None: # Show the autoplayer or response move that was made lm = self.last_move @@ -1480,8 +1484,8 @@ def client_state( lm = lastmove if lm is not None: reply["lastmove"] = lm.details(self.state) - # Successful challenge? - succ_chall = isinstance(lm, ResponseMove) and lm.score(self.state) < 0 + # Successful challenge? + succ_chall = lm.is_successful_challenge(self.state) newmoves = [ (m.player, m.move.summary(self.state)) for m in self.moves[-num_moves:] ] diff --git a/skraflmechanics.py b/skraflmechanics.py index 8b9a663..150ba88 100755 --- a/skraflmechanics.py +++ b/skraflmechanics.py @@ -81,7 +81,7 @@ " 2 2 ", " 2 2 ", "3 3 3", - ] + ], } _LSC = { @@ -123,8 +123,8 @@ # For each board type, convert the word and letter score strings to integer arrays _xlt = lambda arr: [[1 if c == " " else int(c) for c in row] for row in arr] -_WORDSCORE = { key: _xlt(val) for key, val in _WSC.items() } -_LETTERSCORE = { key: _xlt(val) for key, val in _LSC.items() } +_WORDSCORE = {key: _xlt(val) for key, val in _WSC.items()} +_LETTERSCORE = {key: _xlt(val) for key, val in _LSC.items()} class Board: @@ -495,8 +495,8 @@ def randomize_and_sort(self, bag: Bag) -> None: class State: - """ Represents the state of a game at a particular point. - Contains the current board, the racks, scores, etc. """ + """Represents the state of a game at a particular point. + Contains the current board, the racks, scores, etc.""" def __init__( self, @@ -506,7 +506,10 @@ def __init__( copy: Optional[State] = None, locale: Optional[str] = None, board_type: Optional[str] = None, - ): + ) -> None: + + # The covers laid down in the last challengeable move + self._last_covers: Optional[List[Cover]] = None # pylint: disable=protected-access if copy is None: @@ -527,8 +530,6 @@ def __init__( self._challenge_score = 0 # The rack before the last challengeable move self._last_rack: Optional[str] = None - # The covers laid down in the last challengeable move - self._last_covers = None # Initialize a fresh, full bag of tiles self._tileset = tileset if manual_wordcheck and _DEBUG_MANUAL_WORDCHECK: @@ -559,7 +560,7 @@ def __init__( self._board_type = copy._board_type self._bag = Bag(tileset=None, copy=copy._bag) - def load_board(self, board): + def load_board(self, board: Board) -> None: """ Load a Board into this state """ self._board = board @@ -664,12 +665,12 @@ def last_rack(self) -> Optional[str]: return self._last_rack @property - def last_covers(self): + def last_covers(self) -> Optional[List[Cover]]: """ Return the covers of the last/challengeable move """ return self._last_covers @property - def challenge_score(self): + def challenge_score(self) -> int: """ The score of a challenge move, if made """ return self._challenge_score @@ -683,7 +684,9 @@ def clear_challengeable(self) -> None: self._last_covers = None self._challenge_score = 0 - def set_challengeable(self, score, covers, last_rack): + def set_challengeable( + self, score: int, covers: List[Cover], last_rack: str + ) -> None: """ Set the challengeable state, with the given covers being laid down """ if score and self.manual_wordcheck: self._challenge_score = score @@ -696,7 +699,7 @@ def rack(self, index: int) -> str: """ Return the contents of the rack (indexed by 0 or 1) """ return self._racks[index].contents() - def rack_details(self, index: int): + def rack_details(self, index: int) -> List[Tuple[str, int]]: """ Return the contents of the rack (indexed by 0 or 1) """ assert self._tileset is not None return self._racks[index].details(self._tileset) @@ -743,7 +746,11 @@ def is_last_challenge(self) -> bool: """ Is the game waiting for a potential challenge of the last move? """ return self.is_challengeable() and any(r.is_empty() for r in self._racks) - def finalize_score(self, lost_on_overtime=None, overtime_adjustment=None): + def finalize_score( + self, + lost_on_overtime: Optional[int] = None, + overtime_adjustment: Optional[Tuple[int, int]] = None, + ) -> None: """ When game is completed, calculate the final score adjustments """ if self._game_resigned: @@ -820,7 +827,7 @@ class Cover: # pylint: disable=too-few-public-methods - def __init__(self, row, col, tile, letter): + def __init__(self, row: int, col: int, tile: str, letter: str) -> None: self.row = row self.col = col self.tile = tile @@ -833,7 +840,7 @@ class Error: # pylint: disable=too-few-public-methods - def __init__(self): + def __init__(self) -> None: pass LEGAL = 0 @@ -906,9 +913,9 @@ def __init__(self) -> None: # pylint: disable=unused-argument # noinspection PyUnusedLocal - def details(self, state: State) -> List: - """ Return a tuple list describing tiles committed - to the board by this move """ + def details(self, state: State) -> List[DetailTuple]: + """Return a tuple list describing tiles committed + to the board by this move""" return [] # No tiles # noinspection PyUnusedLocal @@ -953,18 +960,24 @@ def needs_response_move(self) -> bool: # Only True for ChallengeMove instances return False + def is_successful_challenge(self, state: State) -> bool: + """ Is this move a successful challenge? """ + # Only True for ResponseMove instances that denote + # successful challenges (and thus have a negative score) + return False + class Move(MoveBase): - """ Represents a move by a player """ + """ Represents a normal tile move """ # Bonus score for playing all 7 tiles in one move BINGO_BONUS = 50 # If an opponent challenges a valid move, the player gets a bonus INCORRECT_CHALLENGE_BONUS = 10 - def __init__(self, word: str, row: int, col: int, horiz: bool=True) -> None: - super(Move, self).__init__() + def __init__(self, word: str, row: int, col: int, horiz: bool = True) -> None: + super().__init__() # A list of squares covered by the play, i.e. actual tiles # laid down on the board self._covers: List[Cover] = [] @@ -1036,14 +1049,14 @@ def summary(self, state: State) -> SummaryTuple: return (self.short_coordinate(), self._tiles or "", self.score(state)) def short_coordinate(self) -> str: - """ Return the coordinate of the move, - i.e. row letter + column number for horizontal moves or - column number + row letter for vertical ones """ + """Return the coordinate of the move, + i.e. row letter + column number for horizontal moves or + column number + row letter for vertical ones""" return Board.short_coordinate(self._horizontal, self._row, self._col) def __str__(self) -> str: - """ Return the standard move notation of a coordinate - followed by the word formed """ + """Return the standard move notation of a coordinate + followed by the word formed""" return self.short_coordinate() + ":'" + self._word + "'" def add_cover(self, row: int, col: int, tile: str, letter: str) -> bool: @@ -1055,7 +1068,11 @@ def add_cover(self, row: int, col: int, tile: str, letter: str) -> bool: return False if (tile is None) or len(tile) != 1: return False - if (letter is None) or len(letter) != 1 or (letter not in current_alphabet().order): + if ( + (letter is None) + or len(letter) != 1 + or (letter not in current_alphabet().order) + ): return False if tile != "?" and tile != letter: return False @@ -1078,8 +1095,8 @@ def make_covers(self, board: Board, tiles: str) -> None: self.set_tiles(tiles) def enum_covers(tiles: str) -> Iterator[Tuple[str, str]]: - """ Generator to enumerate through a tiles string, - yielding (tile, letter) tuples """ + """Generator to enumerate through a tiles string, + yielding (tile, letter) tuples""" ix = 0 while ix < len(tiles): if tiles[ix] == "?": @@ -1213,11 +1230,12 @@ def check_legality(self, state: State) -> Union[int, Tuple[int, str]]: self._word = "" self._tiles = "" - def add(cix): + def add(cix: int) -> None: """ Add a cover's letter and tile to the word and tiles strings """ ltr = self._covers[cix].letter tile = self._covers[cix].tile self._word += ltr + assert self._tiles is not None self._tiles += tile + (ltr if tile == "?" else "") cix = 0 @@ -1245,10 +1263,10 @@ def add(cix): self._word += ltr self._tiles += ltr - def is_valid_word(word): - """ Check whether a word is in the dictionary, - unless this is a manual game """ - return True if state.manual_wordcheck else word in self._dawg + def is_valid_word(word: str) -> bool: + """Check whether a word is in the dictionary, + unless this is a manual game""" + return state.manual_wordcheck or (word in self._dawg) # Check whether the word is in the dictionary if not is_valid_word(self._word): @@ -1289,8 +1307,8 @@ def is_valid_word(word): return Error.LEGAL def check_words(self, board: Board) -> List[str]: - """ Do simple word validation on this move, returning - a list of invalid words formed """ + """Do simple word validation on this move, returning + a list of invalid words formed""" invalid: List[str] = [] @@ -1412,11 +1430,11 @@ def apply(self, state: State, shallow: bool = False) -> None: class ExchangeMove(MoveBase): - """ Represents an exchange move, where tiles are returned to the bag - and new tiles drawn instead """ + """Represents an exchange move, where tiles are returned to the bag + and new tiles drawn instead""" def __init__(self, tiles: str) -> None: - super(ExchangeMove, self).__init__() + super().__init__() self._tiles = tiles def __str__(self) -> str: @@ -1456,24 +1474,24 @@ def apply(self, state: State, shallow: bool = False) -> None: class ChallengeMove(MoveBase): - """ Represents a challenge move, where the last move played by the - opponent is challenged. + """Represents a challenge move, where the last move played by the + opponent is challenged. - If the challenge is correct, the opponent - loses the points he got for the wrong word. + If the challenge is correct, the opponent + loses the points he got for the wrong word. - Move sequence for a correct challenge: + Move sequence for a correct challenge: - [wrong move, score=X] CHALL score=0 - RESP score=-X [next move by challenger] + [wrong move, score=X] CHALL score=0 + RESP score=-X [next move by challenger] - If the challenge is incorrect, the opponent gets a 10 point - bonus but the challenger does not lose his turn. + If the challenge is incorrect, the opponent gets a 10 point + bonus but the challenger does not lose his turn. - Move sequence for an incorrect challenge: + Move sequence for an incorrect challenge: - [correct move, score=X] CHALL score=0 - RESP score=10 [next move by challenger] + [correct move, score=X] CHALL score=0 + RESP score=10 [next move by challenger] """ @@ -1519,9 +1537,9 @@ class ResponseMove(MoveBase): """ Represents a response to a challenge move """ - def __init__(self) -> None: + def __init__(self, score: Optional[int] = None) -> None: super().__init__() - self._score: Optional[int] = None + self._score = score self._num_covers = 0 def __str__(self) -> str: @@ -1559,6 +1577,12 @@ def score(self, state: State) -> int: assert self._score != 0 return self._score + def is_successful_challenge(self, state: State) -> bool: + """ Is this move a successful challenge? """ + # Only True for ResponseMove instances that denote + # successful challenges (and thus have a negative score) + return self.score(state) < 0 + # noinspection PyMethodMayBeStatic def num_covers(self) -> int: """ Return the number of tiles played in this move """ @@ -1571,6 +1595,7 @@ def apply(self, state: State, shallow: bool = False) -> None: board = state.board() # Remove the last move from the board last_covers = state.last_covers + assert last_covers is not None self._num_covers = -len(last_covers) # Negative cover count for c in last_covers: board.set_letter(c.row, c.col, " ") @@ -1593,22 +1618,22 @@ class PassMove(MoveBase): """ Represents a pass move, where the player does nothing """ - def __str__(self): + def __str__(self) -> str: """ Return a readable string describing the move """ return "Pass" - def replenish(self): + def replenish(self) -> bool: """ Return True if the player's rack should be replenished after the move """ return False # noinspection PyMethodMayBeStatic,PyUnusedLocal # pylint: disable=unused-argument - def summary(self, board): + def summary(self, state: State) -> SummaryTuple: """ Return a summary of the move, as a tuple: (coordinate, word, score) """ return ("", "PASS", 0) # noinspection PyMethodMayBeStatic,PyUnusedLocal - def apply(self, state, shallow=False): + def apply(self, state: State, shallow: bool = False) -> None: """ Apply this move, assumed to be legal, to the current game state """ # Increment the number of consecutive Pass moves state.add_pass() # Clears the challengeable flag @@ -1618,31 +1643,31 @@ class ResignMove(MoveBase): """ Represents a resign move, where the player forfeits the game """ - def __init__(self, forfeited_points): - super(ResignMove, self).__init__() + def __init__(self, forfeited_points: int) -> None: + super().__init__() self._forfeited_points = forfeited_points - def __str__(self): - """ Return the standard move notation of a coordinate - followed by the word formed """ + def __str__(self) -> str: + """Return the standard move notation of a coordinate + followed by the word formed""" return "Resign" - def replenish(self): + def replenish(self) -> bool: """ Return True if the player's rack should be replenished after the move """ return False # pylint: disable=unused-argument - def summary(self, board): + def summary(self, state: State) -> SummaryTuple: """ Return a summary of the move, as a tuple: (coordinate, word, score) """ return ("", "RSGN", -self._forfeited_points) - def score(self, state): + def score(self, state: State) -> int: """ Calculate the score of this move, which is assumed to be legal """ # A resignation loses all points return -self._forfeited_points # noinspection PyMethodMayBeStatic - def apply(self, state, shallow=False): + def apply(self, state: State, shallow: bool = False) -> None: """ Apply this move, assumed to be legal, to the current game state """ # Resign the game, causing is_game_over() to become True state.resign_game() # Clears the challengeable flag diff --git a/skrafltester.py b/skrafltester.py index 40e865c..d9808f0 100755 --- a/skrafltester.py +++ b/skrafltester.py @@ -351,12 +351,12 @@ class Usage(Exception): """ Error reporting exception for wrong command line arguments """ - def __init__(self, msg): - super(Usage, self).__init__(msg) + def __init__(self, msg: getopt.GetoptError) -> None: + super().__init__(msg.msg) self.msg = msg -def main(argv=None): +def main(argv: Optional[List[str]]=None) -> int: """ Guido van Rossum's pattern for a Python main function """ if argv is None: @@ -415,7 +415,7 @@ def main(argv=None): return 0 -def profile_main(): +def profile_main() -> None: """ Main function to invoke for profiling """ From 89ccf5f1c6c5637089de7855d76e0a04bdb84476 Mon Sep 17 00:00:00 2001 From: Vilhjalmur Thorsteinsson Date: Mon, 25 Jan 2021 10:32:58 +0000 Subject: [PATCH 2/2] Small fixes in skrafltester.py and netskrafl.py --- dawgtester.py | 2 ++ netskrafl.py | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dawgtester.py b/dawgtester.py index 4703fc1..f2ec91a 100755 --- a/dawgtester.py +++ b/dawgtester.py @@ -215,10 +215,12 @@ def run(self, fname, relpath): self._test_false("mismri") self._test_false("strinum") self._test_false("sigrihrt") + self._test_false("býj") self._test_true("fau") self._test_true("ifa") self._test_true("yla") + self._test_true("ritu") # All two-letter words on the official list of the # Icelandic Skrafl society diff --git a/netskrafl.py b/netskrafl.py index effb789..fd29378 100755 --- a/netskrafl.py +++ b/netskrafl.py @@ -1714,9 +1714,8 @@ def bestmoves() -> Response: user = current_user() assert user is not None - # !!! FIXME - if False: # not user.has_paid(): - # User must be a paying friend + if not user.has_paid() and not running_local: + # User must be a paying friend, or we're on a development server return jsonify(result=Error.USER_MUST_BE_FRIEND) rq = RequestData(request)