monkeymusic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'hallon'
4
+ gem 'em-websocket'
5
+ gem 'json'
6
+ gem 'rack'
data/Gemfile.lock ADDED
@@ -0,0 +1,31 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ em-websocket (0.5.0)
5
+ eventmachine (>= 0.12.9)
6
+ http_parser.rb (~> 0.5.3)
7
+ eventmachine (1.0.3)
8
+ ffi (1.8.1)
9
+ hallon (0.18.2)
10
+ ffi (~> 1.8)
11
+ spotify (~> 12.5.1)
12
+ weak_observable (~> 1.0)
13
+ http_parser.rb (0.5.3)
14
+ json (1.7.7)
15
+ libspotify (12.1.51.3)
16
+ rack (1.5.2)
17
+ ref (1.0.4)
18
+ spotify (12.5.1)
19
+ ffi (~> 1.0, >= 1.0.11)
20
+ libspotify (~> 12.1.51)
21
+ weak_observable (1.0.1)
22
+ ref (~> 1.0.0)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ em-websocket
29
+ hallon
30
+ json
31
+ rack
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2010 Ryan Bates
2
+ 2013 Oscar Söderlund
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,38 @@
1
+ Monkey Music
2
+ ============
3
+
4
+ This is the runtime for a programming contest, it's a work in progress.
5
+
6
+ To test-drive:
7
+
8
+ ./bin/monkeymusic --user users/poscar.yaml --level levels/testlevel.rb --player-file p1 --player-name p1 --player-file p1 --player-name p2
9
+
10
+ Rules
11
+ -----
12
+
13
+ ### Which monkey gets to move first?
14
+
15
+ Every round, all monkeys are put into a Fisher-Yates shuffled array, and
16
+ the monkeys will be allowed to move in order of increasing index.
17
+
18
+ Input
19
+ -----
20
+
21
+ One line with one positive integer, id, that identifies your monkey.
22
+ One line with two positive integers, w, h. The width and the
23
+ height of the level.
24
+ h lines with w comma-separated strings each. Each string represents a
25
+ space in the level.
26
+
27
+ A space can contain the following:
28
+
29
+ A monkey, on the form M#id, the letter M followed by a hash followed by the id of a monkey.
30
+ A basket, on the form B#id, the letter B followed by a hash followed by
31
+ A track URI, on the form spotify:track:XXXXXXXXXXXXXXX.
32
+ the id of the monkey the basket belongs to.
33
+ A palm tree, on the form P.
34
+
35
+ Output
36
+ ------
37
+
38
+ One of the characters ['N','W','E','S'] followed by a line-break '\n'.
data/bin/monkeymusic ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../lib/monkey_music'
3
+
4
+ runner = MonkeyMusic::Runner.new(ARGV)
5
+ runner.run
@@ -0,0 +1,28 @@
1
+ carrying_capacity 3
2
+ metadata_requests_per_turn 2
3
+ max_turns 100
4
+
5
+ legend({
6
+ "1" => Monkey.player(1),
7
+ "2" => Monkey.player(2),
8
+ "X" => Track.worth(-2),
9
+ "x" => Track.worth(-1),
10
+ "l" => Track.worth(1),
11
+ "t" => Track.worth(2),
12
+ "T" => Track.worth(3),
13
+ "U" => User,
14
+ })
15
+
16
+ layout <<LAYOUT
17
+ ...........
18
+ ..x........
19
+ ...........
20
+ .......T...
21
+ 1..........
22
+ ...t......U
23
+ 2..........
24
+ .......l...
25
+ ...........
26
+ ..X........
27
+ ...........
28
+ LAYOUT
@@ -0,0 +1,24 @@
1
+ width 25
2
+ height 11
3
+ max_turns 200
4
+ carrying_capacity 100
5
+
6
+ legend({
7
+ "1" => Monkey,
8
+ "!" => Track,
9
+ "#" => Wall,
10
+ })
11
+
12
+ layout <<LAYOUT
13
+ #########################
14
+ #! # # !#
15
+ ### ###### # #######
16
+ # # # #
17
+ # ### ###### # #
18
+ # #! # # # #
19
+ # # # ### # # #
20
+ # # # # #1 # #
21
+ # ###### ########## #
22
+ # !# #
23
+ #########################
24
+ LAYOUT
@@ -0,0 +1,21 @@
1
+ # In this level, the monkeys ability to navigate a maze and pick
2
+ # the best out of many tracks is tested. Do not choose poorly...
3
+
4
+ width 20
5
+ height 10
6
+ capacity 1
7
+
8
+ legend({
9
+ "T" => Tube,
10
+ "M" => Monkey,
11
+ "s" => Track,
12
+ "#" => Wall,
13
+ })
14
+
15
+ layout <<EOS
16
+ T..#.....#.....#...s
17
+ ...#..#..#..#..#...s
18
+ .M.#..#..#..#..#...s
19
+ ...#..#..#..#..#...s
20
+ ......#.....#......s
21
+ EOS
File without changes
@@ -0,0 +1,27 @@
1
+ require 'optparse'
2
+
3
+ module MonkeyMusic
4
+ class Game
5
+
6
+ def initialize(level, players, ui)
7
+ @level = level
8
+ @players = players
9
+ @ui = ui
10
+ end
11
+
12
+ def start
13
+ @level.max_turns.times do
14
+ if @level.complete?
15
+ break
16
+ end
17
+ @ui.update(@level)
18
+ # Query players for moves
19
+ @players.each { |p| p.query_move! }
20
+ # Move players in random order
21
+ @players.shuffle.each { |p| p.move! }
22
+ end
23
+ @ui.update(@level)
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,91 @@
1
+ require 'json'
2
+
3
+ module MonkeyMusic
4
+ class Level
5
+ attr_accessor :width, :height, :max_turns
6
+ attr_reader :players
7
+ attr_reader :user
8
+
9
+ def initialize(players, user)
10
+ @players = players
11
+ @user = user
12
+ @width = 0
13
+ @height = 0
14
+ @units = []
15
+ end
16
+
17
+ def add(unit, x, y)
18
+ unit.place!(self, x, y)
19
+ unit.assign_id
20
+ @units << unit
21
+ end
22
+
23
+ def at(x, y)
24
+ @units.detect { |u| u.at?(x, y) }
25
+ end
26
+
27
+ def empty?(x, y)
28
+ at(x, y).nil?
29
+ end
30
+
31
+ def complete?
32
+ (@units.detect { |u| u.kind_of? Track }).nil? &&
33
+ (@players.detect { |p| p.monkey.carrying.count > 0 }).nil?
34
+ end
35
+
36
+ def remove(unit)
37
+ @units.reject! { |u| u == unit }
38
+ end
39
+
40
+ def out_of_bounds?(x, y)
41
+ x < 0 || y < 0 || x > @width-1 || y > @height-1
42
+ end
43
+
44
+ def accessible?(x, y)
45
+ not out_of_bounds?(x, y) && empty?(x, y)
46
+ end
47
+
48
+ def load_from_file(file)
49
+ LevelLoader.new(self).instance_eval(IO.read(file))
50
+ self
51
+ end
52
+
53
+ def to_s
54
+ rows = []
55
+ rows << " " + ("=" * @width)
56
+ @height.times do |y|
57
+ row = ["|"]
58
+ @width.times do |x|
59
+ unit = at(x, y)
60
+ row << if unit then unit.to_s else ' ' end
61
+ end
62
+ row << "|"
63
+ rows << row.join
64
+ end
65
+ rows << " " + ("=" * @width)
66
+ rows.join("\n")
67
+ end
68
+
69
+ def serialize
70
+ rows = []
71
+ @height.times do |y|
72
+ row = []
73
+ @width.times do |x|
74
+ unit = at(x, y)
75
+ row << if unit then unit.serialize else '_' end
76
+ end
77
+ rows << row.join(",")
78
+ end
79
+ rows.join("\n")
80
+ end
81
+
82
+ def as_json
83
+ {
84
+ :width => @width,
85
+ :height => @height,
86
+ :units => @units,
87
+ }.to_json
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,64 @@
1
+ module MonkeyMusic
2
+ #
3
+ # LevelLoader defines a DSL for building levels in a ruby file
4
+ #
5
+ class LevelLoader
6
+ def initialize(level)
7
+ @level = level
8
+ @legend = {}
9
+ end
10
+
11
+ def max_turns(max_turns)
12
+ @level.max_turns = max_turns
13
+ end
14
+
15
+ def metadata_requests_per_turn(n)
16
+ @metadata_requests_per_turn = n
17
+ end
18
+
19
+ def carrying_capacity(capacity)
20
+ @level.players.each { |p| p.monkey.capacity = capacity }
21
+ end
22
+
23
+ def legend(legend)
24
+ @legend = legend
25
+ end
26
+
27
+ def layout(layout)
28
+ # Transform layout into x y indexed array
29
+ units = (layout.lines.map { |l| l.chomp.split(//) }).transpose
30
+ @level.width = units[0].length
31
+ @level.height = units.length
32
+ # Add units from layout to level
33
+ @level.height.times do |y|
34
+ @level.width.times do |x|
35
+ curr_character = units[x][y]
36
+ unit = parse_character(curr_character)
37
+ @level.add(unit, x, y) if unit
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def parse_character(character)
45
+ klass = @legend[character]
46
+ if klass
47
+ unit = if (klass <= Monkey)
48
+ klass.from_players(@level.players)
49
+ elsif klass <= Track
50
+ klass.from_user(@level.user)
51
+ elsif klass == User
52
+ @level.user
53
+ else
54
+ klass.new
55
+ end
56
+ if unit
57
+ unit.character = character
58
+ unit
59
+ end
60
+ end
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,41 @@
1
+ module MonkeyMusic
2
+ class Player
3
+ attr_accessor :monkey
4
+
5
+ def initialize(file)
6
+ @file = file
7
+ @monkey = Monkey.new()
8
+ end
9
+
10
+ def query_move!
11
+ IO.popen(@file, "r+") do |io|
12
+ io.puts @monkey.serialize
13
+ io.puts @monkey.remaining_capacity
14
+ io.puts @monkey.level.width
15
+ io.puts @monkey.level.height
16
+ io.puts @monkey.level.serialize
17
+ move = io.gets
18
+ @next_move = parse_move(move) if move
19
+ end
20
+ end
21
+
22
+ def parse_move(s)
23
+ case s.chomp
24
+ when "N" then :north
25
+ when "W" then :west
26
+ when "E" then :east
27
+ when "S" then :south
28
+ end
29
+ end
30
+
31
+ def move!
32
+ @monkey.move! @next_move if @next_move
33
+ @next_move = nil
34
+ end
35
+
36
+ def to_s
37
+ "#{@monkey.name}: #{@file}"
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,91 @@
1
+ require 'hallon'
2
+
3
+ module MonkeyMusic
4
+ class RecommendationLoader
5
+
6
+ def initialize(loaded_toplists, load_factor = 5)
7
+ @loaded_toplists = loaded_toplists
8
+ @load_factor = load_factor
9
+ end
10
+
11
+ def load_for_user!(user)
12
+ @user = user
13
+ puts "Loading recommendations from album toplist..."
14
+ load_recommendations_from_albums!
15
+ puts "Loading recommendations from artist toplist..."
16
+ load_recommendations_from_artists!
17
+ puts "Loading recommendations from track toplist albums..."
18
+ load_recommendations_from_top_track_albums!
19
+ puts "Loading recommendations from track toplist..."
20
+ load_recommendations_from_already_heard!
21
+ puts "Loading recommendations from disliked artists..."
22
+ load_recommendations_from_disliked!
23
+ end
24
+
25
+ private
26
+
27
+ def load_recommendations_from_albums!
28
+ @loaded_toplists[:top_albums].each do |album|
29
+ puts "Browsing #{album.name}..."
30
+ browse = album.browse
31
+ browse.load unless browse.loaded?
32
+ browse.tracks.first(@load_factor).each do |rec|
33
+ @user.recommendations << parse_track(rec)
34
+ end
35
+ end
36
+ end
37
+
38
+ def load_recommendations_from_artists!
39
+ @loaded_toplists[:top_artists].each do |artist|
40
+ puts "Browsing #{artist.name}..."
41
+ browse = artist.browse(:no_albums)
42
+ browse.load unless browse.loaded?
43
+ browse.top_hits.first(@load_factor).each do |rec|
44
+ @user.recommendations << parse_track(rec)
45
+ end
46
+ end
47
+ end
48
+
49
+ def load_recommendations_from_top_track_albums!
50
+ @loaded_toplists[:top_track_albums].each do |album|
51
+ puts "Browsing #{album.name}..."
52
+ browse = album.browse
53
+ browse.load unless browse.loaded?
54
+ browse.tracks.first(@load_factor).each do |rec|
55
+ @user.recommendations << parse_track(rec)
56
+ end
57
+ end
58
+ end
59
+
60
+ def load_recommendations_from_already_heard!
61
+ @loaded_toplists[:top_tracks].each do |track|
62
+ @user.recommendations << parse_track(track)
63
+ end
64
+ end
65
+
66
+ def load_recommendations_from_disliked!
67
+ @loaded_toplists[:disliked].each do |artist|
68
+ puts "Browsing #{artist.name}..."
69
+ browse = artist.browse(:no_albums)
70
+ browse.load unless browse.loaded?
71
+ browse.top_hits.first(@load_factor).each do |rec|
72
+ @user.recommendations << parse_track(rec)
73
+ end
74
+ end
75
+ end
76
+
77
+ def parse_track(track)
78
+ album = track.album.load
79
+ artist = track.artist.load
80
+ user_track = Track.new
81
+ user_track.uri = track.to_link.to_str
82
+ user_track.name = track.name
83
+ user_track.artist = artist.name
84
+ user_track.album = album.name
85
+ user_track.popularity = track.popularity
86
+ user_track.year = album.release_year
87
+ user_track
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,169 @@
1
+ require 'optparse'
2
+
3
+ module MonkeyMusic
4
+ class Runner
5
+
6
+ def initialize(arguments)
7
+ @arguments = arguments
8
+ @opt_parser = OptionParser.new
9
+ @players = []
10
+ @delay = 1
11
+ init_parser(@opt_parser)
12
+ end
13
+
14
+ def run
15
+ @opt_parser.parse!
16
+ if generate_user?
17
+ # Create user
18
+ user = User.new
19
+ # Connect to libspotify
20
+ Hallon.load_timeout = 0
21
+ session = Hallon::Session.initialize(IO.read(@spotify_appkey_file))
22
+ session.login!(@spotify_account, @spotify_password)
23
+ # Load toplists
24
+ puts "Loading toplists from #{@toplist_file}..."
25
+ toplist_loader = ToplistLoader.new(@toplist_file)
26
+ toplist_loader.load_for_user!(user)
27
+ # Generate recommendations
28
+ puts "Loading recommendations..."
29
+ loaded_toplists = toplist_loader.loaded_toplists
30
+ recommendation_loader = RecommendationLoader.new(loaded_toplists)
31
+ recommendation_loader.load_for_user!(user)
32
+ # Disconnect from libspotify
33
+ session.logout!
34
+ # Evaluate recommendations
35
+ puts "Evaluating recommendations..."
36
+ score_system = ScoreSystem.new
37
+ score_system.evaluate_user_recommendations!(user)
38
+ # Dump and print the user
39
+ File.open(@out_file, 'w') do |f|
40
+ f.write(user.dump)
41
+ end
42
+ puts "====="
43
+ puts "DONE!"
44
+ puts "====="
45
+ puts "Loaded tracks:"
46
+ user.recommendations.group_by(&:multiplier).sort.each do |k,v|
47
+ puts "#Multiplier #{k}:\t#{v.length}"
48
+ end
49
+ exit
50
+ elsif not game_is_playable?
51
+ puts @opt_parser
52
+ exit
53
+ end
54
+ # Load user
55
+ user = User.new
56
+ user.load_from_file(@user_file)
57
+ # Load level
58
+ level = Level.new(@players, user)
59
+ level.load_from_file(@level_file)
60
+ ## Initialize UI
61
+ if browser_ui?
62
+ print "Using browser UI. Press the enter key to start game. "
63
+ gets
64
+ puts "Starting game..."
65
+ ui = BrowserUI.new(@delay)
66
+ else
67
+ ui = ConsoleUI.new(@delay)
68
+ end
69
+ ## Start game
70
+ @game = Game.new(level, @players, ui)
71
+ @game.start
72
+ end
73
+
74
+ private
75
+
76
+ def generate_user?
77
+ (defined? @toplist_file) &&
78
+ (defined? @out_file) &&
79
+ (defined? @spotify_appkey_file) &&
80
+ (defined? @spotify_account) &&
81
+ (defined? @spotify_password)
82
+ end
83
+
84
+ def game_is_playable?
85
+ (defined? @user_file) &&
86
+ (defined? @level_file) &&
87
+ (not @players.empty?)
88
+ end
89
+
90
+ def browser_ui?
91
+ @browser_ui == true
92
+ end
93
+
94
+ def init_parser(opts)
95
+ opts.banner = 'Usage: monkeymusic [options]'
96
+
97
+ opts.on('-l',
98
+ '--level LEVEL',
99
+ 'The level to play.') do |file|
100
+ @level_file = File.join(Dir.getwd, file)
101
+ end
102
+
103
+ opts.on('-u',
104
+ '--user USER',
105
+ 'The user the players will recommend music for.') do |user|
106
+ @user_file = File.join(Dir.getwd, user)
107
+ end
108
+
109
+ opts.on('-p',
110
+ '--player FILE',
111
+ 'The path to a player program.') do |file|
112
+ player_file = File.join(Dir.getwd, file)
113
+ @players << Player.new(player_file)
114
+ end
115
+
116
+ opts.on('-n',
117
+ '--player-name NAME',
118
+ 'Set the name of the last entered player.') do |name|
119
+ @players[-1].monkey.name = name unless @players.empty?
120
+ end
121
+
122
+ opts.on('-d', '--delay DELAY', OptionParser::DecimalNumeric,
123
+ 'The delay (in seconds) between each round.') do |delay|
124
+ @delay = delay
125
+ end
126
+
127
+ opts.on('-g',
128
+ '--generate TOPLIST_FILE',
129
+ 'Generate a user from a toplist file.') do |user|
130
+ @toplist_file = File.join(Dir.getwd, user)
131
+ end
132
+
133
+ opts.on('-o',
134
+ '--out OUT_FILE',
135
+ 'The file to dump a generated user to.') do |file|
136
+ @out_file = File.join(Dir.getwd, file)
137
+ end
138
+
139
+ opts.on('-k',
140
+ '--app-key KEY',
141
+ 'Path to libspotify application key.') do |key|
142
+ @spotify_appkey_file = File.join(Dir.getwd, key)
143
+ end
144
+
145
+ opts.on('-a',
146
+ '--account ACCOUNT',
147
+ 'Username for a Spotify premium account.') do |account|
148
+ @spotify_account = account
149
+ end
150
+
151
+ opts.on('-w', '--password PASSWORD',
152
+ 'Password for a Spotify premium account.') do |password|
153
+ @spotify_password = password
154
+ end
155
+
156
+ opts.on('-b', '--browser-ui',
157
+ 'View the game through the browser instead of the console.') do |password|
158
+ @browser_ui = true
159
+ end
160
+
161
+ opts.on_tail('-h',
162
+ '--help',
163
+ 'Show this message.') do
164
+ puts opts
165
+ exit
166
+ end
167
+ end
168
+ end
169
+ end