Last active
December 15, 2015 09:19
-
-
Save masak/5237570 to your computer and use it in GitHub Desktop.
An idea for a web based Perl 6 implementation of Nomic
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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