Skip to content

Instantly share code, notes, and snippets.

@masak
Last active December 15, 2015 09:19
Show Gist options
  • Save masak/5237570 to your computer and use it in GitHub Desktop.
Save masak/5237570 to your computer and use it in GitHub Desktop.
An idea for a web based Perl 6 implementation of Nomic
enum State <Ongoing Finished>;
enum Role <Voter Proposer>;
enum Choice <Nay Aye>;
enum Result <Lost Won>;
class Vote is rw {
has @.registered_votes;
has State $.state;
has Result $.result;
method register(Choice $choice) { ... }
method count(Choice $choice? --> Int) { ... }
method was_registered_by(Player $player --> Bool) { ... }
method choice_of(Player $player --> Bool) { ... }
}
class Patch is rw {
}
class Proposal is rw {
has Patch $.patch;
}
class Player is rw {
has Int $.score;
has Role $.role;
has Int $.proposals_made;
has Int $.votes_made;
has Int $.consecutives;
has Int $.dissents;
method join() { ... }
method leave() { ... }
method propose(Proposal $proposal) { ... }
method vote(Choice $choice) { ... }
}
class Turn is rw {
has State $.state;
has Player $.proposer;
has Proposal $.proposal;
has Vote $.vote;
}
class Rules {
has Turn $.turn is rw;
has Player @.voters;
method setup {
# A turn takes a week. Every turn lasts between one Tuesday 12:00
# and the next. UTC.
every('Tuesday').at(12, 00).do: { #OK
$.turn.state = Finished;
$.turn = Turn.new();
$.turn.state = Ongoing;
# At each turn, the next player is chosen out of a circular queue
# of eligible voters to be the proposer.
$.turn.proposer = @.voters.shift;
$.turn.proposer.role = Proposer;
$.turn.proposer.proposals_made = 0;
}
# All the players except the proposer are voters.
whenever.a(Player).does('join').do: -> $player {
$player.role = Voter;
}
whenever.a(Turn).changes('state').to(Finished).do: {
my $player = $.turn.proposer;
$player.role = Voter;
@.voters.push($player);
}
# The proposer (and only the proposer) can make one (and only one)
# patch proposal to the game per turn.
enable.a(Player).to('propose').on_condition: { $^player === $.turn.proposer }
enable.a(Player).to('propose').on_condition: { $^player.proposals_made == 0 }
whenever.a(Player).does('propose').do: -> $player, $proposal {
$player.proposals_made($player.proposals_made() + 1);
$.turn.proposal = $proposal;
$.turn.vote = Vote.new();
$.turn.vote.status = Ongoing;
}
# Each eligible voter (except the proposer) can lay exactly one vote for a proposed patch.
enable.a(Player).to('vote').on_condition: { $^player ~~ any @.voters }
enable.a(Player).to('vote').on_condition: { $^player.votes_made == 0 }
# A vote can be "aye" or "nay".
whenever.a(Player).does('vote').do: -> $player, $choice {
$player.votes_made++;
return unless $choice == Aye || $choice == Nay;
$.turn.vote.register($player, $choice);
}
# Voting finishes either the instant everyone has voted, or by the end
# of the turn, whichever comes first.
whenever.a(Vote).with(&all_but_one_voted).does('register').do: &finish_vote;
whenever.a(Turn).with({ defined .vote }).changes('state').to(Finished).do:
{ finish_vote($^turn.vote) };
sub all_but_one_voted($vote) { $vote.count == @.voters - 1 }
sub finish_vote($vote) { $vote.state = Finished }
# If at least half of the registered votes were "aye", the vote is winning.
# Otherwise, the vote is losing.
whenever.a(Vote).changes('state').to(Finished).do: {
$^vote.result(aye_majority($vote) ?? Won !! Lost);
}
sub aye_majority($vote) { $vote.count(Aye) >= $vote.count() / 2 }
# For a winning vote, the patch is applied that is, the rules of this game change
# according to the description in the patch.
# A patch is applied immediately upon voting win, not by the end of the turn.
whenever.a(Vote).changes('result').to(Won).do: {
Game::Mechanics.apply($.turn.proposal.patch);
}
# The player to first reach 200 (or more) points wins. When a winner has been
# thus declared, the game ends.
constant WIN_LIMIT = 200;
whenever.a(Player).s('score').reaches(WIN_LIMIT).do: -> $player {
Game::Mechanics.declare_winner($player);
Game::Mechanics.end_game();
}
# A new player starts with 0 points.
# Players can join or leave any time. If they re-join, they start with 0 points.
enable.a(Player).to('join').always;
enable.a(Player).to('leave').always;
whenever.a(Player).does('join').do: -> $player {
$player.score = 0;
}
# A joining player is put at the end of the circular turn queue. A leaving player
# is taken out of the queue.
whenever.a(Player).does('join').do: -> $player {
@.voters.push($player);
}
whenever.a(Player).does('leave').do: -> $player {
@.voters.=grep({ $_ !== $player });
}
# Failing to make a patch proposal during one's turn deducts 50 points
# from a player.
constant NO_PROPOSAL_PENALTY = 50;
whenever.a(Turn).without({ defined .proposal }).changes('state').to(Finished).do: {
$.turn.proposer.score -= NO_PROPOSAL_PENALTY;
}
# If the voting has started, failing to vote during a turn
# deducts 20 points from a player.
constant NO_VOTE_PENALTY = 20;
whenever.a(Vote).changes('state').to(Finished).do: -> $vote {
for @.voters -> $player {
unless $vote.was_registered_by($player) {
$player.score -= NO_VOTE_PENALTY;
}
}
}
# An accepted patch adds 7 points to the proposer's score.
constant ACCEPTED_PROPOSAL_BONUS = 7;
whenever.a(Vote).changes('result').to(Won).do: {
my $proposer = $.turn.proposer;
$proposer.score += ACCEPTED_PROPOSAL_BONUS;
}
# A rejected patch deducts 2 points from the proposer's score.
constant REJECTED_PROPOSAL_MALUS = 2;
whenever.a(Vote).changes('result').to(Lost).do: {
my $proposer = $.turn.proposer;
$proposer.score -= REJECTED_PROPOSAL_MALUS;
}
# Voting for 10 consecutive turns gives a 300-point bonus to a player.
# This is a one-off bonus, and is only handed out once per player.
constant REWARDED_CONSECUTIVE_VOTINGS = 10;
constant VOTING_PARTICIPATION_BONUS = 300;
whenever.a(Vote).changes('state').to(Finished).do: -> $vote {
for @.voters -> $player {
if $vote.was_registered_by($player) {
$player.consecutives++;
}
else {
$player.consecutives = 0;
}
}
}
whenever.a(Player).s('consecutives').reaches(REWARDED_CONSECUTIVE_VOTINGS).do: -> $player {
$player.score += VOTING_PARTICIPATION_BONUS;
}
# If a player accumulates 5 votes where the player voted against the majority ("nay" to a winning
# vote, or "aye" to a losing vote), 150 points are deducted. This is a recurring malus, reset for
# the player each time it happens.
constant DISSENT_LIMIT = 5;
constant ACCUMULATED_DISSENT_MALUS = 150;
whenever.a(Vote).changes('result').to(Won).do: $increment_dissenters;
whenever.a(Vote).changes('result').to(Lost).do: $increment_dissenters;
sub increment_dissenters(Vote $vote) {
my $result = $vote.result();
for @.voters -> $player {
if $vote.choice_of($player) !== $result {
$player.dissents++;
}
}
}
whenever.a(Player).s('dissents').reaches(DISSENT_LIMIT).do: -> $player {
$player.score -= ACCUMULATED_DISSENT_MALUS;
$player.dissents = 0;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment