shattered_model 0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/base.rb +22 -0
- data/lib/fuzzy_logic.rb +188 -0
- data/lib/linear_interpolator.rb +29 -0
- data/lib/shattered_model.rb +11 -0
- data/lib/shattered_model_tester.rb +2 -0
- metadata +40 -0
data/lib/base.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
include ShatteredSupport
|
|
2
|
+
|
|
3
|
+
module ShatteredModel
|
|
4
|
+
def self.append_features(base)
|
|
5
|
+
super
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def fuzzy_logic( file )
|
|
10
|
+
before_init_set("self", {:fuzzy_logic => [file]})
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
class Base < ShatteredSupport::Base
|
|
14
|
+
def fuzzy_logic=(file)
|
|
15
|
+
@fuzzy_logic = FuzzyLogic.new
|
|
16
|
+
@fuzzy_logic.parse_fuzzy_file(File.dirname(__FILE__)+"/#{file}")
|
|
17
|
+
end
|
|
18
|
+
def update_fuzzy_logic
|
|
19
|
+
@fuzzy_logic.update(self)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/fuzzy_logic.rb
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
# This provides basic fuzzy logic. That is, it will tell you what properties any given
|
|
3
|
+
# attribute contains based upon previous definition.
|
|
4
|
+
#
|
|
5
|
+
# Example:
|
|
6
|
+
# player_logic.add_classification( :hp, :dieing, :hp, :max => 5, :min => 45 )
|
|
7
|
+
# player_logic.add_classification( :hp, :healthy, :max => 80, :min => 65 )
|
|
8
|
+
# player_logic.hp(15) => [[:dieing, 0.75], [:healthy, 0.0]]
|
|
9
|
+
class FuzzyLogic
|
|
10
|
+
attr_reader :consequences
|
|
11
|
+
def initialize
|
|
12
|
+
@attributes = {}
|
|
13
|
+
@consequences = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add_consequence( consequence, classifications={} )
|
|
17
|
+
@consequences[classifications] ||= []
|
|
18
|
+
@consequences[classifications] += [consequence]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add_classification( attribute, classification, options = {} )
|
|
22
|
+
define_attribute(attribute) if(@attributes[attribute].nil?)
|
|
23
|
+
define_interpolator(attribute,classification,options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def consequences_of(options={})
|
|
27
|
+
# transform the passed in attributes to probability sets
|
|
28
|
+
valid_sequences = []
|
|
29
|
+
options.each_pair do |attribute, value|
|
|
30
|
+
valid_sequences << {attribute => classifications_of(attribute,value)}
|
|
31
|
+
end
|
|
32
|
+
# combine the probability sets
|
|
33
|
+
valid_combinations = compile_consequence_combinations(valid_sequences)
|
|
34
|
+
# realize the consequences of the combined probability sets
|
|
35
|
+
return consequences_of_combinations(valid_combinations)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_fuzzy_file( file_name )
|
|
39
|
+
file_name += ".yaml" if not(file_name =~ /\.yaml$/)
|
|
40
|
+
raise ArgumentError, "Cannot find #{file_name}" if not(File.file? file_name)
|
|
41
|
+
begin
|
|
42
|
+
fuzzy_file = YAML::load(File.open(file_name))
|
|
43
|
+
rescue ArgumentError => bang
|
|
44
|
+
raise ArgumentError, "Error when loading #{file_name} : #{bang.message}"
|
|
45
|
+
end
|
|
46
|
+
parse_attributes(fuzzy_file)
|
|
47
|
+
add_consequences_from_parsed(parse_consequences(fuzzy_file['consequences']))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The algorithm always performs an action on update.
|
|
51
|
+
def update(object)
|
|
52
|
+
status = {}
|
|
53
|
+
@attributes.each_pair do |attribute, classifications|
|
|
54
|
+
status.merge!({attribute => object.send(attribute)})
|
|
55
|
+
end
|
|
56
|
+
consequences = consequences_of(status)
|
|
57
|
+
total_probability = 0
|
|
58
|
+
consequences.each_pair do |actions, probability|
|
|
59
|
+
total_probability+=probability
|
|
60
|
+
end
|
|
61
|
+
action_at_probability = rand*total_probability
|
|
62
|
+
total_probability = 0
|
|
63
|
+
consequences.each_pair do |actions, probability|
|
|
64
|
+
total_probability += probability
|
|
65
|
+
if( action_at_probability < total_probability )
|
|
66
|
+
actions.each do |action|
|
|
67
|
+
object.send(action)
|
|
68
|
+
end
|
|
69
|
+
break
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# This function recursively moves through each valid combination of probabilities.
|
|
77
|
+
# It sums the probabilities by multiplication.
|
|
78
|
+
def compile_consequence_combinations(valid_sequences, compiled={}, probability=1.0)
|
|
79
|
+
retv = []
|
|
80
|
+
attribute = valid_sequences[0].keys[0]
|
|
81
|
+
classifications = valid_sequences[0][attribute]
|
|
82
|
+
classifications.each do |classification, chance|
|
|
83
|
+
next if chance == 0
|
|
84
|
+
nprobability=probability*chance
|
|
85
|
+
compiled[attribute]=classification
|
|
86
|
+
if(valid_sequences.length == 1)
|
|
87
|
+
retv << [compiled.dup, nprobability]
|
|
88
|
+
else
|
|
89
|
+
retv += compile_consequence_combinations(valid_sequences[1..-1],compiled,nprobability)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
return retv
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def consequences_of_combinations(combinations)
|
|
96
|
+
consequences = {}
|
|
97
|
+
combinations.each do |combination, probability|
|
|
98
|
+
# where a = { {1 => 2}, 3 } and b = {1,2}, a[b] != 3
|
|
99
|
+
# in order to circumvent this (error?) we use the fact that
|
|
100
|
+
# a.keys[0] == b
|
|
101
|
+
@consequences.keys.each do |index|
|
|
102
|
+
next if index != combination
|
|
103
|
+
consequences[@consequences[index]] ||= 0
|
|
104
|
+
consequences[@consequences[index]]+=probability
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
return consequences
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parse_attributes(yaml)
|
|
111
|
+
yaml['attributes'].each_pair do |attribute, classifications|
|
|
112
|
+
classifications.each_pair do |classification, options|
|
|
113
|
+
options.keys.each do |key|
|
|
114
|
+
options[key.to_sym]=options.delete key
|
|
115
|
+
end
|
|
116
|
+
add_classification( attribute.to_sym, classification.to_sym, options )
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def parse_consequences(consequences,result={})
|
|
122
|
+
compilation = []
|
|
123
|
+
consequences.each_pair do |consequence, action|
|
|
124
|
+
nresult = result.dup
|
|
125
|
+
nresult.merge!(attribute_for(consequence.to_sym) => consequence.to_sym)
|
|
126
|
+
if action.is_a? String
|
|
127
|
+
compilation += [nresult => action.to_sym]
|
|
128
|
+
else
|
|
129
|
+
compilation+=parse_consequences(action,nresult)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
return compilation
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def add_consequences_from_parsed(compilation)
|
|
136
|
+
compilation.each do |item|
|
|
137
|
+
item.each_pair do |index, action|
|
|
138
|
+
add_consequence(action, index)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def attribute_for(unknown_classification)
|
|
144
|
+
@attributes.each_pair do |attribute, classifications|
|
|
145
|
+
classifications.keys.each do |classification|
|
|
146
|
+
return attribute if classification == unknown_classification
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
return nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def define_attribute(attribute)
|
|
153
|
+
@attributes[attribute] = {}
|
|
154
|
+
eval <<-EOF
|
|
155
|
+
class << self
|
|
156
|
+
define_method(:#{attribute}) do |at|
|
|
157
|
+
classifications_of(:#{attribute},at)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
EOF
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def define_interpolator( attribute, classification,options )
|
|
164
|
+
interpolator = LinearInterpolator.new
|
|
165
|
+
@attributes[attribute][classification] = interpolator
|
|
166
|
+
|
|
167
|
+
add_point(interpolator, options[:max],1)
|
|
168
|
+
add_point(interpolator, options[:min],0)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def classifications_of( attribute, at )
|
|
172
|
+
retv = []
|
|
173
|
+
@attributes[attribute].each_pair do |classification,interpolator|
|
|
174
|
+
retv << [ classification, interpolator.value_at(at) ]
|
|
175
|
+
end
|
|
176
|
+
return retv.sort_by { |classification| (1.0-classification[1]) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def add_point( interpolator, points, value )
|
|
180
|
+
if(points.is_a? Array)
|
|
181
|
+
points.each do |x|
|
|
182
|
+
interpolator.add_point(x,value)
|
|
183
|
+
end
|
|
184
|
+
else
|
|
185
|
+
interpolator.add_point(points,value)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# This provides basic linear interpolation between any number of arbitrarily defined points.
|
|
2
|
+
# It is used for fuzzy logic. Although, fuzzy logic based on a statistically normal curve
|
|
3
|
+
# provides best results, linear interpolation provides a "gud 'nuf" approach.
|
|
4
|
+
class LinearInterpolator
|
|
5
|
+
def add_point(x,y)
|
|
6
|
+
@points ||= []
|
|
7
|
+
@points << [x,y]
|
|
8
|
+
@points = @points.sort_by { |x| x[0] }
|
|
9
|
+
end
|
|
10
|
+
def value_at(x)
|
|
11
|
+
return 0 if @points.nil?
|
|
12
|
+
return @points[0][1] if x < @points[0][0]
|
|
13
|
+
return @points[-1][1] if x > @points[-1][0]
|
|
14
|
+
|
|
15
|
+
after,before=nil,nil
|
|
16
|
+
@points.each_with_index do |point,i|
|
|
17
|
+
if(point[0] >= x)
|
|
18
|
+
after = point
|
|
19
|
+
before = @points[i-1]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
m=(before[1]-after[1])/(before[0]-after[0]).to_f
|
|
24
|
+
x-=before[0]
|
|
25
|
+
b=before[1]
|
|
26
|
+
|
|
27
|
+
return m*x+b
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
begin
|
|
2
|
+
# require File.dirname(__FILE__) + '/../../shattered_support/lib/shattered_support'
|
|
3
|
+
require 'shattered_support'
|
|
4
|
+
rescue MissingSourceFile
|
|
5
|
+
require 'rubygems'
|
|
6
|
+
require 'shattered_support'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require File.dirname(__FILE__) + '/base'
|
|
10
|
+
require File.dirname(__FILE__) + '/fuzzy_logic'
|
|
11
|
+
require File.dirname(__FILE__) + '/linear_interpolator'
|
metadata
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
rubygems_version: 0.8.10
|
|
3
|
+
specification_version: 1
|
|
4
|
+
name: shattered_model
|
|
5
|
+
version: !ruby/object:Gem::Version
|
|
6
|
+
version: "0.3"
|
|
7
|
+
date: 2006-04-23
|
|
8
|
+
summary: "Shattered Model: Provides basic commands any game object needs."
|
|
9
|
+
require_paths:
|
|
10
|
+
- lib
|
|
11
|
+
email:
|
|
12
|
+
homepage: http://www.hastilymade.com
|
|
13
|
+
rubyforge_project:
|
|
14
|
+
description: Shattered Model is the basis for the shattered powered game. It facilitatess the most testable and gameplay related code.
|
|
15
|
+
autorequire: shattered_model
|
|
16
|
+
default_executable:
|
|
17
|
+
bindir: bin
|
|
18
|
+
has_rdoc: "true"
|
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
-
|
|
22
|
+
- ">"
|
|
23
|
+
- !ruby/object:Gem::Version
|
|
24
|
+
version: 0.0.0
|
|
25
|
+
version:
|
|
26
|
+
platform: ruby
|
|
27
|
+
authors: []
|
|
28
|
+
files:
|
|
29
|
+
- lib/base.rb
|
|
30
|
+
- lib/fuzzy_logic.rb
|
|
31
|
+
- lib/linear_interpolator.rb
|
|
32
|
+
- lib/shattered_model.rb
|
|
33
|
+
- lib/shattered_model_tester.rb
|
|
34
|
+
test_files: []
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
extra_rdoc_files: []
|
|
37
|
+
executables: []
|
|
38
|
+
extensions: []
|
|
39
|
+
requirements: []
|
|
40
|
+
dependencies: []
|