Last active
December 11, 2015 09:08
-
-
Save s-shin/4577763 to your computer and use it in GitHub Desktop.
CodeVS2.0で書いたコード。あまりの遅さに愕然!
順位は論外なので秘密ですが、ものすごくRubyの勉強になったので良しとします。
This file contains 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
# encoding: utf-8 | |
# 出来る限りロジックに集中できるようにモジュール化する | |
# 座標系はxが→、yが↑で、左下が(0, 0)で統一する。 | |
# (つまりフィールドでもパックでも同じ) | |
# 2次元配列はy, xの順とする(ex. field[y][x])。 | |
# 依存モジュール | |
require 'matrix' # 回転で使用 | |
require 'logger' # デバッグログ出力で使用 | |
require 'thread' # 排他制御に必要 | |
# ## 名前空間 CodeVS2 | |
module CodeVS2 | |
class << self | |
# ### エントリーポイント | |
# - `case_param`: ケースごとに与えられる値P | |
# - `io_in`, `io_out`: 適宜ファイルに変更可能。 | |
def main(case_param, io_in=$stdin, io_out=$stdout) | |
# 一行目は各種情報 | |
line = io_in.gets().chomp! | |
width, height, size, sum, step = line.split(" ").map { |s| s.to_i } | |
# 二行目以降はパック。 | |
# NOTE: largeの時とか逐次読み込まないとダメ? | |
packs = Array.new step do | |
pack = Array.new size do | |
line = io_in.gets().chomp! | |
line.split(" ").map { |s| Cell.new s.to_i } | |
end | |
line = io_in.gets().chomp! | |
abort('恐らく入力ファイルが不正です') if line != 'END' | |
Pack.new pack.reverse # 座標系が↑→なので | |
end | |
# ゲームオブジェクトの作成 | |
game = Game.new width, height, sum, step, packs, case_param | |
while !game.pack_end? and !game.dead? | |
# ブロックで毎ターンごとのパックの配置処理をフック | |
pack, x = yield game, game.current_pack | |
game.update(pack, x) | |
io_out.puts "#{x} #{pack.r}" | |
io_out.flush | |
end | |
end | |
end | |
# ここまで、入出力依存のコード | |
#---------------------------------------------- | |
# ゲーム全体を管理するクラス | |
class Game | |
attr_accessor :field, :sum, :score, :chain_num, :current_step | |
protected :field=, :sum=, :chain_num=, :current_step= | |
# ログを利用するか | |
@@enable_log = true | |
# 点数を計算するか | |
@@enable_calc_score = false | |
# Game#fallにおけるスレッド | |
# 使わないなら`false`、使うならスレッド数を指定 | |
#@@thread_in_fall = 5 | |
@@thread_in_fall = false | |
# Game#deleteにおけるスレッド | |
# 有効にすると点数が若干狂うことを留意 | |
# 使わないなら`false`、使うなら`:by => :direction`などを指定。 | |
#@@thread_in_delete = { :by => :direction } | |
@@thread_in_delete = false | |
# ロガー | |
if @@enable_log | |
@@now = Time.now | |
@@log = Logger.new(open(File.expand_path(File.dirname(__FILE__) + | |
"/debug/#{@@now.to_i}.log"), "w")) | |
@@log.level = Logger::DEBUG # 適宜変更すること | |
@@log.formatter = proc { |severity, datetime, progname, msg| "#{msg}" } | |
end | |
#-------------------------------- | |
# #### コンストラクタ | |
# 最後の引数は複製用 | |
def initialize(width, height, sum, step, packs, case_param, copy=nil) | |
@sum, @max_step, @packs = sum, step, packs | |
@case_param = case_param | |
@chain_num = nil # 連鎖時に使用 | |
if !copy | |
@score = 0 | |
@current_step = 1 | |
@field = Field.new width, height, packs[0].size | |
if @@enable_log | |
@@log.info <<-EOT | |
Ruby v#{RUBY_VERSION} | |
#{@@now.to_s} | |
=== Basic Information === | |
W (width): #{width} | |
H (height): #{height} | |
T (pack size): #{@packs[0].size} | |
S (sum): #{@sum} | |
N (max step): #{@max_step} | |
P (case param): #{@case_param} | |
========================= | |
EOT | |
end | |
else | |
copy.each { |k, v| instance_variable_set(k, v) } | |
end | |
end | |
#-------------------------------- | |
# 全パック終了か | |
def pack_end?(n=0) | |
@current_step + n > @max_step | |
end | |
# 現在のパックの取得 | |
def current_pack | |
next_pack(0) | |
end | |
# 次以降のパックの取得 | |
def next_pack(n=1) | |
pack_end?(n) ? nil : @packs[@current_step+n-1] | |
end | |
#-------------------------------- | |
# ### 更新処理(置く+デバッグ) | |
# Game#forkして使うような時は、デバッグ出力が必要ないので | |
# 直接Game#putを呼び出すと良い。 | |
def update(pack, x) | |
if @@enable_log | |
start = Time.now | |
@@log.debug <<-EOT | |
turn: #{@current_step} | |
pack: | |
#{current_pack.to_s} | |
rotate: #{pack.r * 90} | |
x: #{x} | |
EOT | |
end | |
#--- | |
put(pack, x) | |
#--- | |
if @@enable_log | |
dt = ((Time.now - start) * 1000).round(3) | |
@@log.debug <<-EOT | |
--- | |
field: | |
#{@field.to_s} | |
score: #{@score} | |
chain num: #{@chain_num} | |
time: #{dt}ms | |
========================= | |
EOT | |
end | |
end | |
# 死亡か | |
def dead? | |
# 落ちない | |
# 消えない | |
# デッドライン超えてる | |
([email protected]).each do |x| | |
return true if @field[@field.real_height][x].block? | |
end | |
false | |
end | |
# 置く。 | |
def put(pack, x) | |
abort('置けません!') if @field.pack_outside?(pack, x) | |
# とりあえず置き、落とすフェーズへ | |
y = @field.real_height | |
pack.each_block do |dx, dy, block| | |
@field[y+dy][x+dx].set(block.val) | |
end | |
@chain_num = 0 # ここでチェーン数を初期化 | |
fall | |
@current_step += 1 | |
end | |
def deleted_num(pack, x) | |
g = fork | |
g.fall(false) | |
g.delete(true) | |
end | |
# ブロックを落とす | |
# NOTE: あまりに遅かったのでマルチスレッド化。ただここよりも消す部分の方が | |
# オーバヘッドが高そう。 | |
def fall(auto_delete=true) | |
# x列目についてブロックを落とす処理 | |
fall_x = Proc.new do |x| | |
bottom = 0 | |
catch :next_column do | |
@field.each_block_from_bottom(x) do |_, y, block| | |
# ない場所を探す | |
while @field[bottom][x].block? | |
bottom += 1 | |
throw :next_column if bottom == @field.height | |
end | |
# ない場所よりも下はどうでもいい | |
next if bottom > y | |
# 浮いたブロックを発見したので落とす | |
@field[bottom][x].set(block.val) | |
block.clear | |
end | |
end | |
end | |
# 一列づつ落とせるところまで落とす。 | |
unless @thread_in_fall | |
([email protected]).each { |x| fall_x.call(x) } | |
else | |
# 各処理は競合しないので排他制御しなくてよい。 | |
dx = @field.width / @@thread_in_fall # 切り捨てられる | |
begin_x = 0 | |
threads = [] | |
last_i = @@thread_in_fall - 1 # 次のループの最後のインデックス | |
@@thread_in_fall.times do |i| | |
# ちゃんと最後になるよう微調整 | |
end_x = (i == last_i) ? @field.width : begin_x + dx | |
# スレッド生成(クロージャではまずそうなので引数で渡す) | |
threads << Thread.new(begin_x, end_x) do |b, e| | |
(b...e).each { |x| fall_x.call(x) } | |
end | |
begin_x = end_x | |
end | |
threads.each { |t| t.join } # 実行と待機 | |
end | |
# 消去処理へ。消去があれば再度fallが呼ばれる(相互再帰)。 | |
delete if auto_delete | |
end | |
# 未チェックのブロックを中心に削除処理 | |
def delete(is_test=false) | |
count = 0 # 消滅カウント | |
if !@@thread_in_delete | |
# ブロックで未チェックの物をチェックすれば良い | |
@field.each_block_not_checked do |x, y, block| | |
block.checked = true | |
# 左右方向 | |
count += check_to_delete(is_test) { |dx| @field.get(x+dx, y) } | |
# 上下方向 | |
count += check_to_delete(is_test) { |dx| @field.get(x, y+dx) } | |
# 右斜め上方向 | |
count += check_to_delete(is_test) { |dx| @field.get(x+dx, y+dx) } | |
# 左斜め上方向 | |
count += check_to_delete(is_test) { |dx| @field.get(x-dx, y+dx) } | |
end | |
elsif @@thread_in_delete[:by] == :direction | |
# 上記のcheck_to_delete部分をスレッドにする。 | |
# 注:速くはなるが、点数が狂う可能性がある! | |
#(消滅カウントを正確に出すにはマルチスレッド化することができない。) | |
# もっとも、連鎖数には影響ないし、概算する分には問題ないだろう。 | |
locker = Mutex.new | |
threads = [ | |
proc { |x, y| check_to_delete { |dx| @field.get(x+dx, y) } }, | |
proc { |x, y| check_to_delete { |dx| @field.get(x, y+dx) } }, | |
proc { |x, y| check_to_delete { |dx| @field.get(x+dx, y+dx) } }, | |
proc { |x, y| check_to_delete { |dx| @field.get(x-dx, y+dx) } } | |
].map do |p| | |
Thread.new do | |
@field.each_block_not_checked do |x, y, block| | |
tmp = p.call(x, y) | |
locker.synchronize { count += tmp } | |
end | |
end | |
end | |
threads.each { |t| t.join } | |
elsif @@thread_in_delete[:by] == :region | |
locker = Mutex.new | |
#... | |
end | |
# 削除があるか | |
# 無いときは点数も計算する必要はない | |
if count > 0 | |
# チェックの付いたブロックを削除 | |
@field.each_block_to_be_deleted { |x, y, block| block.clear } | |
# チェーンをインクリメント | |
@chain_num += 1 | |
# 点数計算 | |
update_score count if @@enable_calc_score | |
# 相互再帰で続行 | |
fall | |
end | |
end | |
# 点数計算 | |
def update_score(e) | |
return if e == 0 | |
pn = @case_param + (@current_step-1) % 100 | |
@score += 2**([@chain_num, pn].min - 1) * [1, @chain_num-pn+1].max * e | |
end | |
# 周囲を調べる。 | |
# testがtrueの時は削除があるかどうかの確認だけ行い、 | |
# フィールドの書き換えは行わない。 | |
# 縦、横、斜め2つについて、何個の連結かに注目して処理すれば良い。 | |
# 結合の方向がblockを基準に2方向あるので、2次元的に考えれば良い。 | |
# ex. (左側, 右側) | |
# 1個 (0, 0) | |
# 2個 (0, 1), (1, 0) | |
# 3個 (0, 2), (1, 1), (2, 0) | |
# ... | |
# といのはやめて、右にずっと見れるところまで見て、見れなくなったら | |
# 左を見ていく方針で行く。 | |
def check_to_delete(is_test=false, &block) | |
count = 0 # 削除カウント | |
getCell = proc(&block) | |
left = 0 | |
right = 0 | |
100000.times do # とりあえず適当な回数 | |
# ### 範囲外、またはブロックがあるかどうかのチェック | |
# このアルゴリズムの場合、ブロックが無いところまで見ているということが | |
# 既にチェックが完了していることを意味する。 | |
# ブロックが無いところまで見たら左を見る | |
if !getCell.call(right) or getCell.call(right).none? | |
right -= 1 | |
left += 1 | |
next | |
end | |
# 左もないところまで見たら終了 | |
break if !getCell.call(-left) or getCell.call(-left).none? | |
# ### ブロックがある場合のチェック | |
# 範囲内の和を求める | |
sum = 0 | |
(-left..right).each { |dx| sum += getCell.call(dx).val } | |
# 基準値を超えていたら次のステップ | |
if sum >= @sum | |
# sumが一致していたら削除フラグを立てる | |
if sum == @sum | |
# テストならこの時点で終了 | |
return true if is_test | |
# 同時にここで点数計算のためにブロックの消滅カウントを出すが、 | |
# 既に全て削除フラグが立っていた場合は、カウント済みなので | |
# 無視する | |
already_deleted = true | |
(-left..right).each do |dx| | |
cell = getCell.call(dx) | |
if !cell.to_be_deleted | |
already_deleted = false | |
cell.to_be_deleted = true # 削除フラグ立てる | |
end | |
end | |
count += right + left + 1 unless already_deleted | |
end | |
# 左が詰まったらさらに右を減らしてもう一回トライ | |
if left > 0 | |
right -= 1 | |
next | |
end | |
# 左を見ていく | |
right -= 1 | |
left += 1 | |
else | |
# 第一段階(右を見ていく)ではleftが0 | |
if left == 0 | |
right += 1 | |
else | |
left += 1 | |
end | |
end | |
end | |
# テストならここに来た時点で削除がないのでfalseを返す | |
return false if is_test | |
count | |
end | |
#-------------------------------- | |
# ゲーム状態の複製 | |
def fork | |
Game.new \ | |
@field.width, @field.height, @sum, @max_step, @packs, @case_param, { | |
:@score => @score, | |
:@current_step => @current_step, | |
:@field => Marshal.load(Marshal.dump(@field)) | |
} | |
end | |
end | |
# 1マスを表すセル | |
class Cell | |
# NOTE: 消去処理の時に全座標を計算するのは現実的でない。 | |
# 消去される可能性があるのは、落下したブロックの周りだけであるので、 | |
# そのブロック基準に消去チェックされたかをcheckedに保持する。 | |
# checkedが変化するのは、新しいパックが落ちてきた時とブロックが消えることで | |
# 落ちてきた時(falseになる)、それとチェックが終わった時である(trueになる)。 | |
attr_accessor :val, :checked, :to_be_deleted | |
NONE = 0 | |
def initialize(val=NONE) | |
set(val) | |
end | |
def clear | |
set(NONE) | |
end | |
def set(val) | |
# Cellを渡しても上手いことする? | |
#val = val.val if val.methods.grep(:val).length > 0 | |
@val = val | |
@checked = false | |
@to_be_deleted = false | |
end | |
def none? | |
@val == NONE | |
end | |
def block? | |
!none? | |
end | |
def ==(cell) | |
@val == cell.val | |
end | |
# NOTE: お邪魔ブロック(obs)は、おそらく通常ブロックと一緒くたにして | |
# 処理できるので、判定処理をここでは書いてない。 | |
# 必要だとしても、グローバルな設定情報を必要とするので、 | |
# 設計が厄介になりそう。 | |
end | |
# 2次元セルテーブル | |
class CellTable | |
#include Enumerable | |
attr_accessor :cells | |
def width | |
@cells[0].length | |
end | |
def height | |
@cells.length | |
end | |
# CellTable#cellsでアクセスできるが、直接アクセスできるようにもする | |
def [](idx) | |
@cells[idx] | |
end | |
def outside?(x, y) | |
x < 0 || width <= x || y < 0 || height <= y | |
end | |
def get(x, y) | |
outside?(x, y) ? nil : @cells[y][x] | |
end | |
# 各列のブロック数のうち最大のもの | |
def max_y | |
counts = [] | |
(0...width).each do |x| | |
c = 0 | |
each_block_from_bottom(x) { c += 1 } | |
counts << c | |
end | |
counts.max | |
end | |
# 全てのセルをイテレート | |
def each | |
(0...height).each do |y| | |
(0...width).each do |x| | |
yield x, y, @cells[y][x] | |
end | |
end | |
end | |
# 各ブロックをイテレート | |
def each_block | |
each { |x, y, cell| yield x, y, cell if cell.block? } | |
end | |
# チェックされていないブロックをイテレート | |
def each_block_not_checked | |
each_block { |x, y, cell| yield x, y, cell if !cell.checked } | |
end | |
# 削除フラグの立ったブロックをイテレート | |
def each_block_to_be_deleted | |
each_block { |x, y, cell| yield x, y, cell if cell.to_be_deleted } | |
end | |
# x列目のブロックを下からイテレート | |
def each_block_from_bottom(x) | |
(0...height).each do |y| | |
yield x, y, @cells[y][x] if @cells[y][x].block? | |
end | |
end | |
# 比較 | |
def ==(table) | |
# サイズの比較 | |
return false if width != table.width or height != table.height | |
# 中身の比較 | |
(0...height).each do |y| | |
(0...width).each do |x| | |
return false if @cells[y][x] != table[y][x] | |
end | |
end | |
true | |
end | |
# デバッグに利用 | |
def to_s | |
ret = "" | |
(height-1).downto(0).each do |y| | |
(0...width).each do |x| | |
v = @cells[y][x].val | |
ret += "%2d " % v | |
end | |
ret += "\n" | |
end | |
ret | |
end | |
# 2次元配列をCellTable(Cellの2次元配列)に変換 | |
# NOTE: mixinされるクラスには追加されない | |
def self.from_a(arr) | |
arr.map { |row| row.map { |x| CodeVS2::Cell.new x } } | |
end | |
end | |
# パック | |
class Pack < CellTable | |
attr_accessor :r | |
protected :r= | |
# cells[y][x]: 2次元のCellの配列を指定 | |
def initialize(cells=nil) | |
@cells = cells | |
@r = 0 # 初期状態からの右90度回転数 | |
end | |
# 縦横同じ長さなので | |
def size | |
width | |
end | |
# rは右90度回転数。 | |
# NOTE: 配列を回すか、アクセス時にインデックスを変換するかの2通り実装が | |
# 考えられるが、回転のコストと、インデックス計算のオーバヘッドの | |
# どっちが大きいか。とりあえず回転させることにする。 | |
def rotate(r) | |
r = r % 4 # 0〜3に変換。負の時ちゃんと逆回転になる(ex: -1 % 4 = 3)。 | |
@r += r % 4 | |
return if r == 0 # 回転なし | |
# TODO: 速度に影響が出るようなら最適化する | |
# 回転角度 | |
rad = - Math::PI / 2 * r | |
# 回転行列 | |
mat = Matrix[ | |
[Math.cos(rad).round, -Math.sin(rad).round], | |
[Math.sin(rad).round, Math.cos(rad).round] | |
] | |
# 中央点 | |
o = Matrix[[(size-1)/2.0], [(size-1)/2.0]] | |
# 新しいパックセルテーブル | |
newCells = Array.new(size) { Array.new size } | |
each do |x, y, cell| | |
a = Matrix[[x], [y]] | |
oa = a - o # 中央からの相対座標にする | |
ob = mat * oa # 回転 | |
b = ob + o # 絶対座標に戻す | |
newCells[b[1, 0].round][b[0, 0].round] = cell | |
end | |
@cells = newCells # 置き換える | |
end | |
# 左端ブロックのx座標 | |
# NOTE: cellsのセット時に計算してしまえば毎回計算する必要はない。 | |
# よって、最適化の余地あり。 | |
def left_side | |
catch :end do | |
# 左から探索 | |
(0...size).each do |x| | |
(0...size).each do |y| | |
throw :end, x if @cells[y][x].block? | |
end | |
end | |
end | |
end | |
# 右端ブロックのx座標 | |
def right_side | |
catch :end do | |
# 右から探索 | |
(size-1).downto(0).each do |x| | |
(0...size).each do |y| | |
throw :end, x if @cells[y][x].block? | |
end | |
end | |
end | |
end | |
end | |
# フィールド | |
class Field < CellTable | |
attr_accessor :real_height | |
protected :cells=, :real_height= | |
def initialize(width, height, pack_size) | |
# パックサイズの分だけ高さを余分に確保する | |
@real_height = height | |
@cells = Array.new(height+pack_size) { Array.new(width) {Cell.new} } | |
end | |
# 端のチェック | |
def pack_outside?(pack, x) | |
0 > x + pack.left_side or x + pack.right_side >= width | |
end | |
end | |
end |
This file contains 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
# encoding: utf-8 | |
require File.expand_path(File.dirname(__FILE__) + '/codevs2') | |
# 動かさない | |
class Simple | |
def update(game, pack) | |
[pack, 0] | |
end | |
end | |
#------------------ | |
runner = Simple.new | |
param = ARGV[0] ? ARGV[0].to_i : 25 | |
CodeVS2.main(param) do |game, pack| | |
runner.update(game, pack) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment