From: Jean Privat Date: Wed, 22 Apr 2015 04:18:23 +0000 (+0700) Subject: Merge: Nitdoc: more cleaning and uniformize method names X-Git-Tag: v0.7.4~13 X-Git-Url: http://nitlanguage.org?hp=170f5bc6752555e1c09d23c1975dd8b3194fde32 Merge: Nitdoc: more cleaning and uniformize method names Démos: * [stdlib](http://gresil.org/jenkins/job/CI-nitdoc/ws/doc/stdlib/index.html) * [nitc](http://gresil.org/jenkins/job/CI-nitdoc/ws/doc/nitc/index.html) Pull-Request: #1283 Reviewed-by: Jean Privat Reviewed-by: Romain Chanoir --- diff --git a/contrib/nitrpg/README.md b/contrib/nitrpg/README.md new file mode 100644 index 0000000..c91c77e --- /dev/null +++ b/contrib/nitrpg/README.md @@ -0,0 +1,84 @@ +# Welcome to NitRPG! + +NitRPG is a Role Playing Game that takes place on [GitHub](https://github.com/). + +In NitRPG, GitHub users are represented by players that battle on repo for +nitcoins and glory. + +## Features + +* Auto-update with GitHub hooks +* Display repo statistics +* Display players statsitics +* Repo actions are rewarded by nitcoins +* Players can unlock achievements + +## How to install + +From the `nit` root: + +~~~bash +> cd contrib/nitrpg +> make +~~~ + +### Configuring the GitHub hook + +NitRPG needs you to add a new GitHub hook on your repo to keep the game +`listener` up-to-date automatically. + +Hook configuration: + +* **Payload URL**: URL and port to the listener (ex: `http://yourdomain.com:8080`) +* **Content type**: `application/json` +* **Wich events**: `Send me everything` + +Be sure to set the hook as `Active` in the GitHub admin panel. + +### Starting the listener + +The `listener` program is used to listen to GitHub hooks and update game data. +It should alwaysd be up if you want your game to be kept up-to-date. + +To run the listener: + + ./listener + +The arguments `host` and `port` must correspond to what you entered in your +GitHub hook settings. + +### Starting the web server + +The `web` program act as a [nitcorn](http://nitlanguage.org/doc/stdlib/module_nitcorn__nitcorn.html) webserver that display the game results live. + +To run the webserver: + + ./web + +The arguments `host` and `port` must correspond to what you entered in your +GitHub hook settings. +The `root` argument is used to specify the path from the domain url to the +NitRPG root. + +For example, if NitRPG is installed in `yourdomain.com/nitrpg`: + + ./web localhost 3000 "/nitrpg" + +Leave it empty if NitRPG is installed at the root of the domain: + + ./web localhost 3000 "" + +The webserver can then be accessed at `http://yourdomain.com:3000/nitrpg/`. + +## RoadMap + +NitRPG stills under heavy development. +Incomming features contain (but are not limited to): + +* Periodized stats (weekly, monthly, yearly, overall) +* Display graphs with stats +* More achievements +* Shop: exchange Nitcoins against glorifying items + +You can suggest new achievements or ideas in the +[NitRPG RoadMap Issue](https://github.com/privat/nit/issues/1161). diff --git a/contrib/nitrpg/src/achievements.nit b/contrib/nitrpg/src/achievements.nit index 625bfc2..eeae17e 100644 --- a/contrib/nitrpg/src/achievements.nit +++ b/contrib/nitrpg/src/achievements.nit @@ -400,3 +400,119 @@ class Player10KCommits 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.has_ok_review 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 diff --git a/contrib/nitrpg/src/game.nit b/contrib/nitrpg/src/game.nit index 4d62283..d1dff3e 100644 --- a/contrib/nitrpg/src/game.nit +++ b/contrib/nitrpg/src/game.nit @@ -292,6 +292,19 @@ end # utils +# Sort games by descending number of players. +# +# The first in the list is the game with the more players. +class GamePlayersComparator + super Comparator + + redef type COMPARED: Game + + redef fun compare(a, b) do + return b.load_players.length <=> a.load_players.length + end +end + # Sort players by descending number of nitcoins. # # The first in the list is the player with the more of nitcoins. diff --git a/contrib/nitrpg/src/listener.nit b/contrib/nitrpg/src/listener.nit index 0e4ba00..8bee99b 100644 --- a/contrib/nitrpg/src/listener.nit +++ b/contrib/nitrpg/src/listener.nit @@ -63,6 +63,8 @@ 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) +l.add_reactor(new Player1Comment, new Player100Comments, new Player1KComments) +l.add_reactor(new PlayerPingGod, new PlayerFirstReview, new PlayerSaysNitcoin) print "Listening events on {host}:{port}" l.listen diff --git a/contrib/nitrpg/src/statistics.nit b/contrib/nitrpg/src/statistics.nit index dfdb3ba..7b493c6 100644 --- a/contrib/nitrpg/src/statistics.nit +++ b/contrib/nitrpg/src/statistics.nit @@ -187,3 +187,27 @@ redef class PullRequestEvent end end end + +redef class IssueCommentEvent + + # Count posted comments + redef fun react_stats_event(game) do + if action == "created" then + var player = comment.user.player(game) + game.stats.inc("comments") + player.stats.inc("comments") + # FIXME use a more precise way to locate reviews + if comment.has_ok_review then + game.stats.inc("reviews") + player.stats.inc("reviews") + end + game.save + player.save + end + end +end + +redef class IssueComment + # Does this comment contain a "+1"? + fun has_ok_review: Bool do return body.has("\\+1\\b".to_re) +end diff --git a/contrib/nitrpg/src/templates/panels.nit b/contrib/nitrpg/src/templates/panels.nit index 14538c5..e4e7f8f 100644 --- a/contrib/nitrpg/src/templates/panels.nit +++ b/contrib/nitrpg/src/templates/panels.nit @@ -18,6 +18,7 @@ module panels import templates_events +import markdown # A panel can be displayed in a html page. # @@ -98,6 +99,86 @@ class ErrorPanel end +# A panel that display a markdown content rendered as HTML. +class MDPanel + super Panel + + # Markdown text to display. + var text: String + + redef fun rendering do + add """
+
{{{text.md_to_html}}}
+
""" + end +end + +# Display a list of active game. +# +# Used for NitRPG homepage. +class GamesShortListPanel + super Panel + + # Root url used for links. + var root_url: String + + # List of NitRPG games to display. + var games: Array[Game] + + redef fun render_title do + add "  " + add "Active games" + end + + redef fun render_body do + if games.is_empty then + add "No game yet..." + return + end + var sorted = games.to_a + (new GamePlayersComparator).sort(sorted) + for game in sorted do + add "{game.link} ({game.load_players.length} players)
" + end + end +end + +# A panel that display a list of player in a repo. +class GamesListPanel + super GamesShortListPanel + super TablePanel + + redef fun render_title do + add "  " + add "Active games" + end + + redef fun render_body do + if games.is_empty then + add "
" + add "No player yet..." + add "
" + return + end + var sorted = games.to_a + (new GamePlayersComparator).sort(sorted) + add """ + + + + + """ + for game in sorted do + add "" + add " " + add " " + add " " + add "" + end + add "
GamePlayersAchievements
{game.link}{game.load_players.length}{game.load_achievements.length}
" + end +end + # A panel that display repo statistics. class GameStatusPanel super Panel diff --git a/contrib/nitrpg/src/web.nit b/contrib/nitrpg/src/web.nit index b27c457..5f4992e 100644 --- a/contrib/nitrpg/src/web.nit +++ b/contrib/nitrpg/src/web.nit @@ -53,6 +53,79 @@ class RpgAction rsp.body = page.write_to_string return rsp end + + # Returns the game with `name` or null if no game exists with this name. + fun load_game(name: String): nullable Game do + var repo = api.load_repo(name) + if api.was_error or repo == null then return null + var game = new Game(api, repo) + game.root_url = root_url + return game + end + + # Returns the list of saved games from NitRPG data. + fun load_games: Array[Game] do + var res = new Array[Game] + var rpgdir = "nitrpg_data" + if not rpgdir.file_exists then return res + for user in rpgdir.files do + for repo in "{rpgdir}/{user}".files do + var game = load_game("{user}/{repo}") + if game != null then res.add game + end + end + return res + end +end + +# Repo overview page. +class RpgHome + super RpgAction + + # Response page stub. + var page: NitRpgPage is noinit + + redef fun answer(request, url) do + var readme = load_readme + var games = load_games + var response = new HttpResponse(200) + page = new NitRpgPage(root_url) + page.side_panels.add new GamesShortListPanel(root_url, games) + page.flow_panels.add new MDPanel(readme) + response.body = page.write_to_string + return response + end + + # Load the string content of the nitrpg readme file. + private fun load_readme: String do + var readme = "README.md" + if not readme.file_exists then + return "Unable to locate README file." + end + var file = new FileReader.open(readme) + var text = file.read_all + file.close + return text + end +end + +# Display the list of active game. +class ListGames + super RpgAction + + # Response page stub. + var page: NitRpgPage is noinit + + redef fun answer(request, url) do + var games = load_games + var response = new HttpResponse(200) + page = new NitRpgPage(root_url) + page.breadcrumbs = new Breadcrumbs + page.breadcrumbs.add_link(root_url / "games", "games") + page.flow_panels.add new GamesListPanel(root_url, games) + response.body = page.write_to_string + return response + end end # An action that require a game. @@ -72,16 +145,15 @@ class GameAction var owner = request.param("owner") var repo_name = request.param("repo") if owner == null or repo_name == null then - var msg = "Bad request: should look like /repos/:owner/:repo." + var msg = "Bad request: should look like /games/:owner/:repo." return bad_request(msg) end - var repo = new Repo(api, "{owner}/{repo_name}") - game = new Game(api, repo) - game.root_url = root_url - if api.was_error then + var game = load_game("{owner}/{repo_name}") + if game == null then var msg = api.last_error.message return bad_request("Repo Error: {msg}") end + self.game = game var response = new HttpResponse(200) page = new NitRpgPage(root_url) page.side_panels.add new GameStatusPanel(game) @@ -103,6 +175,11 @@ class GameAction # From where to start the display of events related lists. var list_from = 0 + + # TODO should also check 201, 203 ... + private fun is_response_error(response: HttpResponse): Bool do + return response.status_code != 200 + end end # Repo overview page. @@ -111,6 +188,7 @@ class RepoHome redef fun answer(request, url) do var rsp = prepare_response(request, url) + if is_response_error(rsp) then return rsp page.side_panels.add new ShortListPlayersPanel(game) page.flow_panels.add new PodiumPanel(game) page.flow_panels.add new EventListPanel(game, list_limit, list_from) @@ -126,6 +204,7 @@ class ListPlayers redef fun answer(request, url) do var rsp = prepare_response(request, url) + if is_response_error(rsp) then return rsp page.breadcrumbs.add_link(game.url / "players", "players") page.flow_panels.add new ListPlayersPanel(game) rsp.body = page.write_to_string @@ -139,6 +218,7 @@ class PlayerHome redef fun answer(request, url) do var rsp = prepare_response(request, url) + if is_response_error(rsp) then return rsp var name = request.param("player") if name == null then var msg = "Bad request: should look like /:owner/:repo/:players/:name." @@ -166,6 +246,7 @@ class ListAchievements redef fun answer(request, url) do var rsp = prepare_response(request, url) + if is_response_error(rsp) then return rsp page.breadcrumbs.add_link(game.url / "achievements", "achievements") page.flow_panels.add new AchievementsListPanel(game) rsp.body = page.write_to_string @@ -179,6 +260,7 @@ class AchievementHome redef fun answer(request, url) do var rsp = prepare_response(request, url) + if is_response_error(rsp) then return rsp var name = request.param("achievement") if name == null then var msg = "Bad request: should look like /:owner/:repo/achievements/:achievement." @@ -217,6 +299,8 @@ vh.routes.add new Route("/games/:owner/:repo/players", new ListPlayers(root)) vh.routes.add new Route("/games/:owner/:repo/achievements/:achievement", new AchievementHome(root)) vh.routes.add new Route("/games/:owner/:repo/achievements", new ListAchievements(root)) vh.routes.add new Route("/games/:owner/:repo", new RepoHome(root)) +vh.routes.add new Route("/games", new ListGames(root)) +vh.routes.add new Route("/", new RpgHome(root)) var fac = new HttpFactory.and_libevent fac.config.virtual_hosts.add vh