From: Alexandre Terrasa Date: Thu, 26 Feb 2015 12:13:18 +0000 (+0100) Subject: contrib/nitrpg: introduce achievements X-Git-Tag: v0.7.2~3^2~1 X-Git-Url: http://nitlanguage.org contrib/nitrpg: introduce achievements Signed-off-by: Alexandre Terrasa --- diff --git a/contrib/nitrpg/src/achievements.nit b/contrib/nitrpg/src/achievements.nit new file mode 100644 index 0000000..625bfc2 --- /dev/null +++ b/contrib/nitrpg/src/achievements.nit @@ -0,0 +1,402 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2014-2015 Alexandre Terrasa +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# `nitrpg` achievements. +# +# Players can unlock achievements by performing remarkable actions on the repo. +# Achievements are rewarded by nitcoins. +module achievements + +import events +import statistics + +redef class GameEntity + + # Register a new achievement for this game entity. + # + # Saves the achievement in game data. + # Do nothing is the achievement is already registered. + # + # TODO should update the achievement? + fun add_achievement(achievement: Achievement) do + var key = self.key / achievement.key + if game.store.has_key(key) then return + stats.inc("achievements") + achievement.save_in(self) + save + end + + # Load the event from its `id`. + # + # Looks for the event save file in game data. + # Returns `null` if the event cannot be found. + fun load_achievement(id: String): nullable Achievement do + var key = self.key / "achievements" / id + if not game.store.has_key(key) then return null + var json = game.store.load_object(key) + return new Achievement.from_json(game, json) + end + + # List all events registered in this entity. + # + # This list is reloaded from game data each time its called. + # + # To add events see `add_event`. + fun load_achievements: MapRead[String, Achievement] do + var res = new HashMap[String, Achievement] + var key = self.key / "achievements" + if not game.store.has_collection(key) then return res + var coll = game.store.list_collection(key) + for id in coll do + res[id.to_s] = load_achievement(id.to_s).as(not null) + end + return res + end +end + +# Achievements are rewarded by `nitcoins`. +# +# An achievement represents a notable action performed by a `Player`. +# Player that `unlock` achievements are rewarded by nitcoins. +class Achievement + super GameEntity + + redef var key is lazy do return "achievements" / id + + redef var game + + # Uniq ID for this achievement. + var id: String + + # Name of this achievement. + var name: String + + # Description of the achievement. + var desc: String + + # Reward that this achievement give in nitcoins. + var reward: Int + + # Is this achievement unlocked by somebody? + var is_unlocked: Bool is lazy do return not load_events.is_empty + + # Init `self` from a `json` object. + # + # Used to load achievements from storage. + init from_json(game: Game, json: JsonObject) do + self.game = game + id = json["id"].to_s + name = json["name"].to_s + desc = json["desc"].to_s + reward = json["reward"].as(Int) + end + + redef fun to_json do + var json = super + json["id"] = id + json["name"] = name + json["desc"] = desc + json["reward"] = reward + return json + end +end + +redef class Player + + # Is `a` unlocked for this `Player`? + fun has_achievement(a: Achievement): Bool do + return load_achievement(a.id) != null + end + + # Unlocks an achievement for this Player based on a GithubEvent. + # + # Register the achievement and adds the achievement reward to the player + # nitcoins. + # + # Do nothing is this player has already unlocked the achievement. + # + # TODO: add abstraction so achievements do not depend on GithubEvent. + fun unlock_achievement(a: Achievement, event: GithubEvent) do + if has_achievement(a) then return + nitcoins += a.reward + add_achievement(a) + trigger_unlock_event(a, event) + end + + # Create a new event that marks the achievement unlocking. + fun trigger_unlock_event(achievement: Achievement, event: GithubEvent) do + var obj = new JsonObject + obj["player"] = name + obj["reward"] = achievement.reward + obj["achievement"] = achievement.id + obj["github_event"] = event.json + var ge = new GameEvent(game, "achievement_unlocked", obj) + add_event(ge) + game.add_event(ge) + achievement.add_event(ge) + end +end + +# `GameReactor` dedicated to achievements unlocking. +interface AchievementReactor + super GameReactor + + # Unic ID of the achievement this reactor unlocks. + fun id: String is abstract + + # Name of the achievement this reactor unlocks. + fun name: String is abstract + + # Description of the achievement this reactor unlocks. + fun desc: String is abstract + + # Amount of nitcoins rewarded for unlocking the achievement. + fun reward: Int is abstract + + # Return a new instance of the achievement to unlock. + fun new_achievement(game: Game): Achievement do + var achievement = new Achievement(game, id, name, desc, reward) + game.add_achievement(achievement) + return achievement + end +end + +##################### +### Issues +##################### + +# Unlock achievement after X issues. +# +# Used to factorize behavior. +abstract class PlayerXIssues + super AchievementReactor + + # Number of PR required to unlock the achievement. + var threshold: Int is noinit + + redef fun react_event(game, event) do + if not event isa IssuesEvent then return + if not event.action == "opened" then return + var player = event.issue.user.player(game) + if player.stats["issues"] == threshold then + var a = new_achievement(game) + player.unlock_achievement(a, event) + end + end +end + +# Player open his first issue. +class Player1Issue + super PlayerXIssues + + redef var id = "player_1_issue" + redef var name = "First complaint" + redef var desc = "Open your first issue." + redef var reward = 10 + redef var threshold = 1 +end + +# Player open 100 issues. +class Player100Issues + super PlayerXIssues + + redef var id = "player_100_issues" + redef var name = "Mature whiner" + redef var desc = "Open 100 issues in the game." + redef var reward = 100 + redef var threshold = 100 +end + +# Player open 1 000 issues. +class Player1KIssues + super PlayerXIssues + + redef var id = "player_1000_issues" + redef var name = "You, sir, complain a lot" + redef var desc = "Open 1000 issues in the game." + redef var reward = 1000 + redef var threshold = 1000 +end + +# Player open an issue about nitdoc. +class IssueAboutNitdoc + super AchievementReactor + + redef var id = "issue_about_nitdoc" + redef var name = "Say nitdoc again, I double dare you!" + redef var desc = "Open an issue with \"nitdoc\" in the title." + redef var reward = 10 + + redef fun react_event(game, event) do + if not event isa IssuesEvent then return + if not event.action == "opened" then return + var player = event.issue.user.player(game) + var re = "nitdoc".to_re + re.ignore_case = true + if event.issue.title.has(re) then + var a = new_achievement(game) + player.unlock_achievement(a, event) + end + end +end + +# Player open an issue about FFI. +class IssueAboutFFI + super PlayerXIssues + + redef var id = "issue_about_ffi" + redef var name = "Polyglot what?" + redef var desc = "Open an issue with `ffi` in the title." + redef var reward = 10 + + redef fun react_event(game, event) do + if not event isa IssuesEvent then return + if not event.action == "opened" then return + var player = event.issue.user.player(game) + var re = "\\bffi\\b".to_re + re.ignore_case = true + if event.issue.title.has(re) then + var a = new_achievement(game) + player.unlock_achievement(a, event) + end + end +end + +##################### +### Pull requests +##################### + +# Unlock achievement after X pull requests. +# +# Used to factorize behavior. +abstract class PlayerXPulls + super AchievementReactor + + # Number of PR required to unlock the achievement. + var threshold: Int is noinit + + redef fun react_event(game, event) do + if not event isa PullRequestEvent then return + if not event.action == "opened" then return + var player = event.pull.user.player(game) + if player.stats["pulls"] == threshold then + var a = new_achievement(game) + player.unlock_achievement(a, event) + end + end +end + +# Open your first pull request. +class Player1Pull + super PlayerXPulls + + redef var id = "player_1_pull" + redef var name = "First PR" + redef var desc = "Open your first pull request." + redef var reward = 10 + redef var threshold = 1 +end + +# Author 100 pull requests. +class Player100Pulls + super PlayerXPulls + + redef var id = "player_100_pulls" + redef var name = "100 pull requests!!!" + redef var desc = "Open 100 pull requests in the game." + redef var reward = 100 + redef var threshold = 100 +end + +# Author 1000 pull requests. +class Player1KPulls + super PlayerXPulls + + redef var id = "player_1000_pulls" + redef var name = "1000 PULL REQUESTS!!!" + redef var desc = "Open 1000 pull requests in the game." + redef var reward = 1000 + redef var threshold = 1000 +end + +##################### +### Commits +##################### + +# Unlock achievement after X merged commits. +# +# Used to factorize behavior. +abstract class PlayerXCommits + super AchievementReactor + + # Number of PR required to unlock the achievement. + var threshold: Int is noinit + + redef fun react_event(game, event) do + if not event isa PullRequestEvent then return + if not event.action == "closed" then return + if not event.pull.merged then return + var player = event.pull.user.player(game) + if player.stats["commits"] == threshold then + var a = new_achievement(game) + player.unlock_achievement(a, event) + end + end +end + +# Author your first commit in the game. +class Player1Commit + super PlayerXCommits + + redef var id = "player_1_commit" + redef var name = "First blood" + redef var desc = "Author your first commit in the game." + redef var reward = 10 + redef var threshold = 1 +end + +# Author 100 commits. +class Player100Commits + super PlayerXCommits + + redef var id = "player_100_commits" + redef var name = "100 commits" + redef var desc = "Author 100 commits in the game." + redef var reward = 100 + redef var threshold = 100 +end + +# Author 1 000 commits. +class Player1KCommits + super PlayerXCommits + + redef var id = "player_1000_commits" + redef var name = "1000 commits!!!" + redef var desc = "Author 1000 commits in the game." + redef var reward = 1000 + redef var threshold = 1000 +end + +# Author 10 000 commits. +class Player10KCommits + super PlayerXCommits + + redef var id = "player_10000_commits" + redef var name = "10000 COMMITS!!!" + redef var desc = "Author 10000 commits in the game." + redef var reward = 10000 + redef var threshold = 10000 +end diff --git a/contrib/nitrpg/src/listener.nit b/contrib/nitrpg/src/listener.nit index d35b169..0e4ba00 100644 --- a/contrib/nitrpg/src/listener.nit +++ b/contrib/nitrpg/src/listener.nit @@ -17,8 +17,8 @@ # This tool is runned to listen to `Github::Event` and update the game. module listener -import statistics import reactors +import achievements import github::hooks # `HookListener` that redirects events to a `Game` instance. @@ -34,8 +34,14 @@ class RpgHookListener # TODO handle verbosity with opts game.verbose_lvl = 1 game.message(1, "Received event {event} for {game.repo.full_name}") - for reactor in reactors do reactor.react_event(game, event) + for reactor in reactors do + game.message(2, "Apply reactor {reactor} on {event}") + reactor.react_event(game, event) + end end + + # Register a reactor for this listener. + fun add_reactor(reactors: GameReactor...) do self.reactors.add_all reactors end if args.length != 2 then @@ -51,9 +57,12 @@ var port = args[1].to_i var api = new GithubAPI(get_github_oauth) -var listener = new RpgHookListener(api, host, port) -listener.reactors.add new StatisticsReactor -listener.reactors.add new PlayerReactor +var l = new RpgHookListener(api, host, port) +l.add_reactor(new StatisticsReactor, new PlayerReactor) +l.add_reactor(new Player1Issue, new Player100Issues, new Player1KIssues) +l.add_reactor(new Player1Pull, new Player100Pulls, new Player1KPulls) +l.add_reactor(new Player1Commit, new Player100Commits, new Player1KCommits) +l.add_reactor(new IssueAboutNitdoc, new IssueAboutFFI) print "Listening events on {host}:{port}" -listener.listen +l.listen