oakdex-battle 0.0.1

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.
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