+++ /dev/null
-# This file is part of NIT ( http://www.nitlanguage.org ).
-#
-# Copyright 2014-2015 Alexandre Terrasa <alexandre@moz-code.org>
-#
-# 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.key)
- 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 game
-
- redef var key is lazy do
- var owner = self.owner
- if owner == null then return id
- return "{owner.key}-{id}"
- end
-
- # 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
-
- # Game entity this achievement is about.
- var owner: nullable GameEntity = null
-
- # Init `self` from a `json` object.
- #
- # Used to load achievements from storage.
- init from_json(game: Game, json: JsonObject) do
- init(game, json["id"].to_s, json["name"].to_s, json["desc"].to_s, 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
- json["game"] = game.key
- if owner != null then json["owner"] = owner.key
- 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
-
-#####################
-### Issue Comments
-#####################
-
-# Unlock achievement after X issue comments.
-#
-# Used to factorize behavior.
-abstract class PlayerXComments
- super AchievementReactor
-
- # Number of comments required to unlock the achievement.
- var threshold: Int is noinit
-
- redef fun react_event(game, event) do
- if not event isa IssueCommentEvent then return
- if not event.action == "created" then return
- var player = event.comment.user.player(game)
- if player.stats["comments"] == threshold then
- var a = new_achievement(game)
- player.unlock_achievement(a, event)
- end
- end
-end
-
-# Player author his first comment in issues.
-class Player1Comment
- super PlayerXComments
-
- redef var id = "player_1_comment"
- redef var name = "From lurker to member"
- redef var desc = "Comment on an issue."
- redef var reward = 10
- redef var threshold = 1
-end
-
-# Player author 100 issue comments.
-class Player100Comments
- super PlayerXComments
-
- redef var id = "player_100_comments"
- redef var name = "Chatter"
- redef var desc = "Comment 100 times on issues."
- redef var reward = 100
- redef var threshold = 100
-end
-
-# Player author 1000 issue comments.
-class Player1KComments
- super PlayerXComments
-
- redef var id = "player_1000_comments"
- redef var name = "You sir, talk a lot!"
- redef var desc = "Comment 1000 times on issues."
- redef var reward = 1000
- redef var threshold = 1000
-end
-
-# Ping @privat in a comment.
-class PlayerPingGod
- super AchievementReactor
-
- redef var id = "player_ping_god"
- redef var name = "Ping god"
- redef var desc = "Ping the owner of the repo for the first time."
- redef var reward = 50
-
- redef fun react_event(game, event) do
- if not event isa IssueCommentEvent then return
- var owner = game.repo.owner.login
- if event.comment.body.has("@{owner}".to_re) then
- var player = event.comment.user.player(game)
- var a = new_achievement(game)
- player.unlock_achievement(a, event)
- end
- end
-end
-
-# Give your first +1
-class PlayerFirstReview
- super AchievementReactor
-
- redef var id = "player_first_review"
- redef var name = "First +1"
- redef var desc = "Give a +1 for the first time."
- redef var reward = 10
-
- redef fun react_event(game, event) do
- if not event isa IssueCommentEvent then return
- # FIXME use a more precise way to locate reviews
- if event.comment.is_ack then
- var player = event.comment.user.player(game)
- var a = new_achievement(game)
- player.unlock_achievement(a, event)
- end
- end
-end
-
-# Talk about nitcoin in issue comments.
-class PlayerSaysNitcoin
- super AchievementReactor
-
- redef var id = "player_says_nitcoin"
- redef var name = "Talking about money"
- redef var desc = "Say something about nitcoins in a comment."
- redef var reward = 10
-
- redef fun react_event(game, event) do
- if not event isa IssueCommentEvent then return
- if event.comment.body.has("(n|N)itcoin".to_re) then
- var player = event.comment.user.player(game)
- var a = new_achievement(game)
- player.unlock_achievement(a, event)
- end
- end
-end