oakdex-battle 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ecd31f627a41112b511d36b3f229b4f39f16c50db2f30d2152d6d8be6c2a4084
4
+ data.tar.gz: c4d2ff60f0bc64a148b278f8dc9493a71191298672d0f365fa0a5ce2a3e8ab61
5
+ SHA512:
6
+ metadata.gz: eae5231ca258390c2d3b3f2af6979b34f66c9615ed6e55c7c636d96267cdae3f76a8a86475f19a819aa0bdda1746f6b3efb8f84c0547cbb7290099efd6889ba4
7
+ data.tar.gz: 5a22eb518390c80e4eb7d5ab086ef744588a11de5260043050958a2930286dca62a88efe27f957bd6fccdc2c47c397325a11f5fa143fc5ff6d04399d4f11a2b5
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # <img src="https://v20.imgup.net/oakdex_logfbad.png" alt="fixer" width=282>
2
+
3
+ [![Build Status](https://travis-ci.org/jalyna/oakdex-battle.svg?branch=master)](https://travis-ci.org/jalyna/oakdex-battle) [![Maintainability](https://api.codeclimate.com/v1/badges/ef91681257a6900f03ac/maintainability)](https://codeclimate.com/github/jalyna/oakdex-battle/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/ef91681257a6900f03ac/test_coverage)](https://codeclimate.com/github/jalyna/oakdex-battle/test_coverage)
4
+
5
+ Based on [oakdex-pokedex](https://github.com/jalyna/oakdex-pokedex).
6
+
7
+ ## Getting Started
8
+
9
+ ### 1 vs. 1
10
+
11
+ ```ruby
12
+ require 'oakdex/battle'
13
+
14
+ pok1 = Oakdex::Battle::Pokemon.create('Pikachu', level: 10)
15
+ pok2 = Oakdex::Battle::Pokemon.create('Bulbasaur', {
16
+ exp: 120,
17
+ gender: 'female',
18
+ ability: 'Soundproof',
19
+ nature: 'Bashful',
20
+ item: 'Earth Plate',
21
+ hp: 2,
22
+ iv: {
23
+ hp: 8,
24
+ atk: 12,
25
+ def: 31,
26
+ sp_atk: 12,
27
+ sp_def: 5,
28
+ speed: 14
29
+ },
30
+ ev: {
31
+ hp: 8,
32
+ atk: 12,
33
+ def: 99,
34
+ sp_atk: 4,
35
+ sp_def: 12,
36
+ speed: 14
37
+ },
38
+ moves: [
39
+ ['Swords Dance', 12, 30],
40
+ ['Cut', 40, 44]
41
+ ]
42
+ })
43
+
44
+ trainer1 = Oakdex::Battle::Trainer.new('Ash', [pok1])
45
+ trainer2 = Oakdex::Battle::Trainer.new('Misty', [pok2])
46
+
47
+ battle = Oakdex::Battle.new(trainer1, trainer2)
48
+ battle.continue # => true
49
+ battle.log.size # => 1
50
+ battle.log.last # => [['sends_to_battle', 'Ash', 'Pikachu'], ['sends_to_battle', 'Misty', 'Bulbasaur']]
51
+ battle.arena # => Snapshot of current state as Hash
52
+ battle.finished? # => false
53
+ battle.valid_actions_for(trainer1) # => [{ action: 'move', pokemon: pok1, move: <Move>, target: pok2 }, ...]
54
+
55
+ battle.add_action(trainer1, { action: 'move', pokemon: pok1, move: <Move>, target: pok2 }) # => false
56
+
57
+ battle.add_action(trainer1, { action: 'move', pokemon: pok1, move: <Move>, target: pok2 }) # => true
58
+
59
+ battle.valid_actions_for(trainer1) # => []
60
+ battle.continue # => false
61
+ battle.simulate_action(trainer2) # => true
62
+ battle.valid_actions_for(trainer2) # => []
63
+ battle.continue # => true
64
+
65
+ battle.log.size # => 2
66
+ battle.log.last # => [['uses_move', 'Ash', 'Pikachu', 'Thunder Shock'], ['received_damage', 'Misty', 'Bulbasaur', 'Thunder Shock'], ['uses_move', 'Misty', 'Bulbasaur', 'Leech Seed'], ['move_failed', 'Misty', 'Bulbasaur', 'Leech Seed']]
67
+
68
+ # ...
69
+
70
+ battle.finished? # => true
71
+ battle.winner # => trainer1
72
+ ```
73
+
74
+
75
+ ### Other Battle types
76
+
77
+ ```ruby
78
+ pok3 = Oakdex::Battle::Pokemon.create('Altaria', level: 20)
79
+ pok4 = Oakdex::Battle::Pokemon.create('Elekid', level: 14)
80
+ trainer1 = Oakdex::Battle::Trainer.new('Ash', [pok1, pok3, pok9])
81
+ trainer2 = Oakdex::Battle::Trainer.new('Misty', [pok2, pok4, pok10])
82
+ trainer3 = Oakdex::Battle::Trainer.new('Brock', [pok5, pok6])
83
+ trainer4 = Oakdex::Battle::Trainer.new('Erika', [pok7, pok8])
84
+
85
+ # 2 vs. 2
86
+ battle = Oakdex::Battle.new([trainer1], [trainer2], pokemon_per_side: 2)
87
+ # 3 vs. 3
88
+ battle = Oakdex::Battle.new([trainer1], [trainer2], pokemon_per_side: 3)
89
+ # 2 vs. 2 (1 pokemon each trainer)
90
+ battle = Oakdex::Battle.new([trainer1, trainer3], [trainer2, trainer4])
91
+ ```
92
+
93
+ ## Contributing
94
+
95
+ I would be happy if you want to add your contribution to the project. In order to contribute, you just have to fork this repository.
96
+
97
+ ## License
98
+
99
+ MIT License. See the included MIT-LICENSE file.
100
+
101
+ ## Credits
102
+
103
+ Logo Icon by [Roundicons Freebies](http://www.flaticon.com/authors/roundicons-freebies).
data/lib/oakdex.rb ADDED
@@ -0,0 +1,3 @@
1
+ # Main Namespace Oakdex
2
+ module Oakdex
3
+ end
@@ -0,0 +1,111 @@
1
+ require 'oakdex/pokedex'
2
+
3
+ require 'oakdex/battle/pokemon'
4
+ require 'oakdex/battle/trainer'
5
+ require 'oakdex/battle/move_execution'
6
+ require 'oakdex/battle/action'
7
+ require 'oakdex/battle/damage'
8
+ require 'oakdex/battle/turn'
9
+ require 'oakdex/battle/valid_action_service'
10
+ require 'oakdex/battle/side'
11
+ require 'oakdex/battle/in_battle_pokemon'
12
+
13
+ module Oakdex
14
+ # Represents battle, with has n turns and m sides
15
+ class Battle
16
+ attr_reader :log, :actions, :team1, :team2,
17
+ :sides, :current_log
18
+
19
+ def initialize(team1, team2, options = {})
20
+ @team1 = team1.is_a?(Array) ? team1 : [team1]
21
+ @team2 = team2.is_a?(Array) ? team2 : [team2]
22
+ @options = options
23
+ @sides = []
24
+ @log = []
25
+ @current_log = []
26
+ @actions = []
27
+ @turns = []
28
+ end
29
+
30
+ def pokemon_per_side
31
+ @options[:pokemon_per_side] || @team1.size
32
+ end
33
+
34
+ def arena
35
+ { sides: sides }
36
+ end
37
+
38
+ def valid_actions_for(trainer)
39
+ valid_action_service.valid_actions_for(trainer)
40
+ end
41
+
42
+ def add_action(trainer, action)
43
+ return false unless valid_actions_for(trainer).include?(action)
44
+ @actions << Action.new(trainer, action)
45
+ true
46
+ end
47
+
48
+ def simulate_action(trainer)
49
+ valid_actions = valid_actions_for(trainer)
50
+ return false if valid_actions.empty?
51
+ add_action(trainer, valid_actions.sample)
52
+ end
53
+
54
+ def continue
55
+ return start if sides.empty?
56
+ return false unless trainers.all? { |t| valid_actions_for(t).empty? }
57
+ execute_actions
58
+ true
59
+ end
60
+
61
+ def finished?
62
+ !fainted_sides.empty?
63
+ end
64
+
65
+ def winner
66
+ return if fainted_sides.empty?
67
+ (sides - fainted_sides).flat_map(&:trainers)
68
+ end
69
+
70
+ def add_to_log(*args)
71
+ @current_log << args.to_a
72
+ end
73
+
74
+ def remove_fainted
75
+ sides.each(&:remove_fainted)
76
+ end
77
+
78
+ private
79
+
80
+ def valid_action_service
81
+ @valid_action_service ||= ValidActionService.new(self)
82
+ end
83
+
84
+ def fainted_sides
85
+ sides.select(&:fainted?)
86
+ end
87
+
88
+ def start
89
+ @sides = [@team1, @team2].map do |team|
90
+ Side.new(self, team).tap(&:send_to_battle)
91
+ end
92
+ finish_turn
93
+ true
94
+ end
95
+
96
+ def execute_actions
97
+ @turns << Turn.new(self, @actions).tap(&:execute)
98
+ finish_turn
99
+ end
100
+
101
+ def trainers
102
+ sides.flat_map(&:trainers)
103
+ end
104
+
105
+ def finish_turn
106
+ @log << @current_log
107
+ @current_log = []
108
+ @actions = []
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,115 @@
1
+ require 'forwardable'
2
+
3
+ module Oakdex
4
+ class Battle
5
+ # Represents one Action. One turn has many actions.
6
+ class Action
7
+ RECALL_PRIORITY = 6
8
+
9
+ extend Forwardable
10
+
11
+ def_delegators :@turn, :battle
12
+
13
+ attr_reader :trainer, :damage, :turn
14
+
15
+ def initialize(trainer, attributes)
16
+ @trainer = trainer
17
+ @attributes = attributes
18
+ end
19
+
20
+ def priority
21
+ move&.priority || RECALL_PRIORITY
22
+ end
23
+
24
+ def pokemon
25
+ recall? ? pokemon_by_position : @attributes[:pokemon]
26
+ end
27
+
28
+ def pokemon_position
29
+ recall? ? @attributes[:pokemon] : nil
30
+ end
31
+
32
+ def target
33
+ recall? ? @attributes[:target] : targets
34
+ end
35
+
36
+ def type
37
+ @attributes[:action]
38
+ end
39
+
40
+ def move
41
+ @attributes[:move]
42
+ end
43
+
44
+ def hitting_probability
45
+ ((move.accuracy / 100.0) * (pokemon.accuracy / target.evasion)) * 1000
46
+ end
47
+
48
+ def hitting?
49
+ @hitting = rand(1..1000) <= hitting_probability ? 1 : 0
50
+ @hitting == 1
51
+ end
52
+
53
+ def execute(turn)
54
+ @turn = turn
55
+ return execute_recall if type == 'recall'
56
+ targets.each { |t| MoveExecution.new(self, t).execute }
57
+ end
58
+
59
+ private
60
+
61
+ def targets
62
+ target_list.map do |target|
63
+ target_by_position(target[0], target[1])
64
+ end.compact
65
+ end
66
+
67
+ def target_list
68
+ list = @attributes[:target]
69
+ reutrn [] if list.empty?
70
+ list = [list] unless list[0].is_a?(Array)
71
+ list
72
+ end
73
+
74
+ def recall?
75
+ type == 'recall'
76
+ end
77
+
78
+ def pokemon_by_position
79
+ trainer.in_battle_pokemon
80
+ .find { |ibp| ibp.position == @attributes[:pokemon] }&.pokemon
81
+ end
82
+
83
+ def target_by_position(side, position)
84
+ side.in_battle_pokemon
85
+ .find { |ibp| ibp.position == position }&.pokemon
86
+ end
87
+
88
+ def side
89
+ battle.sides.find { |s| s.trainer_on_side?(trainer) }
90
+ end
91
+
92
+ def execute_recall
93
+ add_recalls_log
94
+ if pokemon
95
+ trainer.remove_from_battle(pokemon, side)
96
+ trainer.send_to_battle(target, side)
97
+ else
98
+ trainer.send_to_battle(target, side)
99
+ end
100
+ end
101
+
102
+ def add_log(*args)
103
+ battle.add_to_log(*args)
104
+ end
105
+
106
+ def add_recalls_log
107
+ if pokemon
108
+ add_log 'recalls', trainer.name, pokemon.name, target.name
109
+ else
110
+ add_log 'recalls_for_fainted', trainer.name, target.name
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,99 @@
1
+ require 'forwardable'
2
+
3
+ module Oakdex
4
+ class Battle
5
+ # Calculates damage
6
+ class Damage
7
+ extend Forwardable
8
+
9
+ def_delegators :@action, :move, :pokemon, :target
10
+
11
+ def initialize(turn, action)
12
+ @turn = turn
13
+ @action = action
14
+ end
15
+
16
+ def damage
17
+ (simple_damage * modifier).to_i
18
+ end
19
+
20
+ def critical?
21
+ @critical ||= rand(1..1000) <= pokemon.critical_hit_prob * 1000 ? 1 : 0
22
+ @critical == 1
23
+ end
24
+
25
+ def effective?
26
+ type_modifier > 1.0
27
+ end
28
+
29
+ def ineffective?
30
+ type_modifier < 1.0
31
+ end
32
+
33
+ private
34
+
35
+ def modifier
36
+ target_modifier * weather_modifier * critical_hit_modifier *
37
+ random_modifier * stab_modifier * type_modifier *
38
+ burn_modifier * status_condition_modifier * other_modifiers
39
+ end
40
+
41
+ def status_condition_modifier
42
+ pokemon.status_conditions.reduce(1.0) do |modifier, condition|
43
+ modifier * condition.damage_modifier(@action)
44
+ end
45
+ end
46
+
47
+ def target_modifier
48
+ 1.0 # TODO: 0.75 if move has more than one target
49
+ end
50
+
51
+ def weather_modifier
52
+ 1.0 # TODO: 1.5 water at rain or fire at harsh sunlight, 0.5 vice versa
53
+ end
54
+
55
+ def critical_hit_modifier
56
+ if critical?
57
+ 1.5
58
+ else
59
+ 1.0
60
+ end
61
+ end
62
+
63
+ def random_modifier
64
+ @random_modifier ||= rand(850..1000) / 1000.0
65
+ end
66
+
67
+ def stab_modifier
68
+ pokemon.types.include?(move.type) ? 1.5 : 1.0
69
+ end
70
+
71
+ def type_modifier
72
+ type_data = Oakdex::Pokedex::Type.find!(move.type)
73
+ target.types.reduce(1.0) do |factor, type|
74
+ factor * type_data.effectivness[type]
75
+ end
76
+ end
77
+
78
+ def burn_modifier
79
+ 1.0 # TODO: 0.5 if attack is burning and physical move
80
+ end
81
+
82
+ def other_modifiers
83
+ 1.0 # TODO: See other https://bulbapedia.bulbagarden.net/wiki/Damage
84
+ end
85
+
86
+ def def_and_atk
87
+ if move.category == 'special'
88
+ (pokemon.sp_atk.to_f / target.sp_def.to_f)
89
+ else
90
+ (pokemon.atk.to_f / target.def.to_f)
91
+ end
92
+ end
93
+
94
+ def simple_damage
95
+ (((2 * pokemon.level) / 5.0 + 2) * move.power * def_and_atk) / 50 + 2
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,159 @@
1
+ require 'forwardable'
2
+
3
+ module Oakdex
4
+ class Battle
5
+ # Represents a pokemon that is in battle
6
+ class InBattlePokemon
7
+ extend Forwardable
8
+
9
+ def_delegators :@pokemon, :current_hp, :moves_with_pp
10
+ def_delegators :@side, :battle
11
+
12
+ attr_reader :pokemon, :position, :side
13
+
14
+ def initialize(pokemon, side, position = 0)
15
+ @pokemon = pokemon
16
+ @side = side
17
+ @position = position
18
+ end
19
+
20
+ def fainted?
21
+ current_hp.zero?
22
+ end
23
+
24
+ def action_added?
25
+ actions.any? { |a| a.pokemon == pokemon }
26
+ end
27
+
28
+ def valid_move_actions
29
+ return [] if action_added?
30
+ moves = moves_with_pp
31
+ moves = [struggle_move] if moves_with_pp.empty?
32
+ moves.flat_map do |move|
33
+ targets_in_battle(move).map do |target|
34
+ {
35
+ action: 'move',
36
+ pokemon: pokemon,
37
+ move: move,
38
+ target: target
39
+ }
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def targets_in_battle(move)
47
+ available_targets(move).map do |targets|
48
+ if targets.last.is_a?(Array)
49
+ targets if targets_in_battle?(targets)
50
+ elsif target_in_battle?(targets)
51
+ targets
52
+ end
53
+ end.compact.reject(&:empty?)
54
+ end
55
+
56
+ def targets_in_battle?(targets)
57
+ targets.any? { |target| target[0].pokemon_in_battle?(target[1]) }
58
+ end
59
+
60
+ def target_in_battle?(target)
61
+ target[0].pokemon_in_battle?(target[1]) ||
62
+ (!target[0].pokemon_left? && target[1] == 0)
63
+ end
64
+
65
+ def struggle_move
66
+ @struggle_move ||= begin
67
+ move_type = Oakdex::Pokedex::Move.find('Struggle')
68
+ Oakdex::Battle::Move.new(move_type, move_type.pp, move_type.pp)
69
+ end
70
+ end
71
+
72
+ def available_targets(move)
73
+ with_target(move) || multiple_targets_adjacent(move) ||
74
+ multiple_targets(move) || []
75
+ end
76
+
77
+ def multiple_targets(move)
78
+ case move.target
79
+ when 'all_users' then [all_users]
80
+ when 'all_except_user' then [all_targets - [self_target]]
81
+ when 'all' then [all_targets]
82
+ when 'all_foes' then [all_foes]
83
+ end
84
+ end
85
+
86
+ def multiple_targets_adjacent(move)
87
+ case move.target
88
+ when 'all_adjacent' then [adjacent]
89
+ when 'adjacent_foes_all' then [adjacent_foes]
90
+ end
91
+ end
92
+
93
+ def with_target(move)
94
+ case move.target
95
+ when 'user', 'user_and_random_adjacent_foe' then [self_target]
96
+ when 'target_adjacent_user_single' then adjacent_users
97
+ when 'target_adjacent_single' then adjacent
98
+ when 'target_user_or_adjacent_user'
99
+ [self_target] + adjacent_users
100
+ end
101
+ end
102
+
103
+ def all_targets
104
+ all_foes + all_users
105
+ end
106
+
107
+ def target_adjacent_single
108
+ adjacent_foes + adjacent_users
109
+ end
110
+
111
+ def adjacent
112
+ adjacent_foes + adjacent_users
113
+ end
114
+
115
+ def self_target
116
+ [@side, position]
117
+ end
118
+
119
+ def adjacent_foes
120
+ [
121
+ [other_side, position - 1],
122
+ [other_side, position],
123
+ [other_side, position + 1]
124
+ ].select { |t| t[1] >= 0 && t[1] < pokemon_per_side }
125
+ end
126
+
127
+ def adjacent_users
128
+ [
129
+ [@side, position - 1],
130
+ [@side, position + 1]
131
+ ].select { |t| t[1] >= 0 && t[1] < pokemon_per_side }
132
+ end
133
+
134
+ def all_users
135
+ pokemon_per_side.times.map { |i| [@side, i] }
136
+ end
137
+
138
+ def all_foes
139
+ pokemon_per_side.times.map { |i| [other_side, i] }
140
+ end
141
+
142
+ def pokemon_per_side
143
+ battle.pokemon_per_side
144
+ end
145
+
146
+ def other_side
147
+ other_sides.first
148
+ end
149
+
150
+ def other_sides
151
+ battle.sides - [@side]
152
+ end
153
+
154
+ def actions
155
+ battle.actions
156
+ end
157
+ end
158
+ end
159
+ end